summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile25
-rw-r--r--api/api.go43
-rw-r--r--api/context.go137
-rw-r--r--api/license.go20
-rw-r--r--api/license_test.go22
-rw-r--r--api/oauth.go343
-rw-r--r--api/post.go7
-rw-r--r--api/team.go114
-rw-r--r--api/team_test.go111
-rw-r--r--api/templates/email_change_subject.html1
-rw-r--r--api/templates/email_change_verify_subject.html1
-rw-r--r--api/templates/error.html37
-rw-r--r--api/templates/find_teams_body.html52
-rw-r--r--api/templates/find_teams_subject.html1
-rw-r--r--api/templates/post_subject.html1
-rw-r--r--api/user.go146
-rw-r--r--api/user_test.go35
-rw-r--r--i18n/en.json28
-rw-r--r--i18n/es.json20
-rw-r--r--model/client.go67
-rw-r--r--model/session.go2
-rw-r--r--model/team.go7
-rw-r--r--templates/authorize.html (renamed from web/templates/authorize.html)0
-rw-r--r--templates/email_change_body.html (renamed from api/templates/email_change_body.html)0
-rw-r--r--templates/email_change_subject.html1
-rw-r--r--templates/email_change_verify_body.html (renamed from api/templates/email_change_verify_body.html)0
-rw-r--r--templates/email_change_verify_subject.html1
-rw-r--r--templates/email_footer.html (renamed from api/templates/email_footer.html)0
-rw-r--r--templates/email_info.html (renamed from api/templates/email_info.html)0
-rw-r--r--templates/error.html24
-rw-r--r--templates/head.html92
-rw-r--r--templates/invite_body.html (renamed from api/templates/invite_body.html)0
-rw-r--r--templates/invite_subject.html (renamed from api/templates/invite_subject.html)0
-rw-r--r--templates/password_change_body.html (renamed from api/templates/password_change_body.html)0
-rw-r--r--templates/password_change_subject.html (renamed from api/templates/password_change_subject.html)0
-rw-r--r--templates/post_body.html (renamed from api/templates/post_body.html)0
-rw-r--r--templates/post_subject.html1
-rw-r--r--templates/reset_body.html (renamed from api/templates/reset_body.html)0
-rw-r--r--templates/reset_subject.html (renamed from api/templates/reset_subject.html)0
-rw-r--r--templates/root.html12
-rw-r--r--templates/signin_change_body.html (renamed from api/templates/signin_change_body.html)0
-rw-r--r--templates/signin_change_subject.html (renamed from api/templates/signin_change_subject.html)0
-rw-r--r--templates/signup_team_body.html (renamed from api/templates/signup_team_body.html)0
-rw-r--r--templates/signup_team_subject.html (renamed from api/templates/signup_team_subject.html)0
-rw-r--r--templates/verify_body.html (renamed from api/templates/verify_body.html)0
-rw-r--r--templates/verify_subject.html (renamed from api/templates/verify_subject.html)0
-rw-r--r--templates/welcome_body.html (renamed from api/templates/welcome_body.html)0
-rw-r--r--templates/welcome_subject.html (renamed from api/templates/welcome_subject.html)0
-rw-r--r--utils/html.go97
-rw-r--r--utils/license.go2
-rw-r--r--web/react/action_creators/global_actions.jsx (renamed from web/react/dispatcher/event_helpers.jsx)30
-rw-r--r--web/react/components/activity_log_modal.jsx36
-rw-r--r--web/react/components/admin_console/admin_controller.jsx11
-rw-r--r--web/react/components/admin_console/admin_navbar_dropdown.jsx19
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx4
-rw-r--r--web/react/components/admin_console/admin_sidebar_header.jsx5
-rw-r--r--web/react/components/admin_console/user_item.jsx2
-rw-r--r--web/react/components/audit_table.jsx21
-rw-r--r--web/react/components/center_panel.jsx47
-rw-r--r--web/react/components/channel_header.jsx32
-rw-r--r--web/react/components/channel_invite_modal.jsx37
-rw-r--r--web/react/components/channel_loader.jsx204
-rw-r--r--web/react/components/channel_notifications_modal.jsx107
-rw-r--r--web/react/components/channel_view.jsx26
-rw-r--r--web/react/components/claim/claim_account.jsx87
-rw-r--r--web/react/components/claim/sso_to_email.jsx2
-rw-r--r--web/react/components/create_post.jsx6
-rw-r--r--web/react/components/delete_channel_modal.jsx4
-rw-r--r--web/react/components/do_verify_email.jsx82
-rw-r--r--web/react/components/docs.jsx41
-rw-r--r--web/react/components/edit_post_modal.jsx4
-rw-r--r--web/react/components/email_verify.jsx108
-rw-r--r--web/react/components/file_attachment.jsx4
-rw-r--r--web/react/components/find_team.jsx135
-rw-r--r--web/react/components/invite_member_modal.jsx4
-rw-r--r--web/react/components/logged_in.jsx224
-rw-r--r--web/react/components/login.jsx222
-rw-r--r--web/react/components/login_email.jsx11
-rw-r--r--web/react/components/navbar.jsx20
-rw-r--r--web/react/components/navbar_dropdown.jsx88
-rw-r--r--web/react/components/needs_team.jsx20
-rw-r--r--web/react/components/not_logged_in.jsx70
-rw-r--r--web/react/components/password_reset.jsx47
-rw-r--r--web/react/components/password_reset_form.jsx105
-rw-r--r--web/react/components/password_reset_send_link.jsx186
-rw-r--r--web/react/components/popover_list_members.jsx2
-rw-r--r--web/react/components/post.jsx19
-rw-r--r--web/react/components/post_body.jsx10
-rw-r--r--web/react/components/post_focus_view.jsx22
-rw-r--r--web/react/components/post_header.jsx17
-rw-r--r--web/react/components/post_info.jsx56
-rw-r--r--web/react/components/posts_view.jsx17
-rw-r--r--web/react/components/posts_view_container.jsx19
-rw-r--r--web/react/components/rhs_comment.jsx8
-rw-r--r--web/react/components/rhs_root_post.jsx8
-rw-r--r--web/react/components/root.jsx90
-rw-r--r--web/react/components/search_results_item.jsx9
-rw-r--r--web/react/components/should_verify_email.jsx111
-rw-r--r--web/react/components/sidebar.jsx30
-rw-r--r--web/react/components/sidebar_header.jsx14
-rw-r--r--web/react/components/sidebar_right.jsx9
-rw-r--r--web/react/components/sidebar_right_menu.jsx39
-rw-r--r--web/react/components/signup_team.jsx159
-rw-r--r--web/react/components/signup_team_complete.jsx121
-rw-r--r--web/react/components/signup_team_complete/components/signup_team_complete.jsx79
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx (renamed from web/react/components/team_signup_display_name_page.jsx)6
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_email_item.jsx (renamed from web/react/components/team_signup_email_item.jsx)2
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_finished.jsx15
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_password_page.jsx (renamed from web/react/components/team_signup_password_page.jsx)15
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx (renamed from web/react/components/team_signup_send_invites_page.jsx)2
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_url_page.jsx (renamed from web/react/components/team_signup_url_page.jsx)8
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_username_page.jsx (renamed from web/react/components/team_signup_username_page.jsx)8
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx (renamed from web/react/components/team_signup_welcome_page.jsx)12
-rw-r--r--web/react/components/signup_team_confirm.jsx47
-rw-r--r--web/react/components/signup_user_complete.jsx331
-rw-r--r--web/react/components/suggestion/at_mention_provider.jsx2
-rw-r--r--web/react/components/suggestion/suggestion_box.jsx12
-rw-r--r--web/react/components/suggestion/suggestion_list.jsx4
-rw-r--r--web/react/components/team_members_modal.jsx32
-rw-r--r--web/react/components/team_settings.jsx3
-rw-r--r--web/react/components/team_signup_with_email.jsx7
-rw-r--r--web/react/components/user_list_row.jsx2
-rw-r--r--web/react/components/user_profile.jsx29
-rw-r--r--web/react/components/user_settings/manage_languages.jsx3
-rw-r--r--web/react/components/user_settings/user_settings_developer.jsx4
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx15
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx16
-rw-r--r--web/react/components/user_settings/user_settings_security.jsx29
-rw-r--r--web/react/package.json2
-rw-r--r--web/react/pages/admin_console.jsx71
-rw-r--r--web/react/pages/channel.jsx97
-rw-r--r--web/react/pages/claim_account.jsx68
-rw-r--r--web/react/pages/docs.jsx64
-rw-r--r--web/react/pages/find_team.jsx62
-rw-r--r--web/react/pages/home.jsx16
-rw-r--r--web/react/pages/login.jsx66
-rw-r--r--web/react/pages/password_reset.jsx68
-rw-r--r--web/react/pages/root.jsx290
-rw-r--r--web/react/pages/signup_team.jsx76
-rw-r--r--web/react/pages/signup_team_complete.jsx66
-rw-r--r--web/react/pages/signup_team_confirm.jsx64
-rw-r--r--web/react/pages/signup_user_complete.jsx69
-rw-r--r--web/react/pages/verify.jsx67
-rw-r--r--web/react/stores/admin_store.jsx10
-rw-r--r--web/react/stores/analytics_store.jsx4
-rw-r--r--web/react/stores/browser_store.jsx6
-rw-r--r--web/react/stores/channel_store.jsx4
-rw-r--r--web/react/stores/file_store.jsx7
-rw-r--r--web/react/stores/localization_store.jsx60
-rw-r--r--web/react/stores/modal_store.jsx4
-rw-r--r--web/react/stores/post_store.jsx4
-rw-r--r--web/react/stores/search_store.jsx4
-rw-r--r--web/react/stores/socket_store.jsx14
-rw-r--r--web/react/stores/suggestion_store.jsx7
-rw-r--r--web/react/stores/team_store.jsx42
-rw-r--r--web/react/stores/user_store.jsx66
-rw-r--r--web/react/utils/async_client.jsx55
-rw-r--r--web/react/utils/channel_intro_messages.jsx49
-rw-r--r--web/react/utils/client.jsx179
-rw-r--r--web/react/utils/constants.jsx5
-rw-r--r--web/react/utils/utils.jsx33
-rw-r--r--web/sass-files/sass/partials/_sidebar--left.scss6
-rw-r--r--web/static/help/Messaging_en.md47
-rw-r--r--web/static/help/Messaging_es.md37
-rw-r--r--web/static/i18n/en.json17
-rw-r--r--web/static/i18n/es.json14
-rw-r--r--web/static/i18n/pt.json5
-rw-r--r--web/templates/admin_console.html21
-rw-r--r--web/templates/channel.html21
-rw-r--r--web/templates/claim_account.html30
-rw-r--r--web/templates/docs.html27
-rw-r--r--web/templates/find_team.html30
-rw-r--r--web/templates/footer.html39
-rw-r--r--web/templates/head.html191
-rw-r--r--web/templates/home.html24
-rw-r--r--web/templates/login.html27
-rw-r--r--web/templates/password_reset.html30
-rw-r--r--web/templates/signup_team.html29
-rw-r--r--web/templates/signup_team_complete.html29
-rw-r--r--web/templates/signup_team_confirm.html26
-rw-r--r--web/templates/signup_user_complete.html29
-rw-r--r--web/templates/verify.html30
-rw-r--r--web/web.go1158
183 files changed, 3426 insertions, 5084 deletions
diff --git a/Makefile b/Makefile
index a7c277e4c..57a28bf3a 100644
--- a/Makefile
+++ b/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'
diff --git a/api/api.go b/api/api.go
index 4fecd3dd4..20f77e558 100644
--- a/api/api.go
+++ b/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 {
diff --git a/api/context.go b/api/context.go
index edcdcbfef..eed035daf 100644
--- a/api/context.go
+++ b/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))
}
diff --git a/api/license.go b/api/license.go
index 23e7946c8..542b45e26 100644
--- a/api/license.go
+++ b/api/license.go
@@ -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)))
+}
diff --git a/api/license_test.go b/api/license_test.go
new file mode 100644
index 000000000..b34aeb7a6
--- /dev/null
+++ b/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)
+ }
+ }
+}
diff --git a/api/oauth.go b/api/oauth.go
index 1ae3dbf78..9b7f3699d 100644
--- a/api/oauth.go
+++ b/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)
+ }
+}
diff --git a/api/post.go b/api/post.go
index cd78b16f0..d0ec5826a 100644
--- a/api/post.go
+++ b/api/post.go
@@ -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
diff --git a/api/team.go b/api/team.go
index 2f680dc76..255982522 100644
--- a/api/team.go
+++ b/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)))
+ }
+}
diff --git a/api/team_test.go b/api/team_test.go
index c942e2e1f..bbbc8385d 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -108,49 +108,36 @@ func TestCreateTeam(t *testing.T) {
}
}
-func TestFindTeamByEmail(t *testing.T) {
+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"}
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 {
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ 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].DisplayName != team.DisplayName {
- t.Fatal()
+ if teams[team.Id].Email != "" {
+ t.Fatal("Non admin users shoudn't get full listings")
}
}
- 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 = 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.GetAllTeams(); err == nil {
- t.Fatal("you shouldn't have permissions")
- }
-
c := &Context{}
c.RequestId = model.NewId()
c.IpAddress = "cmd_line"
@@ -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()
diff --git a/api/templates/email_change_subject.html b/api/templates/email_change_subject.html
deleted file mode 100644
index afabc2191..000000000
--- a/api/templates/email_change_subject.html
+++ /dev/null
@@ -1 +0,0 @@
-{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/templates/email_change_verify_subject.html b/api/templates/email_change_verify_subject.html
deleted file mode 100644
index 4fc4f4846..000000000
--- a/api/templates/email_change_verify_subject.html
+++ /dev/null
@@ -1 +0,0 @@
-{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/templates/error.html b/api/templates/error.html
deleted file mode 100644
index 2f588aead..000000000
--- a/api/templates/error.html
+++ /dev/null
@@ -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>
diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html
deleted file mode 100644
index 1324091aa..000000000
--- a/api/templates/find_teams_body.html
+++ /dev/null
@@ -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}}
-
-
-
diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html
deleted file mode 100644
index ebc339562..000000000
--- a/api/templates/find_teams_subject.html
+++ /dev/null
@@ -1 +0,0 @@
-{{define "find_teams_subject"}}{{.Props.Subject}}{{end}}
diff --git a/api/templates/post_subject.html b/api/templates/post_subject.html
deleted file mode 100644
index 60daaa432..000000000
--- a/api/templates/post_subject.html
+++ /dev/null
@@ -1 +0,0 @@
-{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/user.go b/api/user.go
index b7e6220d8..0841c38aa 100644
--- a/api/user.go
+++ b/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))
+ }
+ }
+}
diff --git a/api/user_test.go b/api/user_test.go
index 1a1cf9634..27f00829f 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -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)
+ }
+ }
+}
diff --git a/i18n/en.json b/i18n/en.json
index bc33fc019..d16de7dbb 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1624,6 +1624,10 @@
"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"
},
@@ -3400,22 +3404,6 @@
"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"
},
@@ -3548,10 +3536,6 @@
"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."
}
]
diff --git a/i18n/es.json b/i18n/es.json
index 4c0c1fd03..93ffb2341 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -3400,22 +3400,6 @@
"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"
},
@@ -3548,10 +3532,6 @@
"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"
},
diff --git a/model/client.go b/model/client.go
index 560e47b76..3adcb980d 100644
--- a/model/client.go
+++ b/model/client.go
@@ -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
+ }
+}
diff --git a/model/session.go b/model/session.go
index 5d9424d64..bf0d9531e 100644
--- a/model/session.go
+++ b/model/session.go
@@ -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"
diff --git a/model/team.go b/model/team.go
index 9e9eaa25f..bed7bbd8d 100644
--- a/model/team.go
+++ b/model/team.go
@@ -232,3 +232,10 @@ func (o *Team) Sanitize() {
o.Email = ""
o.AllowedDomains = ""
}
+
+func (o *Team) SanitizeForNotLoggedIn() {
+ o.Email = ""
+ o.AllowedDomains = ""
+ o.CompanyName = ""
+ o.InviteId = ""
+}
diff --git a/web/templates/authorize.html b/templates/authorize.html
index 0fa36b0ab..0fa36b0ab 100644
--- a/web/templates/authorize.html
+++ b/templates/authorize.html
diff --git a/api/templates/email_change_body.html b/templates/email_change_body.html
index 41b1bcd7d..41b1bcd7d 100644
--- a/api/templates/email_change_body.html
+++ b/templates/email_change_body.html
diff --git a/templates/email_change_subject.html b/templates/email_change_subject.html
new file mode 100644
index 000000000..540bc6eab
--- /dev/null
+++ b/templates/email_change_subject.html
@@ -0,0 +1 @@
+{{define "email_change_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/templates/email_change_verify_body.html b/templates/email_change_verify_body.html
index 0d0c0aaba..0d0c0aaba 100644
--- a/api/templates/email_change_verify_body.html
+++ b/templates/email_change_verify_body.html
diff --git a/templates/email_change_verify_subject.html b/templates/email_change_verify_subject.html
new file mode 100644
index 000000000..04da7593c
--- /dev/null
+++ b/templates/email_change_verify_subject.html
@@ -0,0 +1 @@
+{{define "email_change_verify_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/templates/email_footer.html b/templates/email_footer.html
index 6dc7fa483..6dc7fa483 100644
--- a/api/templates/email_footer.html
+++ b/templates/email_footer.html
diff --git a/api/templates/email_info.html b/templates/email_info.html
index 0a34f18a0..0a34f18a0 100644
--- a/api/templates/email_info.html
+++ b/templates/email_info.html
diff --git a/templates/error.html b/templates/error.html
new file mode 100644
index 000000000..b86039ca3
--- /dev/null
+++ b/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}}
diff --git a/templates/head.html b/templates/head.html
new file mode 100644
index 000000000..a7eacc85f
--- /dev/null
+++ b/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}}
diff --git a/api/templates/invite_body.html b/templates/invite_body.html
index 2b6bde6d3..2b6bde6d3 100644
--- a/api/templates/invite_body.html
+++ b/templates/invite_body.html
diff --git a/api/templates/invite_subject.html b/templates/invite_subject.html
index 504915d50..504915d50 100644
--- a/api/templates/invite_subject.html
+++ b/templates/invite_subject.html
diff --git a/api/templates/password_change_body.html b/templates/password_change_body.html
index 2c4ba10ca..2c4ba10ca 100644
--- a/api/templates/password_change_body.html
+++ b/templates/password_change_body.html
diff --git a/api/templates/password_change_subject.html b/templates/password_change_subject.html
index 897f1210d..897f1210d 100644
--- a/api/templates/password_change_subject.html
+++ b/templates/password_change_subject.html
diff --git a/api/templates/post_body.html b/templates/post_body.html
index 54f34d1dd..54f34d1dd 100644
--- a/api/templates/post_body.html
+++ b/templates/post_body.html
diff --git a/templates/post_subject.html b/templates/post_subject.html
new file mode 100644
index 000000000..9789d4142
--- /dev/null
+++ b/templates/post_subject.html
@@ -0,0 +1 @@
+{{define "post_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/templates/reset_body.html b/templates/reset_body.html
index 69cd44957..69cd44957 100644
--- a/api/templates/reset_body.html
+++ b/templates/reset_body.html
diff --git a/api/templates/reset_subject.html b/templates/reset_subject.html
index a2852d332..a2852d332 100644
--- a/api/templates/reset_subject.html
+++ b/templates/reset_subject.html
diff --git a/templates/root.html b/templates/root.html
new file mode 100644
index 000000000..560c7a4b0
--- /dev/null
+++ b/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}}
diff --git a/api/templates/signin_change_body.html b/templates/signin_change_body.html
index af8577f0f..af8577f0f 100644
--- a/api/templates/signin_change_body.html
+++ b/templates/signin_change_body.html
diff --git a/api/templates/signin_change_subject.html b/templates/signin_change_subject.html
index 606dc4df3..606dc4df3 100644
--- a/api/templates/signin_change_subject.html
+++ b/templates/signin_change_subject.html
diff --git a/api/templates/signup_team_body.html b/templates/signup_team_body.html
index 683a9891e..683a9891e 100644
--- a/api/templates/signup_team_body.html
+++ b/templates/signup_team_body.html
diff --git a/api/templates/signup_team_subject.html b/templates/signup_team_subject.html
index 413a5c8c1..413a5c8c1 100644
--- a/api/templates/signup_team_subject.html
+++ b/templates/signup_team_subject.html
diff --git a/api/templates/verify_body.html b/templates/verify_body.html
index 2b0d25f94..2b0d25f94 100644
--- a/api/templates/verify_body.html
+++ b/templates/verify_body.html
diff --git a/api/templates/verify_subject.html b/templates/verify_subject.html
index ad7fc2aaa..ad7fc2aaa 100644
--- a/api/templates/verify_subject.html
+++ b/templates/verify_subject.html
diff --git a/api/templates/welcome_body.html b/templates/welcome_body.html
index b5ca9beb3..b5ca9beb3 100644
--- a/api/templates/welcome_body.html
+++ b/templates/welcome_body.html
diff --git a/api/templates/welcome_subject.html b/templates/welcome_subject.html
index 95189b900..95189b900 100644
--- a/api/templates/welcome_subject.html
+++ b/templates/welcome_subject.html
diff --git a/utils/html.go b/utils/html.go
new file mode 100644
index 000000000..4203160d5
--- /dev/null
+++ b/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
+}
diff --git a/utils/license.go b/utils/license.go
index 5c975aec2..b1f15ad92 100644
--- a/utils/license.go
+++ b/utils/license.go
@@ -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
diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/action_creators/global_actions.jsx
index 367347d4b..4375d6c87 100644
--- a/web/react/dispatcher/event_helpers.jsx
+++ b/web/react/action_creators/global_actions.jsx
@@ -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();
+}
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx
index 95b4caa12..db366f8ed 100644
--- a/web/react/components/activity_log_modal.jsx
+++ b/web/react/components/activity_log_modal.jsx
@@ -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>
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index 32ed70a99..4c4f21f08 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -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'
diff --git a/web/react/components/admin_console/admin_navbar_dropdown.jsx b/web/react/components/admin_console/admin_navbar_dropdown.jsx
index dc0b3c4cb..ae95f5a3a 100644
--- a/web/react/components/admin_console/admin_navbar_dropdown.jsx
+++ b/web/react/components/admin_console/admin_navbar_dropdown.jsx
@@ -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>
);
}
-} \ No newline at end of file
+}
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index 6621e5743..c2f31f569 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -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();
diff --git a/web/react/components/admin_console/admin_sidebar_header.jsx b/web/react/components/admin_console/admin_sidebar_header.jsx
index 8c9f74934..f1281c6ee 100644
--- a/web/react/components/admin_console/admin_sidebar_header.jsx
+++ b/web/react/components/admin_console/admin_sidebar_header.jsx
@@ -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>
);
}
-} \ No newline at end of file
+}
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
index 4af350bcd..7d6cfb5c3 100644
--- a/web/react/components/admin_console/user_item.jsx
+++ b/web/react/components/admin_console/user_item.jsx
@@ -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'
/>
diff --git a/web/react/components/audit_table.jsx b/web/react/components/audit_table.jsx
index 47eee6d3f..917093840 100644
--- a/web/react/components/audit_table.jsx
+++ b/web/react/components/audit_table.jsx
@@ -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;
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
index 2422588cf..2ea840c1e 100644
--- a/web/react/components/center_panel.jsx
+++ b/web/react/components/center_panel.jsx
@@ -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()))
};
}
- componentDidMount() {
- PreferenceStore.addChangeListener(this.onPreferenceChange);
- ChannelStore.addChangeListener(this.onChannelChange);
- UserStore.addChangeListener(this.onUserChange);
- }
- componentWillUnmount() {
- PreferenceStore.removeChangeListener(this.onPreferenceChange);
- ChannelStore.removeChangeListener(this.onChannelChange);
- UserStore.removeChangeListener(this.onUserChange);
+ validState() {
+ return this.state.user && this.state.channel && this.state.profiles;
}
- onPreferenceChange() {
- const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
- this.setState({showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS});
+ onStoresChange() {
+ this.setState(this.getStateFromStores());
}
- onChannelChange() {
- this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS});
+ componentDidMount() {
+ PreferenceStore.addChangeListener(this.onStoresChange);
+ ChannelStore.addChangeListener(this.onStoresChange);
+ UserStore.addChangeListener(this.onStoresChange);
}
- onUserChange() {
- this.setState({user: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))});
+ componentWillUnmount() {
+ 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;
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 51be13dcf..882c575f0 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -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
};
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 6c8d51abb..4157812a9 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -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}}
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
deleted file mode 100644
index e47f2aa50..000000000
--- a/web/react/components/channel_loader.jsx
+++ /dev/null
@@ -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);
diff --git a/web/react/components/channel_notifications_modal.jsx b/web/react/components/channel_notifications_modal.jsx
index 7048434f8..acefaf024 100644
--- a/web/react/components/channel_notifications_modal.jsx
+++ b/web/react/components/channel_notifications_modal.jsx
@@ -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: ''
};
}
+ updateSection(section) {
+ this.setState({activeSection: section});
+ }
componentWillReceiveProps(nextProps) {
if (!this.props.show && nextProps.show) {
- this.onListenerChange();
- ChannelStore.addChangeListener(this.onListenerChange);
- } else {
- ChannelStore.removeChangeListener(this.onListenerChange);
+ this.setState({
+ notifyLevel: nextProps.channelMember.notify_props.desktop,
+ unreadLevel: nextProps.channelMember.notify_props.mark_unread
+ });
}
}
- 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});
- }
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
};
diff --git a/web/react/components/channel_view.jsx b/web/react/components/channel_view.jsx
index 9c4131292..76744d6d7 100644
--- a/web/react/components/channel_view.jsx
+++ b/web/react/components/channel_view.jsx
@@ -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
};
diff --git a/web/react/components/claim/claim_account.jsx b/web/react/components/claim/claim_account.jsx
index 5b3b584ee..42fd8dafa 100644
--- a/web/react/components/claim/claim_account.jsx
+++ b/web/react/components/claim/claim_account.jsx
@@ -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
};
diff --git a/web/react/components/claim/sso_to_email.jsx b/web/react/components/claim/sso_to_email.jsx
index 74137082a..a16efb57b 100644
--- a/web/react/components/claim/sso_to_email.jsx
+++ b/web/react/components/claim/sso_to_email.jsx
@@ -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);
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 62319b1a7..69cc74842 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -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') {
diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx
index d9113bc9f..70e7a67a8 100644
--- a/web/react/components/delete_channel_modal.jsx
+++ b/web/react/components/delete_channel_modal.jsx
@@ -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');
diff --git a/web/react/components/do_verify_email.jsx b/web/react/components/do_verify_email.jsx
new file mode 100644
index 000000000..df98bf463
--- /dev/null
+++ b/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
+};
diff --git a/web/react/components/docs.jsx b/web/react/components/docs.jsx
deleted file mode 100644
index 6d3a109c2..000000000
--- a/web/react/components/docs.jsx
+++ /dev/null
@@ -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
-};
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index 380ca7bde..f02239fcf 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -3,7 +3,7 @@
import * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
-import * 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;
}
diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx
deleted file mode 100644
index 702a20eba..000000000
--- a/web/react/components/email_verify.jsx
+++ /dev/null
@@ -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
-};
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index 2f6067b86..8abcac8c3 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -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) {
diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx
deleted file mode 100644
index 3ff9787ad..000000000
--- a/web/react/components/find_team.jsx
+++ /dev/null
@@ -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); \ No newline at end of file
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 184ba1357..71cd5b8b6 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -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() {
diff --git a/web/react/components/logged_in.jsx b/web/react/components/logged_in.jsx
new file mode 100644
index 000000000..1ed3694e9
--- /dev/null
+++ b/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
+};
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index 581b8e0b5..30c8ffe4f 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -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'
- />
+ extraBox = (
+ <div className='alert alert-success'>
+ <i className='fa fa-check'/>
+ <FormattedMessage
+ id='login.changed'
+ defaultMessage=' Sign-in method changed successfully'
+ />
+ </div>
);
} 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.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
};
diff --git a/web/react/components/login_email.jsx b/web/react/components/login_email.jsx
index cf1e1bc40..3e0d8919d 100644
--- a/web/react/components/login_email.jsx
+++ b/web/react/components/login_email.jsx
@@ -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); \ No newline at end of file
+export default injectIntl(LoginEmail);
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index 93fe6c05a..974f026d0 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -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;
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 0ddd6ff4f..12227fd13 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -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
};
diff --git a/web/react/components/needs_team.jsx b/web/react/components/needs_team.jsx
new file mode 100644
index 000000000..33b9cd37e
--- /dev/null
+++ b/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
+};
diff --git a/web/react/components/not_logged_in.jsx b/web/react/components/not_logged_in.jsx
new file mode 100644
index 000000000..7af293e77
--- /dev/null
+++ b/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
+};
diff --git a/web/react/components/password_reset.jsx b/web/react/components/password_reset.jsx
deleted file mode 100644
index 4c9bb6310..000000000
--- a/web/react/components/password_reset.jsx
+++ /dev/null
@@ -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
-};
diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx
index 380dbe973..cfd39e440 100644
--- a/web/react/components/password_reset_form.jsx
+++ b/web/react/components/password_reset_form.jsx
@@ -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); \ No newline at end of file
+export default PasswordResetForm;
diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx
index 8cc8a050d..ce6253e16 100644
--- a/web/react/components/password_reset_send_link.jsx
+++ b/web/react/components/password_reset_send_link.jsx
@@ -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); \ No newline at end of file
+export default PasswordResetSendLink;
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index afff78bae..1943fb409 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -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
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 57e919e45..3a855edf2 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -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
};
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 854cb095a..2fa4cebfe 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -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
diff --git a/web/react/components/post_focus_view.jsx b/web/react/components/post_focus_view.jsx
index 44a0bae09..fd654f502 100644
--- a/web/react/components/post_focus_view.jsx
+++ b/web/react/components/post_focus_view.jsx
@@ -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>
);
diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx
index 2803fe387..966775dad 100644
--- a/web/react/components/post_header.jsx
+++ b/web/react/components/post_header.jsx
@@ -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
};
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index ffac6eaef..d0a4c828e 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -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
};
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index c2c739e9a..0a9232850 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -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}) {
diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx
index 976e03fab..b361779d2 100644
--- a/web/react/components/posts_view_container.jsx
+++ b/web/react/components/posts_view_container.jsx
@@ -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) {
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index 9588809eb..9183b761f 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -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'
/>
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index 023f3dd2d..fc1cd0b41 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -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;
diff --git a/web/react/components/root.jsx b/web/react/components/root.jsx
new file mode 100644
index 000000000..70038203b
--- /dev/null
+++ b/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
+};
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index 5ab864b7c..3a091bdd1 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -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'
/>
diff --git a/web/react/components/should_verify_email.jsx b/web/react/components/should_verify_email.jsx
new file mode 100644
index 000000000..c473fe366
--- /dev/null
+++ b/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
+};
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index c7dba306b..5c682d64b 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -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
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index 45b0a5fc4..00d30948a 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -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
};
diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx
index b81c0d099..14853d3a3 100644
--- a/web/react/components/sidebar_right.jsx
+++ b/web/react/components/sidebar_right.jsx
@@ -127,8 +127,13 @@ export default class SidebarRight extends React.Component {
}
return (
- <div className='sidebar-right-container'>
- {content}
+ <div
+ className='sidebar--right'
+ id='sidebar-right'
+ >
+ <div className='sidebar-right-container'>
+ {content}
+ </div>
</div>
);
}
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index 4d714e9f1..c7c5bcfd6 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -5,11 +5,11 @@ import TeamMembersModal from './team_members_modal.jsx';
import ToggleModalButton from './toggle_modal_button.jsx';
import UserSettingsModal from './user_settings/user_settings_modal.jsx';
import UserStore from '../stores/user_store.jsx';
-import * as client from '../utils/client.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 Utils from '../utils/utils.jsx';
import {FormattedMessage} from 'mm-intl';
+import {Link} from 'react-router';
export default class SidebarRightMenu extends React.Component {
componentDidMount() {
@@ -19,18 +19,11 @@ export default class SidebarRightMenu extends React.Component {
constructor(props) {
super(props);
- this.handleLogoutClick = this.handleLogoutClick.bind(this);
-
this.state = {
showUserSettingsModal: false
};
}
- handleLogoutClick(e) {
- e.preventDefault();
- client.logout();
- }
-
render() {
var teamLink = '';
var inviteLink = '';
@@ -42,14 +35,14 @@ export default class SidebarRightMenu extends React.Component {
var isSystemAdmin = false;
if (currentUser != null) {
- isAdmin = utils.isAdmin(currentUser.roles);
- isSystemAdmin = utils.isSystemAdmin(currentUser.roles);
+ isAdmin = Utils.isAdmin(currentUser.roles);
+ isSystemAdmin = Utils.isSystemAdmin(currentUser.roles);
inviteLink = (
<li>
<a
href='#'
- onClick={EventHelpers.showInviteMemberModal}
+ onClick={GlobalActions.showInviteMemberModal}
>
<i className='fa fa-user'></i>
<FormattedMessage
@@ -65,7 +58,7 @@ export default class SidebarRightMenu extends React.Component {
<li>
<a
href='#'
- onClick={EventHelpers.showGetTeamInviteLinkModal}
+ onClick={GlobalActions.showGetTeamInviteLinkModal}
>
<i className='glyphicon glyphicon-link'></i>
<FormattedMessage
@@ -107,13 +100,13 @@ export default class SidebarRightMenu extends React.Component {
);
}
- if (isSystemAdmin && !utils.isMobile()) {
+ if (isSystemAdmin && !Utils.isMobile()) {
consoleLink = (
<li>
<a
- href={'/admin_console?' + utils.getSessionIndex()}
+ href={'/admin_console'}
>
- <i className='fa fa-wrench'></i>
+ <i className='fa fa-wrench'></i>
<FormattedMessage
id='sidebar_right_menu.console'
defaultMessage='System Console'
@@ -168,7 +161,10 @@ export default class SidebarRightMenu extends React.Component {
);
}
return (
- <div>
+ <div
+ className='sidebar--menu'
+ id='sidebar-menu'
+ >
<div className='team__header theme'>
<a
className='team__name'
@@ -196,16 +192,13 @@ export default class SidebarRightMenu extends React.Component {
{manageLink}
{consoleLink}
<li>
- <a
- href='#'
- onClick={this.handleLogoutClick}
- >
+ <Link to={Utils.getTeamURLFromAddressBar() + '/logout'}>
<i className='fa fa-sign-out'></i>
<FormattedMessage
id='sidebar_right_menu.logout'
defaultMessage='Logout'
/>
- </a>
+ </Link>
</li>
<li className='divider'></li>
{helpLink}
diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx
index 26c46dad0..2adf8d111 100644
--- a/web/react/components/signup_team.jsx
+++ b/web/react/components/signup_team.jsx
@@ -6,6 +6,8 @@ import EmailSignUpPage from './team_signup_with_email.jsx';
import SSOSignupPage from './team_signup_with_sso.jsx';
import LdapSignUpPage from './team_signup_with_ldap.jsx';
import Constants from '../utils/constants.jsx';
+import TeamStore from '../stores/team_store.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -14,6 +16,7 @@ export default class TeamSignUp extends React.Component {
super(props);
this.updatePage = this.updatePage.bind(this);
+ this.onTeamUpdate = this.onTeamUpdate.bind(this);
var count = 0;
@@ -46,11 +49,34 @@ export default class TeamSignUp extends React.Component {
this.setState({page});
}
+ componentWillMount() {
+ if (global.window.mm_config.EnableTeamListing === 'true') {
+ AsyncClient.getAllTeams();
+ this.onTeamUpdate();
+ }
+ }
+
+ componentDidMount() {
+ TeamStore.addChangeListener(this.onTeamUpdate);
+ }
+
+ componentWillUnmount() {
+ TeamStore.removeChangeListener(this.onTeamUpdate);
+ }
+
+ onTeamUpdate() {
+ this.setState({
+ teams: TeamStore.getAll()
+ });
+ }
+
render() {
- var teamListing = null;
+ let teamListing = null;
if (global.window.mm_config.EnableTeamListing === 'true') {
- if (this.props.teams.length === 0) {
+ if (this.state.teams == null) {
+ teamListing = (<div/>);
+ } else if (this.state.teams.length === 0) {
if (global.window.mm_config.EnableTeamCreation !== 'true') {
teamListing = (
<div>
@@ -72,23 +98,26 @@ export default class TeamSignUp extends React.Component {
</h4>
<div className='signup-team-all'>
{
- this.props.teams.map((team) => {
- return (
- <div
- key={'team_' + team.name}
- className='signup-team-dir'
- >
- <a
- href={'/' + team.name}
+ Object.values(this.state.teams).map((team) => {
+ if (team.allow_team_listing) {
+ return (
+ <div
+ key={'team_' + team.name}
+ className='signup-team-dir'
>
- <span className='signup-team-dir__name'>{team.display_name}</span>
- <span
- className='glyphicon glyphicon-menu-right right signup-team-dir__arrow'
- aria-hidden='true'
- />
- </a>
- </div>
- );
+ <a
+ href={'/' + team.name}
+ >
+ <span className='signup-team-dir__name'>{team.display_name}</span>
+ <span
+ className='glyphicon glyphicon-menu-right right signup-team-dir__arrow'
+ aria-hidden='true'
+ />
+ </a>
+ </div>
+ );
+ }
+ return null;
})
}
</div>
@@ -103,42 +132,26 @@ export default class TeamSignUp extends React.Component {
}
}
+ let signupMethod = null;
+
if (global.window.mm_config.EnableTeamCreation !== 'true') {
if (teamListing == null) {
- return (
- <div>
- <FormattedMessage
- id='signup_team.disabled'
- defaultMessage='Team creation has been disabled. Please contact an administrator for access.'
- />
- </div>
+ signupMethod = (
+ <FormattedMessage
+ id='signup_team.disabled'
+ defaultMessage='Team creation has been disabled. Please contact an administrator for access.'
+ />
);
}
-
- return (
- <div>
- {teamListing}
- </div>
- );
- }
-
- if (this.state.page === 'choose') {
- return (
- <div>
- {teamListing}
- <ChoosePage
- updatePage={this.updatePage}
- />
- </div>
+ } else if (this.state.page === 'choose') {
+ signupMethod = (
+ <ChoosePage
+ updatePage={this.updatePage}
+ />
);
- }
-
- if (this.state.page === 'email') {
- return (
- <div>
- {teamListing}
- <EmailSignUpPage/>
- </div>
+ } else if (this.state.page === 'email') {
+ signupMethod = (
+ <EmailSignUpPage/>
);
} else if (this.state.page === 'ldap') {
return (
@@ -148,35 +161,45 @@ export default class TeamSignUp extends React.Component {
</div>
);
} else if (this.state.page === 'gitlab') {
- return (
- <div>
- {teamListing}
- <SSOSignupPage service={Constants.GITLAB_SERVICE}/>
- </div>
+ signupMethod = (
+ <SSOSignupPage service={Constants.GITLAB_SERVICE}/>
);
} else if (this.state.page === 'google') {
- return (
- <div>
- {teamListing}
- <SSOSignupPage service={Constants.GOOGLE_SERVICE}/>
- </div>
+ signupMethod = (
+ <SSOSignupPage service={Constants.GOOGLE_SERVICE}/>
);
} else if (this.state.page === 'none') {
- return (
- <div>
- <FormattedMessage
- id='signup_team.none'
- defaultMessage='No team creation method has been enabled. Please contact an administrator for access.'
- />
- </div>
+ signupMethod = (
+ <FormattedMessage
+ id='signup_team.none'
+ defaultMessage='No team creation method has been enabled. Please contact an administrator for access.'
+ />
);
}
- return null;
+ return (
+ <div className='col-sm-12'>
+ <div className='signup-team__container'>
+ <img
+ className='signup-team-logo'
+ src='/static/images/logo.png'
+ />
+ <h1>{global.window.mm_config.SiteName}</h1>
+ <h4 className='color--light'>
+ <FormattedMessage
+ id='web.root.singup_info'
+ />
+ </h4>
+ <div id='signup-team'>
+ {teamListing}
+ {signupMethod}
+ </div>
+ </div>
+ </div>
+ );
}
}
TeamSignUp.propTypes = {
- teams: React.PropTypes.array
};
diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx
deleted file mode 100644
index 16553daeb..000000000
--- a/web/react/components/signup_team_complete.jsx
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import WelcomePage from './team_signup_welcome_page.jsx';
-import TeamDisplayNamePage from './team_signup_display_name_page.jsx';
-import TeamURLPage from './team_signup_url_page.jsx';
-import SendInivtesPage from './team_signup_send_invites_page.jsx';
-import UsernamePage from './team_signup_username_page.jsx';
-import PasswordPage from './team_signup_password_page.jsx';
-import BrowserStore from '../stores/browser_store.jsx';
-
-import {FormattedMessage} from 'mm-intl';
-
-export default class SignupTeamComplete extends React.Component {
- constructor(props) {
- super(props);
-
- this.updateParent = this.updateParent.bind(this);
-
- var initialState = BrowserStore.getGlobalItem(props.hash);
-
- if (!initialState) {
- initialState = {};
- initialState.wizard = 'welcome';
- initialState.team = {};
- initialState.team.email = this.props.email;
- initialState.team.allowed_domains = '';
- initialState.invites = [];
- initialState.invites.push('');
- initialState.invites.push('');
- initialState.invites.push('');
- initialState.user = {};
- initialState.hash = this.props.hash;
- initialState.data = this.props.data;
- }
-
- this.state = initialState;
- }
- updateParent(state, skipSet) {
- BrowserStore.setGlobalItem(this.props.hash, state);
-
- if (!skipSet) {
- this.setState(state);
- }
- }
- render() {
- if (this.state.wizard === 'welcome') {
- return (
- <WelcomePage
- state={this.state}
- updateParent={this.updateParent}
- />
- );
- }
-
- if (this.state.wizard === 'team_display_name') {
- return (
- <TeamDisplayNamePage
- state={this.state}
- updateParent={this.updateParent}
- />
- );
- }
-
- if (this.state.wizard === 'team_url') {
- return (
- <TeamURLPage
- state={this.state}
- updateParent={this.updateParent}
- />
- );
- }
-
- if (this.state.wizard === 'send_invites') {
- return (
- <SendInivtesPage
- state={this.state}
- updateParent={this.updateParent}
- />
- );
- }
-
- if (this.state.wizard === 'username') {
- return (
- <UsernamePage
- state={this.state}
- updateParent={this.updateParent}
- />
- );
- }
-
- if (this.state.wizard === 'password') {
- return (
- <PasswordPage
- state={this.state}
- updateParent={this.updateParent}
- />
- );
- }
-
- return (
- <div>
- <FormattedMessage
- id='signup_team_complete.completed'
- defaultMessage="You've already completed the signup process for this invitation or this invitation has expired."
- />
- </div>
- );
- }
-}
-
-SignupTeamComplete.defaultProps = {
- hash: '',
- email: '',
- data: ''
-};
-SignupTeamComplete.propTypes = {
- hash: React.PropTypes.string,
- email: React.PropTypes.string,
- data: React.PropTypes.string
-};
diff --git a/web/react/components/signup_team_complete/components/signup_team_complete.jsx b/web/react/components/signup_team_complete/components/signup_team_complete.jsx
new file mode 100644
index 000000000..5ad21e941
--- /dev/null
+++ b/web/react/components/signup_team_complete/components/signup_team_complete.jsx
@@ -0,0 +1,79 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import BrowserStore from '../../../stores/browser_store.jsx';
+
+import {FormattedMessage} from 'mm-intl';
+
+import {browserHistory} from 'react-router';
+
+export default class SignupTeamComplete extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateParent = this.updateParent.bind(this);
+ }
+ componentWillMount() {
+ const data = JSON.parse(this.props.location.query.d);
+ this.hash = this.props.location.query.h;
+
+ var initialState = BrowserStore.getGlobalItem(this.hash);
+
+ if (!initialState) {
+ initialState = {};
+ initialState.wizard = 'welcome';
+ initialState.team = {};
+ initialState.team.email = data.email;
+ initialState.team.allowed_domains = '';
+ initialState.invites = [];
+ initialState.invites.push('');
+ initialState.invites.push('');
+ initialState.invites.push('');
+ initialState.user = {};
+ initialState.hash = this.hash;
+ initialState.data = this.props.location.query.d;
+ }
+
+ this.setState(initialState);
+ }
+ componentDidMount() {
+ browserHistory.push('/signup_team_complete/welcome');
+ }
+ updateParent(state, skipSet) {
+ BrowserStore.setGlobalItem(this.hash, state);
+
+ if (!skipSet) {
+ this.setState(state);
+ browserHistory.push('/signup_team_complete/' + state.wizard);
+ }
+ }
+ render() {
+ return (
+ <div>
+ <div className='signup-header'>
+ <a href='/'>
+ <span classNameName='fa fa-chevron-left'/>
+ <FormattedMessage id='web.header.back'/>
+ </a>
+ </div>
+ <div className='col-sm-12'>
+ <div className='signup-team__container'>
+ <div id='signup-team-complete'>
+ {React.cloneElement(this.props.children, {
+ state: this.state,
+ updateParent: this.updateParent
+ })}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+SignupTeamComplete.defaultProps = {
+};
+SignupTeamComplete.propTypes = {
+ location: React.PropTypes.object,
+ children: React.PropTypes.node
+};
diff --git a/web/react/components/team_signup_display_name_page.jsx b/web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx
index f07b50756..280e53ce4 100644
--- a/web/react/components/team_signup_display_name_page.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx
@@ -1,8 +1,8 @@
// 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 * as utils from '../../../utils/utils.jsx';
+import * as client from '../../../utils/client.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
@@ -133,4 +133,4 @@ TeamSignupDisplayNamePage.propTypes = {
updateParent: React.PropTypes.func
};
-export default injectIntl(TeamSignupDisplayNamePage); \ No newline at end of file
+export default injectIntl(TeamSignupDisplayNamePage);
diff --git a/web/react/components/team_signup_email_item.jsx b/web/react/components/signup_team_complete/components/team_signup_email_item.jsx
index 790ec2e5d..c87d6ec07 100644
--- a/web/react/components/team_signup_email_item.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_email_item.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as Utils from '../utils/utils.jsx';
+import * as Utils from '../../../utils/utils.jsx';
import {intlShape, injectIntl, defineMessages} from 'mm-intl';
diff --git a/web/react/components/signup_team_complete/components/team_signup_finished.jsx b/web/react/components/signup_team_complete/components/team_signup_finished.jsx
new file mode 100644
index 000000000..fc5f756e7
--- /dev/null
+++ b/web/react/components/signup_team_complete/components/team_signup_finished.jsx
@@ -0,0 +1,15 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {FormattedMessage} from 'mm-intl';
+
+export default class FinishedPage extends React.Component {
+ render() {
+ return (
+ <FormattedMessage
+ id='signup_team_complete.completed'
+ defaultMessage="You've already completed the signup process for this invitation or this invitation has expired."
+ />
+ );
+ }
+}
diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/signup_team_complete/components/team_signup_password_page.jsx
index 06c04854f..490a11040 100644
--- a/web/react/components/team_signup_password_page.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_password_page.jsx
@@ -1,12 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as Client from '../utils/client.jsx';
-import BrowserStore from '../stores/browser_store.jsx';
-import UserStore from '../stores/user_store.jsx';
-import Constants from '../utils/constants.jsx';
+import * as Client from '../../../utils/client.jsx';
+import BrowserStore from '../../../stores/browser_store.jsx';
+import UserStore from '../../../stores/user_store.jsx';
+import Constants from '../../../utils/constants.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
+import {browserHistory} from 'react-router';
const holders = defineMessages({
passwordError: {
@@ -66,11 +67,11 @@ class TeamSignupPasswordPage extends React.Component {
props.state.wizard = 'finished';
props.updateParent(props.state, true);
- window.location.href = '/' + teamSignup.team.name + '/channels/town-square';
+ browserHistory.push('/' + teamSignup.team.name + '/channels/town-square');
},
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
- window.location.href = '/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name);
+ browserHistory.push('/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name));
} else {
this.setState({serverError: err.message});
$('#finish-button').button('reset');
@@ -211,4 +212,4 @@ TeamSignupPasswordPage.propTypes = {
updateParent: React.PropTypes.func
};
-export default injectIntl(TeamSignupPasswordPage); \ No newline at end of file
+export default injectIntl(TeamSignupPasswordPage);
diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx
index 55cfe5114..5e987ef2c 100644
--- a/web/react/components/team_signup_send_invites_page.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx
@@ -2,7 +2,7 @@
// See License.txt for license information.
import EmailItem from './team_signup_email_item.jsx';
-import * as Client from '../utils/client.jsx';
+import * as Client from '../../../utils/client.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/signup_team_complete/components/team_signup_url_page.jsx
index 2f6c3df49..ec50e2d25 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_url_page.jsx
@@ -1,9 +1,9 @@
// 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 Constants from '../utils/constants.jsx';
+import * as Utils from '../../../utils/utils.jsx';
+import * as Client from '../../../utils/client.jsx';
+import Constants from '../../../utils/constants.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
@@ -202,4 +202,4 @@ TeamSignupUrlPage.propTypes = {
updateParent: React.PropTypes.func
};
-export default injectIntl(TeamSignupUrlPage); \ No newline at end of file
+export default injectIntl(TeamSignupUrlPage);
diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/signup_team_complete/components/team_signup_username_page.jsx
index 0fa9cb103..e56aa4cd7 100644
--- a/web/react/components/team_signup_username_page.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_username_page.jsx
@@ -1,9 +1,9 @@
// 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 Constants from '../utils/constants.jsx';
+import * as Utils from '../../../utils/utils.jsx';
+import * as Client from '../../../utils/client.jsx';
+import Constants from '../../../utils/constants.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
@@ -161,4 +161,4 @@ TeamSignupUsernamePage.propTypes = {
updateParent: React.PropTypes.func
};
-export default injectIntl(TeamSignupUsernamePage); \ No newline at end of file
+export default injectIntl(TeamSignupUsernamePage);
diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx
index 9939c3ffd..97782e54a 100644
--- a/web/react/components/team_signup_welcome_page.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx
@@ -1,12 +1,14 @@
// 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 BrowserStore from '../stores/browser_store.jsx';
+import * as Utils from '../../../utils/utils.jsx';
+import * as Client from '../../../utils/client.jsx';
+import BrowserStore from '../../../stores/browser_store.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
+import {browserHistory} from 'react-router';
+
const holders = defineMessages({
storageError: {
id: 'team_signup_welcome.storageError',
@@ -73,7 +75,7 @@ class TeamSignupWelcomePage extends React.Component {
} else {
this.props.state.wizard = 'finished';
this.props.updateParent(this.props.state);
- window.location.href = '/signup_team_confirm/?email=' + encodeURIComponent(email);
+ browserHistory.push('/signup_team_confirm/?email=' + encodeURIComponent(email));
}
}.bind(this),
function error(err) {
@@ -229,4 +231,4 @@ TeamSignupWelcomePage.propTypes = {
state: React.PropTypes.object
};
-export default injectIntl(TeamSignupWelcomePage); \ No newline at end of file
+export default injectIntl(TeamSignupWelcomePage);
diff --git a/web/react/components/signup_team_confirm.jsx b/web/react/components/signup_team_confirm.jsx
index 290d8e503..1afbb3d30 100644
--- a/web/react/components/signup_team_confirm.jsx
+++ b/web/react/components/signup_team_confirm.jsx
@@ -6,30 +6,41 @@ import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
export default class SignupTeamConfirm extends React.Component {
render() {
return (
- <div className='signup-team__container'>
- <h3>
- <FormattedMessage
- id='signup_team_confirm.title'
- defaultMessage='Sign up Complete'
- />
- </h3>
- <p>
- <FormattedHTMLMessage
- id='signup_team_confirm.checkEmail'
- defaultMessage='Please check your email: <strong>{email}</strong><br />Your email contains a link to set up your team'
- values={{
- email: this.props.email
- }}
- />
- </p>
+ <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 classNameName='signup-team__container'>
+ <h3>
+ <FormattedMessage
+ id='signup_team_confirm.title'
+ defaultMessage='Sign up Complete'
+ />
+ </h3>
+ <p>
+ <FormattedHTMLMessage
+ id='signup_team_confirm.checkEmail'
+ defaultMessage='Please check your email: <strong>{email}</strong><br />Your email contains a link to set up your team'
+ values={{
+ email: this.props.location.query.email
+ }}
+ />
+ </p>
+ </div>
+ </div>
</div>
);
}
}
SignupTeamConfirm.defaultProps = {
- email: ''
};
SignupTeamConfirm.propTypes = {
- email: React.PropTypes.string
+ location: React.PropTypes.object
};
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index dbec3d02d..d2128a50f 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -2,83 +2,130 @@
// See License.txt for license information.
import * as Utils from '../utils/utils.jsx';
-import * as client from '../utils/client.jsx';
+import * as Client from '../utils/client.jsx';
import UserStore from '../stores/user_store.jsx';
import BrowserStore from '../stores/browser_store.jsx';
import Constants from '../utils/constants.jsx';
+import LoadingScreen from '../components/loading_screen.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
-
-const holders = defineMessages({
- required: {
- id: 'signup_user_completed.required',
- defaultMessage: 'This field is required'
- },
- validEmail: {
- id: 'signup_user_completed.validEmail',
- defaultMessage: 'Please enter a valid email address'
- },
- reserved: {
- id: 'signup_user_completed.reserved',
- defaultMessage: 'This username is reserved, please choose a new one.'
- },
- usernameLength: {
- id: 'signup_user_completed.usernameLength',
- defaultMessage: 'Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols \'.\', \'-\' and \'_\'.'
- },
- passwordLength: {
- id: 'signup_user_completed.passwordLength',
- defaultMessage: 'Please enter at least {min} characters'
- }
-});
+import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
+import {browserHistory} from 'react-router';
class SignupUserComplete extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
+ this.inviteInfoRecieved = this.inviteInfoRecieved.bind(this);
+
+ this.state = {
+ data: '',
+ hash: '',
+ usedBefore: false,
+ email: '',
+ teamDisplayName: '',
+ teamName: '',
+ teamId: ''
+ };
+ }
+ componentWillMount() {
+ let data = this.props.location.query.d;
+ let hash = this.props.location.query.h;
+ const inviteId = this.props.location.query.id;
+ let usedBefore = false;
+ let email = '';
+ let teamDisplayName = '';
+ let teamName = '';
+ let teamId = '';
+
+ // If we have a hash in the url then we are attempting to access a private team
+ if (hash) {
+ const parsedData = JSON.parse(data);
+ usedBefore = BrowserStore.getGlobalItem(hash);
+ email = parsedData.email;
+ teamDisplayName = parsedData.display_name;
+ teamName = parsedData.name;
+ teamId = parsedData.id;
+ } else {
+ Client.getInviteInfo(this.inviteInfoRecieved, null, inviteId);
+ data = '';
+ hash = '';
+ }
- var initialState = BrowserStore.getGlobalItem(this.props.hash);
-
- if (!initialState) {
- initialState = {};
- initialState.wizard = 'welcome';
- initialState.user = {};
- initialState.user.team_id = this.props.teamId;
- initialState.user.email = this.props.email;
- initialState.original_email = this.props.email;
+ this.setState({
+ data,
+ hash,
+ usedBefore,
+ email,
+ teamDisplayName,
+ teamName,
+ teamId
+ });
+ }
+ inviteInfoRecieved(data) {
+ if (!data) {
+ return;
}
- this.state = initialState;
+ this.setState({
+ teamDisplayName: data.display_name,
+ teamName: data.name,
+ teamId: data.id
+ });
}
handleSubmit(e) {
e.preventDefault();
- const {formatMessage} = this.props.intl;
const providedEmail = ReactDOM.findDOMNode(this.refs.email).value.trim();
if (!providedEmail) {
- this.setState({nameError: '', emailError: formatMessage(holders.required), passwordError: ''});
+ this.setState({
+ nameError: '',
+ emailError: (<FormattedMessage id='signup_user_completed.required'/>),
+ passwordError: '',
+ serverError: ''
+ });
return;
}
if (!Utils.isEmail(providedEmail)) {
- this.setState({nameError: '', emailError: formatMessage(holders.validEmail), passwordError: ''});
+ this.setState({
+ nameError: '',
+ emailError: (<FormattedMessage id='signup_user_completed.validEmail'/>),
+ passwordError: '',
+ serverError: ''
+ });
return;
}
const providedUsername = ReactDOM.findDOMNode(this.refs.name).value.trim().toLowerCase();
if (!providedUsername) {
- this.setState({nameError: formatMessage(holders.required), emailError: '', passwordError: '', serverError: ''});
+ this.setState({
+ nameError: (<FormattedMessage id='signup_user_completed.required'/>),
+ emailError: '',
+ passwordError: '',
+ serverError: ''
+ });
return;
}
const usernameError = Utils.isValidUsername(providedUsername);
if (usernameError === 'Cannot use a reserved word as a username.') {
- this.setState({nameError: formatMessage(holders.reserved), emailError: '', passwordError: '', serverError: ''});
+ this.setState({
+ nameError: (<FormattedMessage id='signup_user_completed.reserved'/>),
+ emailError: '',
+ passwordError: '',
+ serverError: ''
+ });
return;
} else if (usernameError) {
this.setState({
- nameError: formatMessage(holders.usernameLength, {min: Constants.MIN_USERNAME_LENGTH, max: Constants.MAX_USERNAME_LENGTH}),
+ nameError: (
+ <FormattedMessage
+ id='signup_user_completed.usernameLength'
+ min={Constants.MIN_USERNAME_LENGTH}
+ max={Constants.MAX_USERNAME_LENGTH}
+ />
+ ),
emailError: '',
passwordError: '',
serverError: ''
@@ -88,41 +135,50 @@ class SignupUserComplete extends React.Component {
const providedPassword = ReactDOM.findDOMNode(this.refs.password).value.trim();
if (!providedPassword || providedPassword.length < Constants.MIN_PASSWORD_LENGTH) {
- this.setState({nameError: '', emailError: '', passwordError: formatMessage(holders.passwordLength, {min: Constants.MIN_PASSWORD_LENGTH}), serverError: ''});
+ this.setState({
+ nameError: '',
+ emailError: '',
+ passwordError: (
+ <FormattedMessage
+ id='signup_user_completed.passwordLength'
+ min={Constants.MIN_PASSWORD_LENGTH}
+ />
+ ),
+ serverError: ''
+ });
return;
}
- const user = {
- team_id: this.props.teamId,
- email: providedEmail,
- username: providedUsername,
- password: providedPassword,
- allow_marketing: true
- };
-
this.setState({
- user,
nameError: '',
emailError: '',
passwordError: '',
serverError: ''
});
- client.createUser(user, this.props.data, this.props.hash,
+ const user = {
+ team_id: this.state.teamId,
+ email: providedEmail,
+ username: providedUsername,
+ password: providedPassword,
+ allow_marketing: true
+ };
+
+ Client.createUser(user, this.state.data, this.state.hash,
() => {
- client.track('signup', 'signup_user_02_complete');
+ Client.track('signup', 'signup_user_02_complete');
- client.loginByEmail(this.props.teamName, user.email, user.password,
+ Client.loginByEmail(this.state.teamName, user.email, user.password,
() => {
UserStore.setLastEmail(user.email);
- if (this.props.hash > 0) {
- BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'}));
+ if (this.state.hash > 0) {
+ BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true}));
}
- window.location.href = '/' + this.props.teamName + '/channels/town-square';
+ browserHistory.push('/' + this.state.teamName + '/channels/town-square');
},
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
- window.location.href = '/verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.props.teamName);
+ browserHistory.push('/should_verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.state.teamName));
} else {
this.setState({serverError: err.message});
}
@@ -135,9 +191,10 @@ class SignupUserComplete extends React.Component {
);
}
render() {
- client.track('signup', 'signup_user_01_welcome');
+ Client.track('signup', 'signup_user_01_welcome');
- if (this.state.wizard === 'finished') {
+ // If we have been used then just display a message
+ if (this.state.usedBefore) {
return (
<div>
<FormattedMessage
@@ -148,6 +205,12 @@ class SignupUserComplete extends React.Component {
);
}
+ // If we haven't got a team id yet we are waiting for
+ // the client so just show the standard loading screen
+ if (this.state.teamId === '') {
+ return (<LoadingScreen/>);
+ }
+
// set up error labels
var emailError = null;
var emailHelpText = (
@@ -160,7 +223,7 @@ class SignupUserComplete extends React.Component {
);
var emailDivStyle = 'form-group';
if (this.state.emailError) {
- emailError = <label className='control-label'>{this.state.emailError}</label>;
+ emailError = (<label className='control-label'>{this.state.emailError}</label>);
emailHelpText = '';
emailDivStyle += ' has-error';
}
@@ -203,13 +266,13 @@ class SignupUserComplete extends React.Component {
// set up the email entry and hide it if an email was provided
var yourEmailIs = '';
- if (this.state.user.email) {
+ if (this.state.email) {
yourEmailIs = (
<FormattedHTMLMessage
id='signup_user_completed.emailIs'
defaultMessage="Your email address is <strong>{email}</strong>. You'll use this address to sign in to {siteName}."
values={{
- email: this.state.user.email,
+ email: this.state.email,
siteName: global.window.mm_config.SiteName
}}
/>
@@ -217,7 +280,7 @@ class SignupUserComplete extends React.Component {
}
var emailContainerStyle = 'margin--extra';
- if (this.state.original_email) {
+ if (this.state.email) {
emailContainerStyle = 'hidden';
}
@@ -234,7 +297,7 @@ class SignupUserComplete extends React.Component {
type='email'
ref='email'
className='form-control'
- defaultValue={this.state.user.email}
+ defaultValue={this.state.email}
placeholder=''
maxLength='128'
autoFocus={true}
@@ -249,20 +312,20 @@ class SignupUserComplete extends React.Component {
var signupMessage = [];
if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
signupMessage.push(
- <a
- className='btn btn-custom-login gitlab'
- key='gitlab'
- href={'/' + this.props.teamName + '/signup/gitlab' + window.location.search}
- >
- <span className='icon'/>
- <span>
- <FormattedMessage
- id='signup_user_completed.gitlab'
- defaultMessage='with GitLab'
- />
- </span>
- </a>
- );
+ <a
+ className='btn btn-custom-login gitlab'
+ key='gitlab'
+ href={'/api/v1/oauth/gitlab/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)}
+ >
+ <span className='icon'/>
+ <span>
+ <FormattedMessage
+ id='signup_user_completed.gitlab'
+ defaultMessage='with GitLab'
+ />
+ </span>
+ </a>
+ );
}
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
@@ -270,7 +333,7 @@ class SignupUserComplete extends React.Component {
<a
className='btn btn-custom-login google'
key='google'
- href={'/' + this.props.teamName + '/signup/google' + window.location.search}
+ href={'/api/v1/oauth/google/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)}
>
<span className='icon'/>
<span>
@@ -318,16 +381,16 @@ class SignupUserComplete extends React.Component {
/>
</strong></h5>
<div className={passwordDivStyle}>
- <input
- type='password'
- ref='password'
- className='form-control'
- placeholder=''
- maxLength='128'
- spellCheck='false'
- />
- {passwordError}
- </div>
+ <input
+ type='password'
+ ref='password'
+ className='form-control'
+ placeholder=''
+ maxLength='128'
+ spellCheck='false'
+ />
+ {passwordError}
+ </div>
</div>
</div>
<p className='margin--extra'>
@@ -373,58 +436,56 @@ class SignupUserComplete extends React.Component {
return (
<div>
- <form>
- <img
- className='signup-team-logo'
- src='/static/images/logo.png'
- />
- <h5 className='margin--less'>
- <FormattedMessage
- id='signup_user_completed.welcome'
- defaultMessage='Welcome to:'
- />
- </h5>
- <h2 className='signup-team__name'>{this.props.teamDisplayName}</h2>
- <h2 className='signup-team__subdomain'>
- <FormattedMessage
- id='signup_user_completed.onSite'
- defaultMessage='on {siteName}'
- values={{
- siteName: global.window.mm_config.SiteName
- }}
- />
- </h2>
- <h4 className='color--light'>
- <FormattedMessage
- id='signup_user_completed.lets'
- defaultMessage="Let's create your account"
- />
- </h4>
- {signupMessage}
- {emailSignup}
- {serverError}
- </form>
+ <div className='signup-header'>
+ <a href='/'>
+ <span classNameNameName='fa fa-chevron-left'/>
+ <FormattedMessage id='web.header.back'/>
+ </a>
+ </div>
+ <div className='col-sm-12'>
+ <div className='signup-team__container padding--less'>
+ <form>
+ <img
+ className='signup-team-logo'
+ src='/static/images/logo.png'
+ />
+ <h5 className='margin--less'>
+ <FormattedMessage
+ id='signup_user_completed.welcome'
+ defaultMessage='Welcome to:'
+ />
+ </h5>
+ <h2 className='signup-team__name'>{this.state.teamName}</h2>
+ <h2 className='signup-team__subdomain'>
+ <FormattedMessage
+ id='signup_user_completed.onSite'
+ defaultMessage='on {siteName}'
+ values={{
+ siteName: global.window.mm_config.SiteName
+ }}
+ />
+ </h2>
+ <h4 className='color--light'>
+ <FormattedMessage
+ id='signup_user_completed.lets'
+ defaultMessage="Let's create your account"
+ />
+ </h4>
+ {signupMessage}
+ {emailSignup}
+ {serverError}
+ </form>
+ </div>
+ </div>
</div>
);
}
}
SignupUserComplete.defaultProps = {
- teamName: '',
- hash: '',
- teamId: '',
- email: '',
- data: null,
- teamDisplayName: ''
};
SignupUserComplete.propTypes = {
- intl: intlShape.isRequired,
- teamName: React.PropTypes.string,
- hash: React.PropTypes.string,
- teamId: React.PropTypes.string,
- email: React.PropTypes.string,
- data: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string
+ location: React.PropTypes.object
};
-export default injectIntl(SignupUserComplete); \ No newline at end of file
+export default SignupUserComplete;
diff --git a/web/react/components/suggestion/at_mention_provider.jsx b/web/react/components/suggestion/at_mention_provider.jsx
index 064b75ac5..c5bd13c26 100644
--- a/web/react/components/suggestion/at_mention_provider.jsx
+++ b/web/react/components/suggestion/at_mention_provider.jsx
@@ -40,7 +40,7 @@ class AtMentionSuggestion extends React.Component {
icon = (
<img
className='mention-img'
- src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at + '&' + Utils.getSessionIndex()}
+ src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at}
/>
);
}
diff --git a/web/react/components/suggestion/suggestion_box.jsx b/web/react/components/suggestion/suggestion_box.jsx
index ea9f835eb..12b098cbd 100644
--- a/web/react/components/suggestion/suggestion_box.jsx
+++ b/web/react/components/suggestion/suggestion_box.jsx
@@ -2,7 +2,7 @@
// See License.txt for license information.
import Constants from '../../utils/constants.jsx';
-import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../../action_creators/global_actions.jsx';
import SuggestionStore from '../../stores/suggestion_store.jsx';
import * as Utils from '../../utils/utils.jsx';
@@ -48,7 +48,7 @@ export default class SuggestionBox extends React.Component {
if (!(container.is(e.target) || container.has(e.target).length > 0)) {
// we can't just use blur for this because it fires and hides the children before
// their click handlers can be called
- EventHelpers.emitClearSuggestions(this.suggestionId);
+ GlobalActions.emitClearSuggestions(this.suggestionId);
}
}
@@ -57,7 +57,7 @@ export default class SuggestionBox extends React.Component {
const caret = Utils.getCaretPosition(textbox);
const pretext = textbox.value.substring(0, caret);
- EventHelpers.emitSuggestionPretextChanged(this.suggestionId, pretext);
+ GlobalActions.emitSuggestionPretextChanged(this.suggestionId, pretext);
if (this.props.onUserInput) {
this.props.onUserInput(textbox.value);
@@ -89,13 +89,13 @@ export default class SuggestionBox extends React.Component {
handleKeyDown(e) {
if (SuggestionStore.hasSuggestions(this.suggestionId)) {
if (e.which === KeyCodes.UP) {
- EventHelpers.emitSelectPreviousSuggestion(this.suggestionId);
+ GlobalActions.emitSelectPreviousSuggestion(this.suggestionId);
e.preventDefault();
} else if (e.which === KeyCodes.DOWN) {
- EventHelpers.emitSelectNextSuggestion(this.suggestionId);
+ GlobalActions.emitSelectNextSuggestion(this.suggestionId);
e.preventDefault();
} else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.TAB) {
- EventHelpers.emitCompleteWordSuggestion(this.suggestionId);
+ GlobalActions.emitCompleteWordSuggestion(this.suggestionId);
e.preventDefault();
} else if (this.props.onKeyDown) {
this.props.onKeyDown(e);
diff --git a/web/react/components/suggestion/suggestion_list.jsx b/web/react/components/suggestion/suggestion_list.jsx
index e3ccd0f08..ccebeb990 100644
--- a/web/react/components/suggestion/suggestion_list.jsx
+++ b/web/react/components/suggestion/suggestion_list.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../../action_creators/global_actions.jsx';
import SuggestionStore from '../../stores/suggestion_store.jsx';
export default class SuggestionList extends React.Component {
@@ -36,7 +36,7 @@ export default class SuggestionList extends React.Component {
}
handleItemClick(term, e) {
- EventHelpers.emitCompleteWordSuggestion(this.props.suggestionId, term);
+ GlobalActions.emitCompleteWordSuggestion(this.props.suggestionId, term);
e.preventDefault();
}
diff --git a/web/react/components/team_members_modal.jsx b/web/react/components/team_members_modal.jsx
index 9bdb16438..786e8f947 100644
--- a/web/react/components/team_members_modal.jsx
+++ b/web/react/components/team_members_modal.jsx
@@ -10,8 +10,36 @@ import {FormattedMessage} from 'mm-intl';
const Modal = ReactBootstrap.Modal;
export default class TeamMembersModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.teamChanged = this.teamChanged.bind(this);
+
+ this.state = {
+ team: TeamStore.getCurrent()
+ };
+ }
+ componentDidMount() {
+ if (this.props.show) {
+ this.onShow();
+ }
+
+ TeamStore.addChangeListener(this.teamChanged);
+ }
+
+ componentWillUnmount() {
+ TeamStore.removeChangeListener(this.teamChanged);
+ }
+
+ teamChanged() {
+ this.setState({team: TeamStore.getCurrent()});
+ }
+
render() {
- const team = TeamStore.getCurrent();
+ let teamDisplayName = '';
+ if (this.state.team) {
+ teamDisplayName = this.state.team.display_name;
+ }
let maxHeight = 1000;
if (Utils.windowHeight() <= 1200) {
@@ -29,7 +57,7 @@ export default class TeamMembersModal extends React.Component {
id='team_member_modal.members'
defaultMessage='{team} Members'
values={{
- team: team.display_name
+ team: teamDisplayName
}}
/>
</Modal.Header>
diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx
index e3207d573..0eb9d1211 100644
--- a/web/react/components/team_settings.jsx
+++ b/web/react/components/team_settings.jsx
@@ -28,6 +28,9 @@ export default class TeamSettings extends React.Component {
}
}
render() {
+ if (!this.state.team) {
+ return null;
+ }
var result;
switch (this.props.activeTab) {
case 'general':
diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx
index 7dd645b25..a81b22d90 100644
--- a/web/react/components/team_signup_with_email.jsx
+++ b/web/react/components/team_signup_with_email.jsx
@@ -5,6 +5,7 @@ import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
+import {browserHistory} from 'react-router';
const holders = defineMessages({
emailError: {
@@ -47,9 +48,9 @@ class EmailSignUpPage extends React.Component {
Client.signupTeam(team.email,
(data) => {
if (data.follow_link) {
- window.location.href = data.follow_link;
+ browserHistory.push(data.follow_link);
} else {
- window.location.href = `/signup_team_confirm/?email=${encodeURIComponent(team.email)}`;
+ browserHistory.push(`/signup_team_confirm/?email=${encodeURIComponent(team.email)}`);
}
},
(err) => {
@@ -117,4 +118,4 @@ EmailSignUpPage.propTypes = {
intl: intlShape.isRequired
};
-export default injectIntl(EmailSignUpPage); \ No newline at end of file
+export default injectIntl(EmailSignUpPage);
diff --git a/web/react/components/user_list_row.jsx b/web/react/components/user_list_row.jsx
index d8442e770..1ca40687f 100644
--- a/web/react/components/user_list_row.jsx
+++ b/web/react/components/user_list_row.jsx
@@ -32,7 +32,7 @@ export default function UserListRow({user, actions}) {
>
<img
className='profile-img'
- src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
/>
<div
className='user-list-item__details'
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index 31b2b9907..e7a286b77 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -26,22 +26,27 @@ export default class UserProfile extends React.Component {
}
}
render() {
- var name = Utils.displayUsername(this.props.user.id);
- if (this.props.overwriteName) {
- name = this.props.overwriteName;
- } else if (!name) {
- name = '...';
+ let name = '...';
+ let email = '';
+ let profileImg = '';
+ if (this.props.user) {
+ name = Utils.displayUsername(this.props.user.id);
+ email = this.props.user.email;
+ profileImg = '/api/v1/users/' + this.props.user.id + '/image?time=' + this.props.user.update_at;
}
- if (this.props.disablePopover) {
- return <div>{name}</div>;
+ if (this.props.overwriteName) {
+ name = this.props.overwriteName;
}
- var profileImg = '/api/v1/users/' + this.props.user.id + '/image?time=' + this.props.user.update_at + '&' + Utils.getSessionIndex();
if (this.props.overwriteImage) {
profileImg = this.props.overwriteImage;
}
+ if (this.props.disablePopover) {
+ return <div>{name}</div>;
+ }
+
var dataContent = [];
dataContent.push(
<img
@@ -69,14 +74,14 @@ export default class UserProfile extends React.Component {
dataContent.push(
<div
data-toggle='tooltip'
- title={this.props.user.email}
+ title={email}
key='user-popover-email'
>
<a
- href={'mailto:' + this.props.user.email}
+ href={'mailto:' + email}
className='text-nowrap text-lowercase user-popover__email'
>
- {this.props.user.email}
+ {email}
</a>
</div>
);
@@ -114,7 +119,7 @@ UserProfile.defaultProps = {
disablePopover: false
};
UserProfile.propTypes = {
- user: React.PropTypes.object.isRequired,
+ user: React.PropTypes.object,
overwriteName: React.PropTypes.string,
overwriteImage: React.PropTypes.string,
disablePopover: React.PropTypes.bool
diff --git a/web/react/components/user_settings/manage_languages.jsx b/web/react/components/user_settings/manage_languages.jsx
index 2d1c74717..6b00a65c7 100644
--- a/web/react/components/user_settings/manage_languages.jsx
+++ b/web/react/components/user_settings/manage_languages.jsx
@@ -5,6 +5,7 @@ import SettingItemMax from '../setting_item_max.jsx';
import * as Client from '../../utils/client.jsx';
import * as Utils from '../../utils/utils.jsx';
+import * as GlobalActions from '../../action_creators/global_actions.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -41,7 +42,7 @@ export default class ManageLanguage extends React.Component {
submitUser(user) {
Client.updateUser(user,
() => {
- window.location.reload(true);
+ GlobalActions.newLocalizationSelected(user.locale);
},
(err) => {
let serverError;
diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx
index 0acfd4a16..1dd564c8d 100644
--- a/web/react/components/user_settings/user_settings_developer.jsx
+++ b/web/react/components/user_settings/user_settings_developer.jsx
@@ -3,7 +3,7 @@
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
-import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../../action_creators/global_actions.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
@@ -28,7 +28,7 @@ class DeveloperTab extends React.Component {
}
register() {
this.props.closeModal();
- EventHelpers.showRegisterAppModal();
+ GlobalActions.showRegisterAppModal();
}
render() {
var appSection;
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index b0b1c414e..235892819 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -13,7 +13,7 @@ import Constants from '../../utils/constants.jsx';
import * as AsyncClient from '../../utils/async_client.jsx';
import * as Utils from '../../utils/utils.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
+import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl';
const holders = defineMessages({
usernameReserved: {
@@ -712,7 +712,7 @@ class UserSettingsGeneralTab extends React.Component {
<SettingPicture
title={formatMessage(holders.profilePicture)}
submit={this.submitPicture}
- src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update + '&' + Utils.getSessionIndex()}
+ src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update}
server_error={serverError}
client_error={clientError}
updateSection={(e) => {
@@ -729,7 +729,14 @@ class UserSettingsGeneralTab extends React.Component {
let minMessage = formatMessage(holders.uploadImage);
if (user.last_picture_update) {
minMessage = formatMessage(holders.imageUpdated, {
- date: new Date(user.last_picture_update).toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'})
+ date: (
+ <FormattedDate
+ value={new Date(user.last_picture_update)}
+ day='2-digit'
+ month='short'
+ year='numeric'
+ />
+ )
});
}
pictureSection = (
@@ -805,4 +812,4 @@ UserSettingsGeneralTab.propTypes = {
collapseModal: React.PropTypes.func.isRequired
};
-export default injectIntl(UserSettingsGeneralTab); \ No newline at end of file
+export default injectIntl(UserSettingsGeneralTab);
diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index fa3415988..0c4a3d526 100644
--- a/web/react/components/user_settings/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -73,27 +73,35 @@ class UserSettingsModal extends React.Component {
this.updateTab = this.updateTab.bind(this);
this.updateSection = this.updateSection.bind(this);
+ this.onUserChanged = this.onUserChanged.bind(this);
this.state = {
active_tab: 'general',
active_section: '',
showConfirmModal: false,
- enforceFocus: true
+ enforceFocus: true,
+ currentUser: UserStore.getCurrentUser()
};
this.requireConfirm = false;
}
+ onUserChanged() {
+ this.setState({currentUser: UserStore.getCurrentUser()});
+ }
+
componentDidMount() {
if (this.props.show) {
this.handleShow();
}
+ UserStore.addChangeListener(this.onUserChanged);
}
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
this.handleShow();
}
+ UserStore.removeChangeListener(this.onUserChanged);
}
handleShow() {
@@ -235,8 +243,10 @@ class UserSettingsModal extends React.Component {
render() {
const {formatMessage} = this.props.intl;
- var currentUser = UserStore.getCurrentUser();
- var isAdmin = Utils.isAdmin(currentUser.roles);
+ if (this.state.currentUser == null) {
+ return (<div/>);
+ }
+ var isAdmin = Utils.isAdmin(this.state.currentUser.roles);
var tabs = [];
tabs.push({name: 'general', uiName: formatMessage(holders.general), icon: 'glyphicon glyphicon-cog'});
diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx
index cba7ffdea..0b6b6c398 100644
--- a/web/react/components/user_settings/user_settings_security.jsx
+++ b/web/react/components/user_settings/user_settings_security.jsx
@@ -14,7 +14,7 @@ import * as AsyncClient from '../../utils/async_client.jsx';
import * as Utils from '../../utils/utils.jsx';
import Constants from '../../utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
+import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl';
const holders = defineMessages({
currentPasswordError: {
@@ -218,11 +218,24 @@ class SecurityTab extends React.Component {
var describe;
var d = new Date(this.props.user.last_password_update);
- const locale = global.window.mm_locale;
const hours12 = !Utils.isMilitaryTime();
describe = formatMessage(holders.lastUpdated, {
- date: d.toLocaleDateString(locale, {month: 'short', day: '2-digit', year: 'numeric'}),
- time: d.toLocaleTimeString(locale, {hour12: hours12, hour: '2-digit', minute: '2-digit'})
+ date: (
+ <FormattedDate
+ value={d}
+ day='2-digit'
+ month='short'
+ year='numeric'
+ />
+ ),
+ time: (
+ <FormattedTime
+ value={d}
+ hour12={hours12}
+ hour='2-digit'
+ minute='2-digit'
+ />
+ )
});
updateSectionStatus = function updateSection() {
@@ -251,7 +264,7 @@ class SecurityTab extends React.Component {
<div>
<a
className='btn btn-primary'
- href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email)}
+ href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service}
>
<FormattedMessage
id='user.settings.security.switchEmail'
@@ -269,7 +282,7 @@ class SecurityTab extends React.Component {
<div>
<a
className='btn btn-primary'
- href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&new_type=' + Constants.GITLAB_SERVICE}
+ href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service + '&new_type=' + Constants.GITLAB_SERVICE}
>
<FormattedMessage
id='user.settings.security.switchGitlab'
@@ -287,7 +300,7 @@ class SecurityTab extends React.Component {
<div>
<a
className='btn btn-primary'
- href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&new_type=' + Constants.GOOGLE_SERVICE}
+ href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service + '&new_type=' + Constants.GOOGLE_SERVICE}
>
<FormattedMessage
id='user.settings.security.switchGoogle'
@@ -456,4 +469,4 @@ SecurityTab.propTypes = {
setEnforceFocus: React.PropTypes.func.isRequired
};
-export default injectIntl(SecurityTab); \ No newline at end of file
+export default injectIntl(SecurityTab);
diff --git a/web/react/package.json b/web/react/package.json
index 07ffa0cdf..509c9967b 100644
--- a/web/react/package.json
+++ b/web/react/package.json
@@ -11,6 +11,8 @@
"marked": "mattermost/marked#cb85e5cc81bc7937dbb73c3c53d9532b1b97e3ca",
"mm-intl": "mattermost/mm-intl#805442fd474fa40cd586ddeda404dbbe8e60626d",
"object-assign": "4.0.1",
+ "react": "0.14.3",
+ "react-router": "2.0.0",
"twemoji": "1.4.1"
},
"devDependencies": {
diff --git a/web/react/pages/admin_console.jsx b/web/react/pages/admin_console.jsx
deleted file mode 100644
index 989936d9e..000000000
--- a/web/react/pages/admin_console.jsx
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import ErrorBar from '../components/error_bar.jsx';
-import SelectTeamModal from '../components/admin_console/select_team_modal.jsx';
-import AdminController from '../components/admin_console/admin_controller.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <div>
- <ErrorBar/>
- <AdminController
- tab={this.props.map.ActiveTab}
- teamId={this.props.map.TeamId}
- />
- <SelectTeamModal/>
- </div>
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_admin_console_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('admin_controller')
- );
-};
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
deleted file mode 100644
index bc78c049c..000000000
--- a/web/react/pages/channel.jsx
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import ChannelView from '../components/channel_view.jsx';
-import ChannelLoader from '../components/channel_loader.jsx';
-import ErrorBar from '../components/error_bar.jsx';
-import * as Client from '../utils/client.jsx';
-
-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 * as EventHelpers from '../dispatcher/event_helpers.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <div className='channel-view'>
- <ChannelLoader/>
- <ErrorBar/>
- <ChannelView/>
- <GetPostLinkModal/>
- <GetTeamInviteLinkModal/>
- <InviteMemberModal/>
- <ImportThemeModal/>
- <TeamSettingsModal/>
- <MoreChannelsModal/>
- <EditPostModal/>
- <DeletePostModal/>
- <RemovedFromChannelModal/>
- <RegisterAppModal/>
- </div>
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_channel_page = function setup(props, team, channel) {
- if (props.PostId === '') {
- EventHelpers.emitChannelClickEvent(channel);
- } else {
- EventHelpers.emitPostFocusEvent(props.PostId);
- }
-
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('channel_view')
- );
-};
diff --git a/web/react/pages/claim_account.jsx b/web/react/pages/claim_account.jsx
deleted file mode 100644
index abbf72ea3..000000000
--- a/web/react/pages/claim_account.jsx
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import ClaimAccount from '../components/claim/claim_account.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <ClaimAccount
- email={this.props.map.Email}
- currentType={this.props.map.CurrentType}
- newType={this.props.map.NewType}
- teamName={this.props.map.TeamName}
- teamDisplayName={this.props.map.TeamDisplayName}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_claim_account_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('claim')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/docs.jsx b/web/react/pages/docs.jsx
deleted file mode 100644
index 2e47e3e6a..000000000
--- a/web/react/pages/docs.jsx
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import Docs from '../components/docs.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <Docs site={this.props.map.Site}/>
- </IntlProvider>
- );
- }
-}
-
-global.window.mm_user = global.window.mm_user || {};
-
-global.window.setup_documentation_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('docs')
- );
-};
diff --git a/web/react/pages/find_team.jsx b/web/react/pages/find_team.jsx
deleted file mode 100644
index 93394fcde..000000000
--- a/web/react/pages/find_team.jsx
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import FindTeam from '../components/find_team.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <FindTeam/>
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_find_team_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('find-team')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx
deleted file mode 100644
index ff81c4994..000000000
--- a/web/react/pages/home.jsx
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import TeamStore from '../stores/team_store.jsx';
-import Constants from '../utils/constants.jsx';
-
-function setupHomePage() {
- var last = null;
- if (last == null || last.length === 0) {
- window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL;
- } else {
- window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + last;
- }
-}
-
-global.window.setup_home_page = setupHomePage;
diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx
deleted file mode 100644
index ec9080945..000000000
--- a/web/react/pages/login.jsx
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import * as Client from '../utils/client.jsx';
-import Login from '../components/login.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <Login
- teamDisplayName={this.props.map.TeamDisplayName}
- teamName={this.props.map.TeamName}
- inviteId={this.props.map.InviteId}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_login_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('login')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/password_reset.jsx b/web/react/pages/password_reset.jsx
deleted file mode 100644
index 7caff5034..000000000
--- a/web/react/pages/password_reset.jsx
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import PasswordReset from '../components/password_reset.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <PasswordReset
- isReset={this.props.map.IsReset}
- teamDisplayName={this.props.map.TeamDisplayName}
- teamName={this.props.map.TeamName}
- hash={this.props.map.Hash}
- data={this.props.map.Data}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_password_reset_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('reset')
- );
-};
diff --git a/web/react/pages/root.jsx b/web/react/pages/root.jsx
new file mode 100644
index 000000000..d0b06e32e
--- /dev/null
+++ b/web/react/pages/root.jsx
@@ -0,0 +1,290 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router';
+import Root from '../components/root.jsx';
+import Login from '../components/login.jsx';
+import LoggedIn from '../components/logged_in.jsx';
+import NotLoggedIn from '../components/not_logged_in.jsx';
+import NeedsTeam from '../components/needs_team.jsx';
+import PasswordResetSendLink from '../components/password_reset_send_link.jsx';
+import PasswordResetForm from '../components/password_reset_form.jsx';
+import ChannelView from '../components/channel_view.jsx';
+import Sidebar from '../components/sidebar.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import ErrorStore from '../stores/error_store.jsx';
+import BrowserStore from '../stores/browser_store.jsx';
+import SignupTeam from '../components/signup_team.jsx';
+import * as Client from '../utils/client.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
+import SignupTeamConfirm from '../components/signup_team_confirm.jsx';
+import SignupUserComplete from '../components/signup_user_complete.jsx';
+import ShouldVerifyEmail from '../components/should_verify_email.jsx';
+import DoVerifyEmail from '../components/do_verify_email.jsx';
+import AdminConsole from '../components/admin_console/admin_controller.jsx';
+import ClaimAccount from '../components/claim/claim_account.jsx';
+
+import SignupTeamComplete from '../components/signup_team_complete/components/signup_team_complete.jsx';
+import WelcomePage from '../components/signup_team_complete/components/team_signup_welcome_page.jsx';
+import TeamDisplayNamePage from '../components/signup_team_complete/components/team_signup_display_name_page.jsx';
+import TeamURLPage from '../components/signup_team_complete/components/team_signup_url_page.jsx';
+import SendInivtesPage from '../components/signup_team_complete/components/team_signup_send_invites_page.jsx';
+import UsernamePage from '../components/signup_team_complete/components/team_signup_username_page.jsx';
+import PasswordPage from '../components/signup_team_complete/components/team_signup_password_page.jsx';
+import FinishedPage from '../components/signup_team_complete/components/team_signup_finished.jsx';
+
+// This is for anything that needs to be done for ALL react components.
+// This runs before we start to render anything.
+function preRenderSetup(callwhendone) {
+ const d1 = Client.getClientConfig(
+ (data, textStatus, xhr) => {
+ if (!data) {
+ return;
+ }
+
+ global.window.mm_config = data;
+
+ var serverVersion = xhr.getResponseHeader('X-Version-ID');
+
+ if (serverVersion !== BrowserStore.getLastServerVersion()) {
+ if (!BrowserStore.getLastServerVersion() || BrowserStore.getLastServerVersion() === '') {
+ BrowserStore.setLastServerVersion(serverVersion);
+ } else {
+ BrowserStore.setLastServerVersion(serverVersion);
+ window.location.reload(true);
+ console.log('Detected version update refreshing the page'); //eslint-disable-line no-console
+ }
+ }
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getClientConfig');
+ }
+ );
+
+ const d2 = Client.getClientLicenceConfig(
+ (data) => {
+ if (!data) {
+ return;
+ }
+
+ global.window.mm_license = data;
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getClientLicenceConfig');
+ }
+ );
+
+ // Set these here so they don't fail in client.jsx track
+ global.window.analytics = {};
+ global.window.analytics.page = () => {
+ // Do Nothing
+ };
+ global.window.analytics.track = () => {
+ // Do Nothing
+ };
+
+ $.when(d1, d2).done(callwhendone);
+}
+
+function preLoggedIn(nextState, replace, callback) {
+ const d1 = Client.getAllPreferences(
+ (data) => {
+ if (!data) {
+ return;
+ }
+
+ PreferenceStore.setPreferences(data);
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getAllPreferences');
+ }
+ );
+
+ const d2 = AsyncClient.getChannels();
+
+ $.when(d1, d2).done(() => callback());
+}
+
+function onChannelChange(nextState) {
+ const channelName = nextState.params.channel;
+
+ // Make sure we have all the channels
+ AsyncClient.getChannels(true);
+
+ // Get our channel's ID
+ const channel = ChannelStore.getByName(channelName);
+
+ // User clicked channel
+ GlobalActions.emitChannelClickEvent(channel);
+}
+
+function onRootEnter(nextState, replace, callback) {
+ if (nextState.location.pathname === '/') {
+ Client.getMeLoggedIn((data) => {
+ if (!data || data.logged_in === 'false') {
+ replace({pathname: '/signup_team'});
+ callback();
+ } else {
+ replace({pathname: '/' + data.team_name + '/channels/town-square'});
+ callback();
+ }
+ });
+ return;
+ }
+
+ callback();
+}
+
+function onPermalinkEnter(nextState) {
+ const postId = nextState.params.postid;
+
+ GlobalActions.emitPostFocusEvent(postId);
+}
+
+function onLoggedOut(nextState) {
+ const teamName = nextState.params.team;
+ Client.logout(
+ () => {
+ browserHistory.push('/' + teamName + '/login');
+ BrowserStore.signalLogout();
+ BrowserStore.clear();
+ ErrorStore.clearLastError();
+ },
+ () => {
+ browserHistory.push('/' + teamName + '/login');
+ }
+ );
+}
+
+function renderRootComponent() {
+ ReactDOM.render((
+ <Router
+ history={browserHistory}
+ >
+ <Route
+ path='/'
+ component={Root}
+ onEnter={onRootEnter}
+ >
+ <Route
+ component={LoggedIn}
+ onEnter={preLoggedIn}
+ >
+ <Route
+ path=':team/channels/:channel'
+ onEnter={onChannelChange}
+ components={{
+ sidebar: Sidebar,
+ center: ChannelView
+ }}
+ />
+ <Route
+ path=':team/pl/:postid'
+ onEnter={onPermalinkEnter}
+ components={{
+ sidebar: Sidebar,
+ center: ChannelView
+ }}
+ />
+ <Route
+ path=':team/logout'
+ onEnter={onLoggedOut}
+ components={{
+ sidebar: null,
+ center: null
+ }}
+ />
+ <Route
+ path='admin_console'
+ components={{
+ sidebar: null,
+ center: AdminConsole
+ }}
+ />
+ </Route>
+ <Route component={NotLoggedIn}>
+ <Route
+ path='signup_team'
+ component={SignupTeam}
+ />
+ <Route
+ path='signup_team_complete'
+ component={SignupTeamComplete}
+ >
+ <IndexRoute component={FinishedPage}/>
+ <Route
+ path='welcome'
+ component={WelcomePage}
+ />
+ <Route
+ path='team_display_name'
+ component={TeamDisplayNamePage}
+ />
+ <Route
+ path='team_url'
+ component={TeamURLPage}
+ />
+ <Route
+ path='invites'
+ component={SendInivtesPage}
+ />
+ <Route
+ path='username'
+ component={UsernamePage}
+ />
+ <Route
+ path='password'
+ component={PasswordPage}
+ />
+ </Route>
+ <Route
+ path='signup_user_complete'
+ component={SignupUserComplete}
+ />
+ <Route
+ path='signup_team_confirm'
+ component={SignupTeamConfirm}
+ />
+ <Route
+ path='should_verify_email'
+ component={ShouldVerifyEmail}
+ />
+ <Route
+ path='do_verify_email'
+ component={DoVerifyEmail}
+ />
+ <Route
+ path=':team'
+ component={NeedsTeam}
+ >
+ <IndexRedirect to='login'/>
+ <Route
+ path='login'
+ component={Login}
+ />
+ <Route
+ path='claim'
+ component={ClaimAccount}
+ />
+ <Route
+ path='reset_password'
+ component={PasswordResetSendLink}
+ />
+ <Route
+ path='reset_password_complete'
+ component={PasswordResetForm}
+ />
+ </Route>
+ </Route>
+ </Route>
+ </Router>
+ ),
+ document.getElementById('root'));
+}
+
+global.window.setup_root = () => {
+ // Do the pre-render setup and call renderRootComponent when done
+ preRenderSetup(renderRootComponent);
+};
diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx
deleted file mode 100644
index f276c3ff7..000000000
--- a/web/react/pages/signup_team.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import SignupTeam from '../components/signup_team.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired,
- teams: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <SignupTeam teams={this.props.teams}/>
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_signup_team_page = function setup(props) {
- var teams = [];
-
- for (var prop in props) {
- if (props.hasOwnProperty(prop)) {
- if (prop !== 'Title' && prop !== 'Locale' && prop !== 'Info') {
- teams.push({name: prop, display_name: props[prop]});
- }
- }
- }
-
- ReactDOM.render(
- <Root
- map={props}
- teams={teams}
- />,
- document.getElementById('signup-team')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/signup_team_complete.jsx b/web/react/pages/signup_team_complete.jsx
deleted file mode 100644
index 8c237f698..000000000
--- a/web/react/pages/signup_team_complete.jsx
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import SignupTeamComplete from '../components/signup_team_complete.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <SignupTeamComplete
- email={this.props.map.Email}
- hash={this.props.map.Hash}
- data={this.props.map.Data}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_signup_team_complete_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('signup-team-complete')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/signup_team_confirm.jsx b/web/react/pages/signup_team_confirm.jsx
deleted file mode 100644
index 13c8f3fd0..000000000
--- a/web/react/pages/signup_team_confirm.jsx
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import SignupTeamConfirm from '../components/signup_team_confirm.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <SignupTeamConfirm
- email={this.props.map.Email}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_signup_team_confirm_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('signup-team-confirm')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx
deleted file mode 100644
index a14f2140b..000000000
--- a/web/react/pages/signup_user_complete.jsx
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import SignupUserComplete from '../components/signup_user_complete.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <SignupUserComplete
- teamId={this.props.map.TeamId}
- teamName={this.props.map.TeamName}
- teamDisplayName={this.props.map.TeamDisplayName}
- email={this.props.map.Email}
- hash={this.props.map.Hash}
- data={this.props.map.Data}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_signup_user_complete_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('signup-user-complete')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx
deleted file mode 100644
index 6b336daa1..000000000
--- a/web/react/pages/verify.jsx
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import EmailVerify from '../components/email_verify.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <EmailVerify
- isVerified={this.props.map.IsVerified}
- teamURL={this.props.map.TeamURL}
- userEmail={this.props.map.UserEmail}
- resendSuccess={this.props.map.ResendSuccess}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setupVerifyPage = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('verify')
- );
-};
diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx
index 5c911e94b..9f7f6e7ff 100644
--- a/web/react/stores/admin_store.jsx
+++ b/web/react/stores/admin_store.jsx
@@ -121,7 +121,11 @@ class AdminStoreClass extends EventEmitter {
}
getSelectedTeams() {
- return BrowserStore.getItem('seleted_teams');
+ const result = BrowserStore.getItem('seleted_teams');
+ if (!result) {
+ return {};
+ }
+ return result;
}
saveSelectedTeams(teams) {
@@ -156,7 +160,3 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => {
});
export default AdminStore;
-
-if (window.mm_config.EnableDeveloper === 'true') {
- window.AdminStore = AdminStore;
-}
diff --git a/web/react/stores/analytics_store.jsx b/web/react/stores/analytics_store.jsx
index 0ad989206..ec827f6d7 100644
--- a/web/react/stores/analytics_store.jsx
+++ b/web/react/stores/analytics_store.jsx
@@ -83,7 +83,3 @@ AnalyticsStore.dispatchToken = AppDispatcher.register((payload) => {
});
export default AnalyticsStore;
-
-if (window.mm_config.EnableDeveloper === 'true') {
- window.AnalyticsStore = AnalyticsStore;
-}
diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx
index 3417faaaf..3b35916b3 100644
--- a/web/react/stores/browser_store.jsx
+++ b/web/react/stores/browser_store.jsx
@@ -4,8 +4,8 @@
import {generateId} from '../utils/utils.jsx';
function getPrefix() {
- if (global.window.mm_user) {
- return global.window.mm_user.id + '_';
+ if (global.window.mm_current_user_id) {
+ return global.window.mm_current_user_id + '_';
}
return 'unknown_';
@@ -31,7 +31,9 @@ class BrowserStoreClass {
this.isSignallingLogout = this.isSignallingLogout.bind(this);
this.signalLogin = this.signalLogin.bind(this);
this.isSignallingLogin = this.isSignallingLogin.bind(this);
+ }
+ checkVersion() {
var currentVersion = sessionStorage.getItem('storage_version');
if (currentVersion !== global.window.mm_config.Version) {
sessionStorage.clear();
diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx
index eac24b071..60cb10de7 100644
--- a/web/react/stores/channel_store.jsx
+++ b/web/react/stores/channel_store.jsx
@@ -350,7 +350,3 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
});
export default ChannelStore;
-
-if (window.mm_config.EnableDeveloper === 'true') {
- window.ChannelStore = ChannelStore;
-}
diff --git a/web/react/stores/file_store.jsx b/web/react/stores/file_store.jsx
index c1fd0ef74..6d7e0f354 100644
--- a/web/react/stores/file_store.jsx
+++ b/web/react/stores/file_store.jsx
@@ -57,9 +57,4 @@ class FileStore extends EventEmitter {
}
}
-const instance = new FileStore();
-export default instance;
-
-if (window.mm_config.EnableDeveloper === 'true') {
- window.FileStore = instance;
-}
+export default new FileStore();
diff --git a/web/react/stores/localization_store.jsx b/web/react/stores/localization_store.jsx
new file mode 100644
index 000000000..0e3a63724
--- /dev/null
+++ b/web/react/stores/localization_store.jsx
@@ -0,0 +1,60 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import EventEmitter from 'events';
+import Constants from '../utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
+
+const CHANGE_EVENT = 'change';
+
+class LocalizationStoreClass extends EventEmitter {
+ constructor() {
+ super();
+
+ this.currentLocale = 'en';
+ this.currentTranslations = null;
+ }
+
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ setCurrentLocale(locale, translations) {
+ this.currentLocale = locale;
+ this.currentTranslations = translations;
+ }
+
+ getLocale() {
+ return this.currentLocale;
+ }
+
+ getTranslations() {
+ return this.currentTranslations;
+ }
+}
+
+var LocalizationStore = new LocalizationStoreClass();
+LocalizationStore.setMaxListeners(0);
+
+LocalizationStore.dispatchToken = AppDispatcher.register((payload) => {
+ var action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECEIVED_LOCALE:
+ LocalizationStore.setCurrentLocale(action.locale, action.translations);
+ LocalizationStore.emitChange();
+ break;
+ default:
+ }
+});
+
+export default LocalizationStore;
diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx
index 1819fffc0..5ea38030b 100644
--- a/web/react/stores/modal_store.jsx
+++ b/web/react/stores/modal_store.jsx
@@ -45,7 +45,3 @@ class ModalStoreClass extends EventEmitter {
const ModalStore = new ModalStoreClass();
export default ModalStore;
-
-if (window.mm_config.EnableDeveloper === 'true') {
- window.ModalStore = ModalStore;
-}
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 5cc3f300d..a6dfcd46f 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -608,7 +608,3 @@ function isPostListNull(pl) {
return false;
}
-
-if (window.mm_config.EnableDeveloper === 'true') {
- window.PostStore = PostStore;
-}
diff --git a/web/react/stores/search_store.jsx b/web/react/stores/search_store.jsx
index 96071665c..549f355ef 100644
--- a/web/react/stores/search_store.jsx
+++ b/web/react/stores/search_store.jsx
@@ -135,7 +135,3 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => {
});
export default SearchStore;
-
-if (window.mm_config.EnableDeveloper === 'true') {
- window.SearchStore = SearchStore;
-}
diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx
index 9b2b049b7..ad24a04cd 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -10,7 +10,7 @@ import EventEmitter from 'events';
import * as Utils from '../utils/utils.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import Constants from '../utils/constants.jsx';
const SocketEvents = Constants.SocketEvents;
@@ -42,10 +42,6 @@ class SocketStoreClass extends EventEmitter {
return;
}
- if (!global.window.hasOwnProperty('mm_session_token_index')) {
- return;
- }
-
this.setMaxListeners(0);
if (window.WebSocket && !conn) {
@@ -54,7 +50,7 @@ class SocketStoreClass extends EventEmitter {
protocol = 'wss://';
}
- var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket?' + Utils.getSessionIndex();
+ var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket';
if (this.failCount === 0) {
console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console
@@ -204,7 +200,7 @@ class SocketStoreClass extends EventEmitter {
function handleNewPostEvent(msg, translations) {
// Store post
const post = JSON.parse(msg.props.post);
- EventHelpers.emitPostRecievedEvent(post);
+ GlobalActions.emitPostRecievedEvent(post);
// Update channel state
if (ChannelStore.getCurrentId() === msg.channel_id) {
@@ -291,7 +287,7 @@ function handlePostEditEvent(msg) {
function handlePostDeleteEvent(msg) {
const post = JSON.parse(msg.props.post);
- EventHelpers.emitPostDeletedEvent(post);
+ GlobalActions.emitPostDeletedEvent(post);
}
function handleNewUserEvent() {
@@ -337,7 +333,7 @@ function handleChannelViewedEvent(msg) {
function handlePreferenceChangedEvent(msg) {
const preference = JSON.parse(msg.props.preference);
- EventHelpers.emitPreferenceChangedEvent(preference);
+ GlobalActions.emitPreferenceChangedEvent(preference);
}
var SocketStore = new SocketStoreClass();
diff --git a/web/react/stores/suggestion_store.jsx b/web/react/stores/suggestion_store.jsx
index 487bae843..efd2b76ed 100644
--- a/web/react/stores/suggestion_store.jsx
+++ b/web/react/stores/suggestion_store.jsx
@@ -258,9 +258,4 @@ class SuggestionStore extends EventEmitter {
}
}
-const instance = new SuggestionStore();
-export default instance;
-
-if (window.mm_config.EnableDeveloper === 'true') {
- window.SuggestionStore = instance;
-}
+export default new SuggestionStore();
diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx
index 493d6bc4d..354a07b72 100644
--- a/web/react/stores/team_store.jsx
+++ b/web/react/stores/team_store.jsx
@@ -6,7 +6,6 @@ import EventEmitter from 'events';
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
-import BrowserStore from '../stores/browser_store.jsx';
const CHANGE_EVENT = 'change';
@@ -33,6 +32,9 @@ class TeamStoreClass extends EventEmitter {
this.getCurrentTeamUrl = this.getCurrentTeamUrl.bind(this);
this.getCurrentInviteLink = this.getCurrentInviteLink.bind(this);
this.saveTeam = this.saveTeam.bind(this);
+
+ this.teams = {};
+ this.currentTeamId = '';
}
emitChange() {
@@ -65,11 +67,11 @@ class TeamStoreClass extends EventEmitter {
}
getAll() {
- return BrowserStore.getItem('user_teams', {});
+ return this.teams;
}
getCurrentId() {
- var team = global.window.mm_team;
+ var team = this.get(this.currentTeamId);
if (team) {
return team.id;
@@ -79,11 +81,13 @@ class TeamStoreClass extends EventEmitter {
}
getCurrent() {
- if (global.window.mm_team != null && this.get(global.window.mm_team.id) == null) {
- this.saveTeam(global.window.mm_team);
+ const team = this.teams[this.currentTeamId];
+
+ if (team) {
+ return team;
}
- return global.window.mm_team;
+ return null;
}
getCurrentTeamUrl() {
@@ -104,9 +108,16 @@ class TeamStoreClass extends EventEmitter {
}
saveTeam(team) {
- var teams = this.getAll();
- teams[team.id] = team;
- BrowserStore.setItem('user_teams', teams);
+ this.teams[team.id] = team;
+ }
+
+ saveTeams(teams) {
+ this.teams = teams;
+ }
+
+ saveMyTeam(team) {
+ this.saveTeam(team);
+ this.currentTeamId = team.id;
}
}
@@ -116,17 +127,16 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
- case ActionTypes.RECEIVED_TEAM:
- TeamStore.saveTeam(action.team);
+ case ActionTypes.RECEIVED_MY_TEAM:
+ TeamStore.saveMyTeam(action.team);
+ TeamStore.emitChange();
+ break;
+ case ActionTypes.RECEIVED_ALL_TEAMS:
+ TeamStore.saveTeams(action.teams);
TeamStore.emitChange();
break;
-
default:
}
});
export default TeamStore;
-
-if (window.mm_config.EnableDeveloper === 'true') {
- window.TeamStore = TeamStore;
-}
diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx
index 9fcd2440e..c1e5c75dc 100644
--- a/web/react/stores/user_store.jsx
+++ b/web/react/stores/user_store.jsx
@@ -11,13 +11,13 @@ import BrowserStore from './browser_store.jsx';
const CHANGE_EVENT = 'change';
const CHANGE_EVENT_SESSIONS = 'change_sessions';
const CHANGE_EVENT_AUDITS = 'change_audits';
-const CHANGE_EVENT_TEAMS = 'change_teams';
const CHANGE_EVENT_STATUSES = 'change_statuses';
class UserStoreClass extends EventEmitter {
constructor() {
super();
this.profileCache = null;
+ this.currentUserId = '';
}
emitChange(userId) {
@@ -56,18 +56,6 @@ class UserStoreClass extends EventEmitter {
this.removeListener(CHANGE_EVENT_AUDITS, callback);
}
- emitTeamsChange() {
- this.emit(CHANGE_EVENT_TEAMS);
- }
-
- addTeamsChangeListener(callback) {
- this.on(CHANGE_EVENT_TEAMS, callback);
- }
-
- removeTeamsChangeListener(callback) {
- this.removeListener(CHANGE_EVENT_TEAMS, callback);
- }
-
emitStatusesChange() {
this.emit(CHANGE_EVENT_STATUSES);
}
@@ -81,26 +69,17 @@ class UserStoreClass extends EventEmitter {
}
getCurrentUser() {
- if (this.getProfiles()[global.window.mm_user.id] == null) {
- this.saveProfile(global.window.mm_user);
- }
-
- return global.window.mm_user;
+ return this.getProfiles()[this.currentUserId];
}
setCurrentUser(user) {
- var oldUser = global.window.mm_user;
-
- if (oldUser.id === user.id) {
- global.window.mm_user = user;
- this.saveProfile(user);
- } else {
- throw new Error('Problem with setCurrentUser old_user_id=' + oldUser.id + ' new_user_id=' + user.id);
- }
+ this.saveProfile(user);
+ this.currentUserId = user.id;
+ global.window.mm_current_user_id = this.currentUserId;
}
getCurrentId() {
- var user = global.window.mm_user;
+ var user = this.getCurrentUser();
if (user) {
return user.id;
@@ -200,11 +179,22 @@ class UserStoreClass extends EventEmitter {
saveProfiles(profiles) {
const currentId = this.getCurrentId();
- if (currentId in profiles) {
- delete profiles[currentId];
+ if (this.profileCache) {
+ const currentUser = this.profileCache[currentId];
+ if (currentUser) {
+ if (currentId in profiles) {
+ delete profiles[currentId];
+ }
+
+ this.profileCache = profiles;
+ this.profileCache[currentId] = currentUser;
+ } else {
+ this.profileCache = profiles;
+ }
+ } else {
+ this.profileCache = profiles;
}
- this.profileCache = profiles;
BrowserStore.setItem('profiles', profiles);
}
@@ -224,14 +214,6 @@ class UserStoreClass extends EventEmitter {
return BrowserStore.getItem('audits', {loading: true});
}
- setTeams(teams) {
- BrowserStore.setItem('teams', teams);
- }
-
- getTeams() {
- return BrowserStore.getItem('teams', []);
- }
-
getCurrentMentionKeys() {
return this.getMentionKeys(this.getCurrentId());
}
@@ -312,10 +294,6 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => {
UserStore.setAudits(action.audits);
UserStore.emitAuditsChange();
break;
- case ActionTypes.RECEIVED_TEAMS:
- UserStore.setTeams(action.teams);
- UserStore.emitTeamsChange();
- break;
case ActionTypes.RECEIVED_STATUSES:
UserStore.pSetStatuses(action.statuses);
UserStore.emitStatusesChange();
@@ -325,7 +303,3 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => {
});
export {UserStore as default};
-
-if (window.mm_config.EnableDeveloper === 'true') {
- window.UserStore = UserStore;
-}
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 7d5e1bd0f..b9770a6e9 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import * as client from './client.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import BrowserStore from '../stores/browser_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
@@ -44,15 +45,19 @@ function isCallInProgress(callName) {
export function getChannels(checkVersion) {
if (isCallInProgress('getChannels')) {
- return;
+ return null;
}
callTracker.getChannels = utils.getTimestamp();
- client.getChannels(
+ return client.getChannels(
(data, textStatus, xhr) => {
callTracker.getChannels = 0;
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
if (checkVersion) {
var serverVersion = xhr.getResponseHeader('X-Version-ID');
@@ -67,10 +72,6 @@ export function getChannels(checkVersion) {
}
}
- if (xhr.status === 304 || !data) {
- return;
- }
-
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_CHANNELS,
channels: data.channels,
@@ -392,36 +393,6 @@ export function getAllTeams() {
);
}
-export function findTeams(email) {
- if (isCallInProgress('findTeams_' + email)) {
- return;
- }
-
- var user = UserStore.getCurrentUser();
- if (user) {
- callTracker['findTeams_' + email] = utils.getTimestamp();
- client.findTeams(
- user.email,
- function findTeamsSuccess(data, textStatus, xhr) {
- callTracker['findTeams_' + email] = 0;
-
- if (xhr.status === 304 || !data) {
- return;
- }
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_TEAMS,
- teams: data
- });
- },
- function findTeamsFailure(err) {
- callTracker['findTeams_' + email] = 0;
- dispatchError(err, 'findTeams');
- }
- );
- }
-}
-
export function search(terms) {
if (isCallInProgress('search_' + String(terms))) {
return;
@@ -645,11 +616,11 @@ export function getPostsAfter(postId, offset, numPost) {
export function getMe() {
if (isCallInProgress('getMe')) {
- return;
+ return null;
}
callTracker.getMe = utils.getTimestamp();
- client.getMe(
+ return client.getMe(
(data, textStatus, xhr) => {
callTracker.getMe = 0;
@@ -661,6 +632,8 @@ export function getMe() {
type: ActionTypes.RECEIVED_ME,
me: data
});
+
+ GlobalActions.newLocalizationSelected(data.locale);
},
(err) => {
callTracker.getMe = 0;
@@ -706,11 +679,11 @@ export function getStatuses() {
export function getMyTeam() {
if (isCallInProgress('getMyTeam')) {
- return;
+ return null;
}
callTracker.getMyTeam = utils.getTimestamp();
- client.getMyTeam(
+ return client.getMyTeam(
function getMyTeamSuccess(data, textStatus, xhr) {
callTracker.getMyTeam = 0;
@@ -719,7 +692,7 @@ export function getMyTeam() {
}
AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_TEAM,
+ type: ActionTypes.RECEIVED_MY_TEAM,
team: data
});
},
diff --git a/web/react/utils/channel_intro_messages.jsx b/web/react/utils/channel_intro_messages.jsx
index ed94f94b8..94f3f0ce0 100644
--- a/web/react/utils/channel_intro_messages.jsx
+++ b/web/react/utils/channel_intro_messages.jsx
@@ -8,8 +8,7 @@ import ToggleModalButton from '../components/toggle_modal_button.jsx';
import UserProfile from '../components/user_profile.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import Constants from '../utils/constants.jsx';
-import TeamStore from '../stores/team_store.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import {FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'mm-intl';
@@ -40,7 +39,7 @@ export function createDMIntroMessage(channel) {
<div className='post-profile-img__container channel-intro-img'>
<img
className='post-profile-img'
- src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at + '&' + Utils.getSessionIndex()}
+ src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at}
height='50'
width='50'
/>
@@ -93,37 +92,19 @@ export function createOffTopicIntroMessage(channel) {
}
export function createDefaultIntroMessage(channel) {
- const team = TeamStore.getCurrent();
- let inviteModalLink;
- if (team.type === Constants.INVITE_TEAM) {
- inviteModalLink = (
- <a
- className='intro-links'
- href='#'
- onClick={EventHelpers.showInviteMemberModal}
- >
- <i className='fa fa-user-plus'></i>
- <FormattedMessage
- id='intro_messages.inviteOthers'
- defaultMessage='Invite others to this team'
- />
- </a>
- );
- } else {
- inviteModalLink = (
- <a
- className='intro-links'
- href='#'
- onClick={EventHelpers.showGetTeamInviteLinkModal}
- >
- <i className='fa fa-user-plus'></i>
- <FormattedMessage
- id='intro_messages.inviteOthers'
- defaultMessage='Invite others to this team'
- />
- </a>
- );
- }
+ const inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ onClick={GlobalActions.showGetTeamInviteLinkModal}
+ >
+ <i className='fa fa-user-plus'></i>
+ <FormattedMessage
+ id='intro_messages.inviteOthers'
+ defaultMessage='Invite others to this team'
+ />
+ </a>
+ );
return (
<div className='channel-intro'>
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 76d42137a..e00f28a14 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -1,8 +1,8 @@
// See License.txt for license information.
import BrowserStore from '../stores/browser_store.jsx';
-import TeamStore from '../stores/team_store.jsx';
-import ErrorStore from '../stores/error_store.jsx';
+
+import {browserHistory} from 'react-router';
let translations = {
connectionError: 'There appears to be a problem with your internet connection.',
@@ -50,10 +50,10 @@ function handleError(methodName, xhr, status, err) {
if (xhr.status === 401) {
if (window.location.href.indexOf('/channels') === 0) {
- window.location.pathname = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search);
+ browserHistory.push('/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search));
} else {
- var teamURL = window.location.href.split('/channels')[0];
- window.location.href = teamURL + '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search);
+ var teamURL = window.location.pathname.split('/channels')[0];
+ browserHistory.push(teamURL + '/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search));
}
}
@@ -289,13 +289,17 @@ export function switchToEmail(data, success, error) {
track('api', 'api_users_switch_to_email');
}
-export function logout() {
+export function logout(success, error) {
track('api', 'api_users_logout');
- var currentTeamUrl = TeamStore.getCurrentTeamUrl();
- BrowserStore.signalLogout();
- BrowserStore.clear();
- ErrorStore.clearLastError();
- window.location.href = currentTeamUrl + '/logout';
+ $.ajax({
+ url: '/api/v1/users/logout',
+ type: 'POST',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('logout', xhr, status, err);
+ error(e);
+ }
+ });
}
export function loginByEmail(name, email, password, success, error) {
@@ -437,7 +441,7 @@ export function getServerAudits(success, error) {
}
export function getConfig(success, error) {
- $.ajax({
+ return $.ajax({
url: '/api/v1/admin/config',
dataType: 'json',
contentType: 'application/json',
@@ -457,7 +461,6 @@ export function getAnalytics(name, teamId, success, error) {
} else {
url += teamId + '/' + name;
}
-
$.ajax({
url,
dataType: 'json',
@@ -471,6 +474,34 @@ export function getAnalytics(name, teamId, success, error) {
});
}
+export function getClientConfig(success, error) {
+ return $.ajax({
+ url: '/api/v1/admin/client_props',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getClientConfig', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getTeamAnalytics(teamId, name, success, error) {
+ $.ajax({
+ url: '/api/v1/admin/analytics/' + teamId + '/' + name,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('getTeamAnalytics', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function saveConfig(config, success, error) {
$.ajax({
url: '/api/v1/admin/save_config',
@@ -529,6 +560,21 @@ export function getAllTeams(success, error) {
});
}
+export function getMeLoggedIn(success, error) {
+ return $.ajax({
+ cache: false,
+ url: '/api/v1/users/me_logged_in',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getMeLoggedIn', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getMe(success, error) {
var currentUser = null;
$.ajax({
@@ -635,38 +681,6 @@ export function findTeamByName(teamName, success, error) {
});
}
-export function findTeamsSendEmail(email, success, error) {
- $.ajax({
- url: '/api/v1/teams/email_teams',
- dataType: 'json',
- contentType: 'application/json',
- type: 'POST',
- data: JSON.stringify({email: email}),
- success,
- error: function onError(xhr, status, err) {
- var e = handleError('findTeamsSendEmail', xhr, status, err);
- error(e);
- }
- });
-
- track('api', 'api_teams_email_teams');
-}
-
-export function findTeams(email, success, error) {
- $.ajax({
- url: '/api/v1/teams/find_teams',
- dataType: 'json',
- contentType: 'application/json',
- type: 'POST',
- data: JSON.stringify({email: email}),
- success,
- error: function onError(xhr, status, err) {
- var e = handleError('findTeams', xhr, status, err);
- error(e);
- }
- });
-}
-
export function createChannel(channel, success, error) {
$.ajax({
url: '/api/v1/channels/create',
@@ -835,7 +849,7 @@ export function updateLastViewedAt(channelId, success, error) {
}
export function getChannels(success, error) {
- $.ajax({
+ return $.ajax({
cache: false,
url: '/api/v1/channels/',
dataType: 'json',
@@ -901,7 +915,7 @@ export function getChannelExtraInfo(id, memberLimit, success, error) {
url += '/' + memberLimit;
}
- $.ajax({
+ return $.ajax({
url,
dataType: 'json',
contentType: 'application/json',
@@ -1018,7 +1032,7 @@ export function getPostsPage(channelId, offset, limit, success, error, complete)
}
export function getPosts(channelId, since, success, error, complete) {
- $.ajax({
+ return $.ajax({
url: '/api/v1/channels/' + channelId + '/posts/' + since,
dataType: 'json',
type: 'GET',
@@ -1347,7 +1361,7 @@ export function getStatuses(ids, success, error) {
}
export function getMyTeam(success, error) {
- $.ajax({
+ return $.ajax({
url: '/api/v1/teams/me',
dataType: 'json',
type: 'GET',
@@ -1437,7 +1451,7 @@ export function listIncomingHooks(success, error) {
}
export function getAllPreferences(success, error) {
- $.ajax({
+ return $.ajax({
url: '/api/v1/preferences/',
dataType: 'json',
type: 'GET',
@@ -1569,3 +1583,68 @@ export function removeLicenseFile(success, error) {
track('api', 'api_license_upload');
}
+
+export function getClientLicenceConfig(success, error) {
+ return $.ajax({
+ url: '/api/v1/license/client_config',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getClientLicenceConfig', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getInviteInfo(success, error, id) {
+ $.ajax({
+ url: '/api/v1/teams/get_invite_info',
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json',
+ data: JSON.stringify({invite_id: id}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getInviteInfo', xhr, status, err);
+ if (error) {
+ error(e);
+ }
+ }
+ });
+}
+
+export function verifyEmail(success, error, uid, hid) {
+ $.ajax({
+ url: '/api/v1/users/verify_email',
+ type: 'POST',
+ contentType: 'application/json',
+ dataType: 'text',
+ data: JSON.stringify({uid, hid}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('verifyEmail', xhr, status, err);
+ if (error) {
+ error(e);
+ }
+ }
+ });
+}
+
+export function resendVerification(success, error, teamName, email) {
+ $.ajax({
+ url: '/api/v1/users/resend_verification',
+ type: 'POST',
+ contentType: 'application/json',
+ dataType: 'text',
+ data: JSON.stringify({team_name: teamName, email}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('resendVerification', xhr, status, err);
+ if (error) {
+ error(e);
+ }
+ }
+ });
+}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index daea9f43e..2cff4dbed 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -42,13 +42,15 @@ export default {
RECEIVED_MSG: null,
- RECEIVED_TEAM: null,
+ RECEIVED_MY_TEAM: null,
RECEIVED_CONFIG: null,
RECEIVED_LOGS: null,
RECEIVED_SERVER_AUDITS: null,
RECEIVED_ALL_TEAMS: null,
+ RECEIVED_LOCALE: null,
+
SHOW_SEARCH: null,
TOGGLE_IMPORT_THEME_MODAL: null,
@@ -143,6 +145,7 @@ export default {
EMAIL_SERVICE: 'email',
SIGNIN_CHANGE: 'signin_change',
SIGNIN_VERIFIED: 'verified',
+ SESSION_EXPIRED: 'expired',
POST_CHUNK_SIZE: 60,
MAX_POST_CHUNKS: 3,
POST_FOCUS_CONTEXT_RADIUS: 10,
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 6942a8e08..88777164b 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -2,9 +2,10 @@
// See License.txt for license information.
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 ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx';
+import LocalizationStore from '../stores/localization_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
import TeamStore from '../stores/team_store.jsx';
import Constants from '../utils/constants.jsx';
@@ -941,7 +942,7 @@ export function updateAddressBar(channelName) {
}
export function switchChannel(channel) {
- EventHelpers.emitChannelClickEvent(channel);
+ GlobalActions.emitChannelClickEvent(channel);
updateAddressBar(channel.name);
@@ -1130,8 +1131,8 @@ export function fileSizeToString(bytes) {
// Converts a filename (like those attached to Post objects) to a url that can be used to retrieve attachments from the server.
export function getFileUrl(filename, isDownload) {
- const downloadParam = isDownload ? '&download=1' : '';
- return getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + getSessionIndex() + downloadParam;
+ const downloadParam = isDownload ? '?download=1' : '';
+ return getWindowLocationOrigin() + '/api/v1/files/get' + filename + downloadParam;
}
// Gets the name of a file (including extension) from a given url or file path.
@@ -1151,14 +1152,6 @@ export function getWebsocketPort(protocol) {
return '';
}
-export function getSessionIndex() {
- if (global.window.mm_session_token_index >= 0) {
- return 'session_token_index=' + global.window.mm_session_token_index;
- }
-
- return '';
-}
-
// Generates a RFC-4122 version 4 compliant globally unique identifier.
export function generateId() {
// implementation taken from http://stackoverflow.com/a/2117523
@@ -1405,3 +1398,19 @@ export function isPostEphemeral(post) {
export function getRootId(post) {
return post.root_id === '' ? post.id : post.root_id;
}
+
+export function localizeMessage(id, defaultMessage) {
+ const translations = LocalizationStore.getTranslations();
+ if (translations) {
+ const value = translations[id];
+ if (value) {
+ return value;
+ }
+ }
+
+ if (defaultMessage) {
+ return defaultMessage;
+ }
+
+ return id;
+}
diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss
index 5e7f04724..44681291c 100644
--- a/web/sass-files/sass/partials/_sidebar--left.scss
+++ b/web/sass-files/sass/partials/_sidebar--left.scss
@@ -24,12 +24,6 @@
padding: 1em 1em 0;
display: none;
}
- > div {
- height: 100%;
- position: absolute;
- padding-bottom: 70px;
- width: 100%;
- }
.badge {
background-color: $primary-color;
position: absolute;
diff --git a/web/static/help/Messaging_en.md b/web/static/help/Messaging_en.md
deleted file mode 100644
index 2063ad41c..000000000
--- a/web/static/help/Messaging_en.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# Messaging
-
-### Writing Messages
-
-You can write messages using the input box with the text "Write a message..." at the bottom of Mattermost.
-
-Press **ENTER** to send a message. Use **Shift+ENTER** to create a new line without sending a message.
-
-### Formatting Messages
-
-Mattermost messages are formatted using a standard called "markdown". Here are examples:
-
-| Text Entered | How it appears |
-|:---------------|:---------------|
-|`**bold**`| **bold** |
-| `_italic_`|_italic_|
-|`[hyperlink](http://mattermost.org)`|[hyperlink](http://mattermost.org)|
-|`![embedded image](https://travis-ci.org/mattermost/platform.svg)`|![embedded image](https://travis-ci.org/mattermost/platform.svg)|
-|`:smile:` `:sheep:` `:alien:`|:smile: :sheep: :alien:|
-
-Emojis provided free from [Emoji One](http://emojione.com/). Check out a full list of Emojis [here](http://emoji.codes/).
-
-
-### Mentioning Teammates
-
-You can mention a teammate by using the `@` symbol plus their username to send them a special notification to draw their attention.
-
-For example, you might write:
-
-```
-@alice how did your interview go with the new candidate?
-```
-
-Which sends a special mention notification to **alice** to check your message.
-
-To mention a teammate, press `@` and you should see a list of team members who can be messaged. You can either type their username or use the **Up** and **Down** arrow keys and then **ENTER** to select them to be mentioned.
-
-You can configure how you'd like to be alerted about mentions of your username, your first name, your nickname, or other keywords from **Account Settings** > **Notifications** and you can set channel-specific preferences from **[Channel Name]** > **Notification Preferences**
-
-### Messages Dropdown Menu
-
-To get to the Messages Dropdown Menu, hover over a message and click on the [...] menu. This shows a dropdown list containing additional actions you can perform on a message:
-
-- **Reply:** Opens up the sidebar so you can reply to a message in a comment thread.
-- **Permalink:** Creates a link to the message. Sharing this link with other users in the channel lets them view the linked message in the Message Archives.
-- **Delete:** Deletes the message so it is no longer visible. Team Administrators and System Administrators can also delete another user's message.
-- **Edit:** Lets you edit your own message.
diff --git a/web/static/help/Messaging_es.md b/web/static/help/Messaging_es.md
deleted file mode 100644
index d3947f36a..000000000
--- a/web/static/help/Messaging_es.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# Mensajes
-
-## Escribiendo Mensajes
-
-Puedes escribir mensajes utilizando el cuadro de texto que dice "Escribe un mensaje..." al final de Mattermost.
-
-Presiona **RETORNO** para enviar un mensaje. Utiliza **Shift+RETORNO** para crear una nueva linea sin enviar el mensaje.
-
-## Darle formato a los Mensajes
-
-Los mensajes de Mattermost se les asigna formato utilizando un estándard que se llama "markdown". Aquí algunos ejemplos:
-
-| Texto escrito | Como aparece |
-|:--------------|:-------------|
-|`**negrita**`| **negrita** |
-| `_italica_`|_italica_|
-|`[hipervinculo](http://mattermost.org)`|[hipervinculo](http://mattermost.org)|
-|`![imagen embebida](https://travis-ci.org/mattermost/platform.svg)`|![imagen embebida](https://travis-ci.org/mattermost/platform.svg)|
-|`:smile:` `:sheep:` `:alien:`|:smile: :sheep: :alien:|
-
-Revisa la lista completa de Emojis [aquí](http://www.emoji-cheat-sheet.com/).
-
-## Mencionando a compañeros
-
-Puedes mencionar a un compañero al utilizar el simbolo `@` más el nombre de usuario para enviarles una notificación especial que llame su atención.
-
-Por ejemplo, podrías escribir:
-
-```
-@alicia como te fue con la entrevista del nuevo candidato?
-```
-
-Lo cual enviará una notificación especial de mención a **alicia** para que lea tu mensaje.
-
-Para mencionar un compañero, presiona `@` y podrás ver una lista de los miembros de equipo a quienes puedes mandarles un mensaje. Puedes escribir su nombre de usuario o utilizar las flechas de **Arriba** y **Abajo** y presionar **RETORNO** para seleccionarlos.
-
-Puedes configurar como te gustaría ser notificado cuando alguien te menciona por nombre de usuario, tu primer nombre, sobrenombre o cualquier otra palabra clave en **Configurar Cuenta** > **Notificaciones** y puedes asignar preferencias especificas para un canal en **[Nombre del Canal]** > **Preferencias de Notificación**
diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json
index d2e340641..2a536925c 100644
--- a/web/static/i18n/en.json
+++ b/web/static/i18n/en.json
@@ -661,8 +661,10 @@
"email_signup.find": "Find my teams",
"email_verify.almost": "{siteName}: You are almost done",
"email_verify.notVerifiedBody": "Please verify your email address. Check your inbox for an email.",
+ "email_verify.verifyFailed": "Failed to verify your email.",
"email_verify.resend": "Resend Email",
"email_verify.sent": " Verification email sent.",
+ "email_verify.failed": " Failed to send verification email.",
"email_verify.verified": "{siteName} Email Verified",
"email_verify.verifiedBody": "<p>Your email has been verified! Click <a href={url}>here</a> to log in.</p>",
"error_bar.preview_mode": "Preview Mode: Email notifications have not been configured",
@@ -758,6 +760,7 @@
"login.or": "or",
"login.signTo": "Sign in to:",
"login.verified": " Email Verified",
+ "login.session_expired": " Your session has expired. Please login again.",
"login_email.badTeam": "Bad team name",
"login_email.email": "Email",
"login_email.emailReq": "An email is required",
@@ -822,16 +825,16 @@
"navbar_dropdown.teamSettings": "Team Settings",
"password_form.change": "Change my password",
"password_form.click": "Click <a href={url}>here</a> to log in.",
- "password_form.enter": "Enter a new password for your {teamDisplayName} {siteName} account.",
+ "password_form.enter": "Enter a new password for your {siteName} account.",
"password_form.error": "Please enter at least {chars} characters.",
"password_form.pwd": "Password",
"password_form.title": "Password Reset",
"password_form.update": "Your password has been updated successfully.",
"password_send.checkInbox": "Please check your inbox.",
- "password_send.description": "To reset your password, enter the email address you used to sign up for {teamName}.",
+ "password_send.description": "To reset your password, enter the email address you used to sign up.",
"password_send.email": "Email",
"password_send.error": "Please enter a valid email address.",
- "password_send.link": "<p>A password reset link has been sent to <b>{email}</b> for your <b>{teamDisplayName}</b> team on {hostname}.</p>",
+ "password_send.link": "<p>A password reset link has been sent to <b>{email}</b></p>",
"password_send.reset": "Reset my password",
"password_send.title": "Password Reset",
"post_attachment.collapse": "â–² collapse text",
@@ -1303,5 +1306,11 @@
"view_image.loading": "Loading ",
"view_image_popover.download": "Download",
"view_image_popover.file": "File {count} of {total}",
- "view_image_popover.publicLink": "Get Public Link"
+ "view_image_popover.publicLink": "Get Public Link",
+ "web.footer.about": "About",
+ "web.footer.help": "Help",
+ "web.footer.privacy": "Privacy",
+ "web.footer.terms": "Terms",
+ "web.header.back": "Back",
+ "web.root.singup_info": "All team communication in one place, searchable and accessible anywhere"
}
diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json
index c6b16a293..f42dc879a 100644
--- a/web/static/i18n/es.json
+++ b/web/static/i18n/es.json
@@ -822,16 +822,16 @@
"navbar_dropdown.teamSettings": "Configurar Equipo",
"password_form.change": "Cambiar mi contraseña",
"password_form.click": " Pincha <a href={url}>aquí</a> para iniciar sesión.",
- "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {teamDisplayName} {siteName}.",
+ "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {siteName}.",
"password_form.error": "Por favor ingresa al menos {chars} caracteres.",
"password_form.pwd": "Contraseña",
"password_form.title": "Restablecer Contraseña",
"password_form.update": "Tu contraseña ha sido actualizada satisfactoriamente.",
"password_send.checkInbox": "Por favor revisa tu bandeja de entrada.",
- "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte en {teamName}.",
+ "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte.",
"password_send.email": "Correo electrónico",
"password_send.error": "Por favor ingresa una dirección correo electrónico válida.",
- "password_send.link": "<p>Se ha enviado un enlace para restablecer la contraseña a <b>{email}</b> para tu equipo <b>{teamDisplayName}</b> en {hostname}.</p>",
+ "password_send.link": "<p>Se ha enviado un enlace para restablecer la contraseña a <b>{email}</b></p>",
"password_send.reset": "Restablecer mi contraseña",
"password_send.title": "Restablecer Contraseña",
"post_attachment.collapse": "â–² colapsar texto",
@@ -1303,5 +1303,11 @@
"view_image.loading": "Cargando ",
"view_image_popover.download": "Descargar",
"view_image_popover.file": "Archivo {count} de {total}",
- "view_image_popover.publicLink": "Obtener Enlace Público"
+ "view_image_popover.publicLink": "Obtener Enlace Público",
+ "web.footer.about": "Acerca",
+ "web.footer.help": "Ayuda",
+ "web.footer.privacy": "Privacidad",
+ "web.footer.terms": "Términos",
+ "web.header.back": "Atrás",
+ "web.root.singup_info": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte"
}
diff --git a/web/static/i18n/pt.json b/web/static/i18n/pt.json
index b9b8f4c07..d276e339a 100644
--- a/web/static/i18n/pt.json
+++ b/web/static/i18n/pt.json
@@ -820,16 +820,15 @@
"navbar_dropdown.teamSettings": "Configurações da Equipe",
"password_form.change": "Alterar minha senha",
"password_form.click": "Clique <a href={url}>aqui</a> para logar.",
- "password_form.enter": "Entre uma nova senha para sua conta {teamDisplayName} {siteName}.",
+ "password_form.enter": "Entre uma nova senha para sua conta {siteName}.",
"password_form.error": "Por favor, insira pelo menos {chars} caracteres.",
"password_form.pwd": "Senha",
"password_form.title": "Resetar Senha",
"password_form.update": "Sua senha foi atualizada com sucesso.",
"password_send.checkInbox": "Por favor verifique sua caixa de entrada.",
- "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever em {teamName}.",
+ "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever.",
"password_send.email": "E-mail",
"password_send.error": "Por favor entre um endereço de e-mail válido.",
- "password_send.link": "<p>Um link para resetar a sua senha na equipe <b>{teamDisplayName}</b> em {hostname} foi enviado para <b>{email}</b>.</p>",
"password_send.reset": "Resetar minha senha",
"password_send.title": "Resetar Senha",
"post_attachment.collapse": "â–² recolher texto",
diff --git a/web/templates/admin_console.html b/web/templates/admin_console.html
deleted file mode 100644
index 08c90493e..000000000
--- a/web/templates/admin_console.html
+++ /dev/null
@@ -1,21 +0,0 @@
-
-{{define "admin_console"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body>
-<script src="/static/js/Chart.min.js"></script>
-
-<div id='admin_controller'></div>
-
-<script>
- window.setup_admin_console_page({{ .Props }});
-
- $(document).ready(function(){
- $('[data-toggle="tooltip"]').tooltip();
- $('[data-toggle="popover"]').popover();
- });
-</script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/channel.html b/web/templates/channel.html
deleted file mode 100644
index 94d79a022..000000000
--- a/web/templates/channel.html
+++ /dev/null
@@ -1,21 +0,0 @@
-
-{{define "channel"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body>
- <div id="channel_view" class='channel-view'></div>
-<script>
- window.setup_channel_page({{ .Props }}, {{ .Team }}, {{ .Channel }});
- $('body').tooltip( {selector: '[data-toggle=tooltip]'} );
- var modals = $('.modal-body').not('.edit-modal-body');
- if($(window).height() > 1200){
- modals.css('max-height', 1000);
- } else {
- modals.css('max-height', $(window).height() - 200);
- }
- modals.perfectScrollbar();
-</script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/claim_account.html b/web/templates/claim_account.html
deleted file mode 100644
index 2a9126d1b..000000000
--- a/web/templates/claim_account.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{{define "claim_account"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="signup-team__container">
- <img class="signup-team-logo" src="/static/images/logo.png" />
- <div id="claim"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- <div>
- </div>
- <script>
- window.setup_claim_account_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/docs.html b/web/templates/docs.html
deleted file mode 100644
index dc18e5cb6..000000000
--- a/web/templates/docs.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{{define "docs"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
-<div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="docs__page" id="docs"></div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
-</div>
-<script>
- window.setup_documentation_page({{ .Props }});
-</script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/find_team.html b/web/templates/find_team.html
deleted file mode 100644
index b7e1d7eca..000000000
--- a/web/templates/find_team.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{{define "find_team"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="signup-team__container">
- <img class="signup-team-logo" src="/static/images/logo.png" />
- <div id="find-team"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
- window.setup_find_team_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/footer.html b/web/templates/footer.html
deleted file mode 100644
index 5b11328fb..000000000
--- a/web/templates/footer.html
+++ /dev/null
@@ -1,39 +0,0 @@
-{{define "footer"}}
-<div class="footer-pane col-xs-12">
- <div class="col-xs-12">
- <span class="pull-right footer-site-name">{{ .ClientCfg.SiteName }}</span>
- </div>
- <div class="col-xs-12">
- <span class="pull-right footer-link copyright">© 2015 Mattermost, Inc.</span>
- <a id="help_link" class="pull-right footer-link" href="#">{{ .ClientCfg.FooterHelp }}</a>
- <a id="terms_link" class="pull-right footer-link" href="#">{{ .ClientCfg.FooterTerms }}</a>
- <a id="privacy_link" class="pull-right footer-link" href="#">{{ .ClientCfg.FooterPrivacy }}</a>
- <a id="about_link" class="pull-right footer-link" href="#">{{ .ClientCfg.FooterAbout }}</a>
- </div>
-</div>
-<script>
- if (window.mm_config.HelpLink) {
- document.getElementById("help_link").setAttribute("href", window.mm_config.HelpLink);
- } else {
- $("#help_link").remove();
- }
-
- if (window.mm_config.TermsOfServiceLink) {
- document.getElementById("terms_link").setAttribute("href", window.mm_config.TermsOfServiceLink);
- } else {
- $("#terms_link").remove();
- }
-
- if (window.mm_config.PrivacyPolicyLink) {
- document.getElementById("privacy_link").setAttribute("href", window.mm_config.PrivacyPolicyLink);
- } else {
- $("#privacy_link").remove();
- }
-
- if (window.mm_config.AboutLink) {
- document.getElementById("about_link").setAttribute("href", window.mm_config.AboutLink);
- } else {
- $("#about_link").remove();
- }
-</script>
-{{end}}
diff --git a/web/templates/head.html b/web/templates/head.html
deleted file mode 100644
index 61b1aa12b..000000000
--- a/web/templates/head.html
+++ /dev/null
@@ -1,191 +0,0 @@
-{{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>
-
- <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.mm_config = {{ .ClientCfg }};
- window.mm_license = {{ .ClientLicense }};
- window.mm_team = {{ .Team }};
- window.mm_user = {{ .User }};
- window.mm_channel = {{ .Channel }};
- window.mm_locale = {{ .Locale }};
- window.mm_preferences = {{ .Preferences }};
-
- $(function() {
- if (window.mm_preferences != null) {
- PreferenceStore.setPreferences(window.mm_preferences);
- PreferenceStore.emitChange();
- }
- });
-
- if ({{.SessionTokenIndex}} >= 0) {
- window.mm_session_token_index = {{.SessionTokenIndex}};
- $.ajaxSetup({
- headers: {
- 'X-MM-TokenIndex': mm_session_token_index,
- 'Accept-Language': mm_locale
- }
- });
- } else {
- $.ajaxSetup({
- headers: {
- 'Accept-Language': mm_locale
- }
- });
- }
-
- $(function () {
- $(window).bind('storage', function (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');
- window.location.href = '/' + window.mm_team.name;
- }
-
- 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');
- location.reload();
- }
- });
- });
-
- $(window).on('beforeunload', function(){
- if (window.SocketStore) {
- SocketStore.close();
- }
- });
- </script>
-
- <script>
- window.onerror = function(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();
- }
- }
- </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>
-
- <script type="text/javascript">
- if (window.mm_config.SegmentDeveloperKey != null && window.mm_config.SegmentDeveloperKey !== "") {
- !function(){var analytics=window.analytics=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);
- if (window.mm_user) {
- analytics.identify(window.mm_user.id, {
- name: window.mm_user.nickname,
- email: window.mm_user.email,
- createdAt: window.mm_user.create_at,
- username: window.mm_user.username,
- team_id: window.mm_user.team_id,
- id: window.mm_user.id
- });
- }
- analytics.page();
- }}();
- } else {
- analytics = {};
- analytics.page = function(){};
- analytics.track = function(){};
- }
- </script>
-</head>
-{{end}}
diff --git a/web/templates/home.html b/web/templates/home.html
deleted file mode 100644
index 08876d41d..000000000
--- a/web/templates/home.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{{define "home"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body>
- <div class="container-fluid">
- <div class="sidebar--right" id="sidebar-right"></div>
- <div class="sidebar--left" id="sidebar-left"></div>
- <div class="inner__wrap">
- <div class="row header">
- <div id="navbar"></div>
- </div>
- <div class="row main">
- <div class="hidden-xs" id="sidebar"></div>
- <div class="app__content"></div>
- </div>
- </div>
- </div>
- <script>
- window.setup_home_page();
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/login.html b/web/templates/login.html
deleted file mode 100644
index 88540a906..000000000
--- a/web/templates/login.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{{define "login"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div id="login"></div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
- window.setup_login_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/password_reset.html b/web/templates/password_reset.html
deleted file mode 100644
index e68f8b693..000000000
--- a/web/templates/password_reset.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{{define "password_reset"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="signup-team__container">
- <img class="signup-team-logo" src="/static/images/logo.png" />
- <div id="reset"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
- window.setup_password_reset_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/signup_team.html b/web/templates/signup_team.html
deleted file mode 100644
index afba58066..000000000
--- a/web/templates/signup_team.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{{define "signup_team"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="col-sm-12">
- <div class="signup-team__container">
- <img class="signup-team-logo" src="/static/images/logo.png" />
- <h1>{{ .ClientCfg.SiteName }}</h1>
- <h4 class="color--light">{{.Props.Info}}</h4>
- <div id="signup-team"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
-window.setup_signup_team_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/signup_team_complete.html b/web/templates/signup_team_complete.html
deleted file mode 100644
index 3873d8978..000000000
--- a/web/templates/signup_team_complete.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{{define "signup_team_complete"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="signup-team__container">
- <div id="signup-team-complete"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
-window.setup_signup_team_complete_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/signup_team_confirm.html b/web/templates/signup_team_confirm.html
deleted file mode 100644
index 31f1ba95b..000000000
--- a/web/templates/signup_team_confirm.html
+++ /dev/null
@@ -1,26 +0,0 @@
-{{define "signup_team_confirm"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div id="signup-team-confirm"></div>
- </div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
-window.setup_signup_team_confirm_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/signup_user_complete.html b/web/templates/signup_user_complete.html
deleted file mode 100644
index 937a89dd2..000000000
--- a/web/templates/signup_user_complete.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{{define "signup_user_complete"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="signup-team__container padding--less">
- <div id="signup-user-complete"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
- window.setup_signup_user_complete_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/verify.html b/web/templates/verify.html
deleted file mode 100644
index 2e5496d7a..000000000
--- a/web/templates/verify.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{{define "verify"}}
-<!DOCTYPE html>
-<html>
- {{template "head" . }}
- <body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="signup-team__container">
- <img class="signup-team-logo" src="/static/images/logo.png" />
- <div id="verify"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
- window.setupVerifyPage({{ .Props }});
- </script>
- </body>
-</html>
-{{end}}
diff --git a/web/web.go b/web/web.go
index 09450b976..2a44ece00 100644
--- a/web/web.go
+++ b/web/web.go
@@ -4,67 +4,16 @@
package web
import (
- "fmt"
+ "net/http"
+ "strings"
+
l4g "github.com/alecthomas/log4go"
- "github.com/gorilla/mux"
"github.com/mattermost/platform/api"
"github.com/mattermost/platform/model"
- "github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
"github.com/mssola/user_agent"
- "gopkg.in/fsnotify.v1"
- "html/template"
- "net/http"
- "net/url"
- "strconv"
- "strings"
)
-var Templates *template.Template
-
-type HtmlTemplatePage api.Page
-
-func NewHtmlTemplatePage(templateName string, title string, locale string) *HtmlTemplatePage {
-
- if len(title) > 0 {
- title = utils.Cfg.TeamSettings.SiteName + " - " + title
- }
-
- props := make(map[string]string)
- props["Title"] = title
- return &HtmlTemplatePage{
- TemplateName: templateName,
- Props: props,
- ClientCfg: utils.ClientCfg,
- ClientLicense: utils.ClientLicense,
- Locale: locale,
- }
-}
-
-func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) {
- if me.Team != nil {
- me.Team.Sanitize()
- }
-
- if me.User != nil {
- me.User.Sanitize(map[string]bool{})
- me.Locale = me.User.Locale
- }
-
- me.Props["Locale"] = me.Locale
- me.SessionTokenIndex = c.SessionTokenIndex
-
- me.ClientCfg["HeaderBack"] = c.T("web.header.back")
- me.ClientCfg["FooterHelp"] = c.T("web.footer.help")
- me.ClientCfg["FooterTerms"] = c.T("web.footer.terms")
- me.ClientCfg["FooterPrivacy"] = c.T("web.footer.privacy")
- me.ClientCfg["FooterAbout"] = c.T("web.footer.about")
-
- if err := Templates.ExecuteTemplate(w, me.TemplateName, me); err != nil {
- c.SetUnknownError(me.TemplateName, err.Error())
- }
-}
-
func InitWeb() {
l4g.Debug(utils.T("web.init.debug"))
@@ -74,81 +23,7 @@ func InitWeb() {
l4g.Debug("Using static directory at %v", staticDir)
mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))))
- mainrouter.Handle("/", api.AppHandlerIndependent(root)).Methods("GET")
- mainrouter.Handle("/oauth/authorize", api.UserRequired(authorizeOAuth)).Methods("GET")
- mainrouter.Handle("/oauth/access_token", api.ApiAppHandler(getAccessToken)).Methods("POST")
-
- mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET")
- mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET")
- mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET")
- mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET")
- mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET")
- mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET")
- mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8)
- mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8)
- mainrouter.Handle("/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET")
-
- mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET")
- mainrouter.Handle("/admin_console/", api.UserRequired(adminConsole)).Methods("GET")
- mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}", api.UserRequired(adminConsole)).Methods("GET")
- mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}/{team:[A-Za-z0-9-]*}", api.UserRequired(adminConsole)).Methods("GET")
-
- mainrouter.Handle("/hooks/{id:[A-Za-z0-9]+}", api.ApiAppHandler(incomingWebhook)).Methods("POST")
-
- mainrouter.Handle("/docs/{doc:[A-Za-z0-9]+}", api.AppHandlerIndependent(docs)).Methods("GET")
-
- // ----------------------------------------------------------------------------------------------
- // *ANYTHING* team specific should go below this line
- // ----------------------------------------------------------------------------------------------
-
- mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET")
- mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET")
- mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET")
- mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET")
- mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET")
- mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/claim", api.AppHandler(claimAccount)).Methods("GET")
- mainrouter.Handle("/{team}/pl/{postid}", api.AppHandler(postPermalink)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
- mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
- mainrouter.Handle("/{team}/channels/{channelname}", api.AppHandler(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
- mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
-
- watchAndParseTemplates()
-}
-
-func watchAndParseTemplates() {
-
- templatesDir := utils.FindDir("web/templates")
- l4g.Debug(utils.T("web.parsing_templates.debug"), templatesDir)
- var err error
- if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil {
- l4g.Error(utils.T("web.parsing_templates.error"), err)
- }
-
- watcher, err := fsnotify.NewWatcher()
- if err != nil {
- l4g.Error(utils.T("web.create_dir.error"), err)
- }
-
- go func() {
- for {
- select {
- case event := <-watcher.Events:
- if event.Op&fsnotify.Write == fsnotify.Write {
- l4g.Info(utils.T("web.reparse_templates.info"), event.Name)
- if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil {
- l4g.Error(utils.T("web.parsing_templates.error"), err)
- }
- }
- case err := <-watcher.Errors:
- l4g.Error(utils.T("web.dir_fail.error"), err)
- }
- }
- }()
-
- err = watcher.Add(templatesDir)
- if err != nil {
- l4g.Error(utils.T("web.watcher_fail.error"), err)
- }
+ mainrouter.Handle("/{anything:.*}", api.AppHandlerIndependent(root)).Methods("GET")
}
var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7;Safari/8"
@@ -177,1026 +52,9 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) {
return
}
- if len(c.Session.UserId) == 0 {
- page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale)
- page.Props["Info"] = c.T("web.root.singup_info")
-
- if result := <-api.Srv.Store.Team().GetAllTeamListing(); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- teams := result.Data.([]*model.Team)
- for _, team := range teams {
- page.Props[team.Name] = team.DisplayName
- }
-
- if len(teams) == 1 && *utils.Cfg.TeamSettings.EnableTeamListing && !utils.Cfg.TeamSettings.EnableTeamCreation {
- http.Redirect(w, r, c.GetSiteURL()+"/"+teams[0].Name, http.StatusTemporaryRedirect)
- return
- }
- }
-
- page.Render(c, w)
- } else {
- teamChan := api.Srv.Store.Team().Get(c.Session.TeamId)
- userChan := api.Srv.Store.User().Get(c.Session.UserId)
-
- var team *model.Team
- if tr := <-teamChan; tr.Err != nil {
- c.Err = tr.Err
- return
- } else {
- team = tr.Data.(*model.Team)
-
- }
-
- var user *model.User
- if ur := <-userChan; ur.Err != nil {
- c.Err = ur.Err
- return
- } else {
- user = ur.Data.(*model.User)
- }
-
- page := NewHtmlTemplatePage("home", c.T("web.root.home_title"), c.Locale)
- page.Team = team
- page.User = user
- page.Render(c, w)
- }
-}
-
-func signup(c *api.Context, w http.ResponseWriter, r *http.Request) {
-
- if !CheckBrowserCompatability(c, r) {
- return
- }
-
- page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale)
- page.Render(c, w)
-}
-
-func login(c *api.Context, w http.ResponseWriter, r *http.Request) {
- if !CheckBrowserCompatability(c, r) {
- return
- }
- params := mux.Vars(r)
- teamName := params["team"]
-
- var team *model.Team
- if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil {
- l4g.Error(utils.T("web.login.error"), teamName, tResult.Err.Message)
- http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
- return
- } else {
- team = tResult.Data.(*model.Team)
- }
-
- // We still might be able to switch to this team because we've logged in before
- _, session := api.FindMultiSessionForTeamId(r, team.Id)
- if session != nil {
- w.Header().Set(model.HEADER_TOKEN, session.Token)
- lastViewChannelName := "town-square"
- if lastViewResult := <-api.Srv.Store.Preference().Get(session.UserId, model.PREFERENCE_CATEGORY_LAST, model.PREFERENCE_NAME_LAST_CHANNEL); lastViewResult.Err == nil {
- if lastViewChannelResult := <-api.Srv.Store.Channel().Get(lastViewResult.Data.(model.Preference).Value); lastViewChannelResult.Err == nil {
- lastViewChannelName = lastViewChannelResult.Data.(*model.Channel).Name
- }
- }
-
- http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/"+lastViewChannelName, http.StatusTemporaryRedirect)
- return
- }
-
- page := NewHtmlTemplatePage("login", c.T("web.login.login_title"), c.Locale)
- page.Props["TeamDisplayName"] = team.DisplayName
- page.Props["TeamName"] = team.Name
-
- if team.AllowOpenInvite {
- page.Props["InviteId"] = team.InviteId
- }
-
- page.Render(c, w)
-}
-
-func signupTeamConfirm(c *api.Context, w http.ResponseWriter, r *http.Request) {
- email := r.FormValue("email")
-
- page := NewHtmlTemplatePage("signup_team_confirm", c.T("web.signup_team_confirm.title"), c.Locale)
- page.Props["Email"] = email
- page.Render(c, w)
-}
-
-func signupTeamComplete(c *api.Context, w http.ResponseWriter, r *http.Request) {
- data := r.FormValue("d")
- hash := r.FormValue("h")
-
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
- c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.invalid_link.app_error", nil, "")
- return
- }
-
- props := model.MapFromJson(strings.NewReader(data))
-
- t, err := strconv.ParseInt(props["time"], 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60*24*30 { // 30 days
- c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.link_expired.app_error", nil, "")
- return
- }
-
- page := NewHtmlTemplatePage("signup_team_complete", c.T("web.signup_team_complete.title"), c.Locale)
- page.Props["Email"] = props["email"]
- page.Props["Data"] = data
- page.Props["Hash"] = hash
- page.Render(c, w)
-}
-
-func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) {
-
- id := r.FormValue("id")
- data := r.FormValue("d")
- hash := r.FormValue("h")
- var props map[string]string
-
- if len(id) > 0 {
- props = make(map[string]string)
-
- if result := <-api.Srv.Store.Team().GetByInviteId(id); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team := result.Data.(*model.Team)
- if !(team.Type == model.TEAM_OPEN || (team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0)) {
- c.Err = model.NewLocAppError("signupUserComplete", "web.signup_user_complete.no_invites.app_error", nil, "id="+id)
- return
- }
-
- props["email"] = ""
- props["display_name"] = team.DisplayName
- props["name"] = team.Name
- props["id"] = team.Id
- data = model.MapToJson(props)
- hash = ""
- }
- } else {
-
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
- c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_invalid.app_error", nil, "")
- return
- }
-
- props = model.MapFromJson(strings.NewReader(data))
-
- t, err := strconv.ParseInt(props["time"], 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hour
- c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_expired.app_error", nil, "")
- return
- }
- }
-
- page := NewHtmlTemplatePage("signup_user_complete", c.T("web.signup_user_complete.title"), c.Locale)
- page.Props["Email"] = props["email"]
- page.Props["TeamDisplayName"] = props["display_name"]
- page.Props["TeamName"] = props["name"]
- page.Props["TeamId"] = props["id"]
- page.Props["Data"] = data
- page.Props["Hash"] = hash
- page.Render(c, w)
-}
-
-func logout(c *api.Context, w http.ResponseWriter, r *http.Request) {
- api.Logout(c, w, r)
- http.Redirect(w, r, c.GetTeamURL(), http.StatusTemporaryRedirect)
-}
-
-func postPermalink(c *api.Context, w http.ResponseWriter, r *http.Request) {
- params := mux.Vars(r)
- teamName := params["team"]
- postId := params["postid"]
-
- if len(postId) != 26 {
- c.Err = model.NewLocAppError("postPermalink", "web.post_permalink.app_error", nil, "id="+postId)
- return
- }
-
- team := checkSessionSwitch(c, w, r, teamName)
- if team == nil {
- // Error already set by getTeam
- return
- }
-
- var post *model.Post
- if result := <-api.Srv.Store.Post().Get(postId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- postlist := result.Data.(*model.PostList)
- post = postlist.Posts[postlist.Order[0]]
- }
-
- var channel *model.Channel
- if result := <-api.Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- if result.Data.(int64) == 0 {
- if channel = autoJoinChannelId(c, w, r, post.ChannelId); channel == nil {
- http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
- return
- }
- } else {
- if result := <-api.Srv.Store.Channel().Get(post.ChannelId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- channel = result.Data.(*model.Channel)
- }
- }
- }
-
- doLoadChannel(c, w, r, team, channel, post.Id)
-}
-
-func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
- params := mux.Vars(r)
- name := params["channelname"]
- teamName := params["team"]
-
- team := checkSessionSwitch(c, w, r, teamName)
- if team == nil {
- // Error already set by getTeam
- return
- }
-
- var channel *model.Channel
- if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- channelId := result.Data.(string)
- if len(channelId) == 0 {
- if channel = autoJoinChannelName(c, w, r, name); channel == nil {
- http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
- return
- }
- } else {
- if result := <-api.Srv.Store.Channel().Get(channelId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- channel = result.Data.(*model.Channel)
- }
- }
- }
-
- doLoadChannel(c, w, r, team, channel, "")
-}
-
-func autoJoinChannelName(c *api.Context, w http.ResponseWriter, r *http.Request, channelName string) *model.Channel {
- if strings.Index(channelName, "__") > 0 {
- // It's a direct message channel that doesn't exist yet so let's create it
- ids := strings.Split(channelName, "__")
- otherUserId := ""
- if ids[0] == c.Session.UserId {
- otherUserId = ids[1]
- } else {
- otherUserId = ids[0]
- }
-
- if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil {
- api.Handle404(w, r)
- return nil
- } else {
- return sc
- }
- } else {
- // We will attempt to auto-join open channels
- return joinOpenChannel(c, w, r, api.Srv.Store.Channel().GetByName(c.Session.TeamId, channelName))
- }
-
- return nil
-}
-
-func autoJoinChannelId(c *api.Context, w http.ResponseWriter, r *http.Request, channelId string) *model.Channel {
- return joinOpenChannel(c, w, r, api.Srv.Store.Channel().Get(channelId))
-}
-
-func joinOpenChannel(c *api.Context, w http.ResponseWriter, r *http.Request, channel store.StoreChannel) *model.Channel {
- if cr := <-channel; cr.Err != nil {
- http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
- return nil
- } else {
- channel := cr.Data.(*model.Channel)
- if channel.Type == model.CHANNEL_OPEN {
- api.JoinChannel(c, channel.Id, "")
- if c.Err != nil {
- return nil
- }
- } else {
- http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
- return nil
- }
- return channel
- }
-}
-
-func checkSessionSwitch(c *api.Context, w http.ResponseWriter, r *http.Request, teamName string) *model.Team {
- var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
- c.Err = result.Err
- return nil
- } else {
- team = result.Data.(*model.Team)
- }
-
- // We are logged into a different team. Lets see if we have another
- // session in the cookie that will give us access.
- if c.Session.TeamId != team.Id {
- index, session := api.FindMultiSessionForTeamId(r, team.Id)
- if session == nil {
- // redirect to login
- http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/?redirect="+url.QueryEscape(r.URL.Path), http.StatusTemporaryRedirect)
- } else {
- c.Session = *session
- c.SessionTokenIndex = index
- }
- }
-
- return team
-}
-
-func doLoadChannel(c *api.Context, w http.ResponseWriter, r *http.Request, team *model.Team, channel *model.Channel, postid string) {
- userChan := api.Srv.Store.User().Get(c.Session.UserId)
- prefChan := api.Srv.Store.Preference().GetAll(c.Session.UserId)
-
- var user *model.User
- if ur := <-userChan; ur.Err != nil {
- c.Err = ur.Err
- c.RemoveSessionCookie(w, r)
- l4g.Error(utils.T("web.do_load_channel.error"), c.Session.UserId)
- return
- } else {
- user = ur.Data.(*model.User)
- }
-
- var preferences model.Preferences
- if result := <-prefChan; result.Err != nil {
- l4g.Error("Error in getting preferences for id=%v", c.Session.UserId)
- } else {
- preferences = result.Data.(model.Preferences)
- }
-
- page := NewHtmlTemplatePage("channel", "", c.Locale)
- page.Props["Title"] = channel.DisplayName + " - " + team.DisplayName + " " + page.ClientCfg["SiteName"]
- page.Props["TeamDisplayName"] = team.DisplayName
- page.Props["ChannelName"] = channel.Name
- page.Props["ChannelId"] = channel.Id
- page.Props["PostId"] = postid
- page.Team = team
- page.User = user
- page.Channel = channel
- page.Preferences = &preferences
- page.Render(c, w)
-}
-
-func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) {
- resend := r.URL.Query().Get("resend")
- resendSuccess := r.URL.Query().Get("resend_success")
- name := r.URL.Query().Get("teamname")
- email := r.URL.Query().Get("email")
- hashedId := r.URL.Query().Get("hid")
- userId := r.URL.Query().Get("uid")
-
- var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(name); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team = result.Data.(*model.Team)
- }
-
- if resend == "true" {
- if result := <-api.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 {
- api.SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
- } else {
- api.SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
- }
-
- newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1)
- http.Redirect(w, r, newAddress, http.StatusFound)
- return
- }
- }
-
- if len(userId) == 26 && len(hashedId) != 0 && model.ComparePassword(hashedId, userId) {
- if c.Err = (<-api.Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil {
- return
- } else {
- c.LogAudit("Email Verified")
- http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?extra=verified&email="+url.QueryEscape(email), http.StatusTemporaryRedirect)
- return
- }
- }
-
- page := NewHtmlTemplatePage("verify", c.T("web.email_verified.title"), c.Locale)
- page.Props["TeamURL"] = c.GetTeamURLFromTeam(team)
- page.Props["UserEmail"] = email
- page.Props["ResendSuccess"] = resendSuccess
- page.Render(c, w)
-}
-
-func findTeam(c *api.Context, w http.ResponseWriter, r *http.Request) {
- page := NewHtmlTemplatePage("find_team", c.T("web.find_team.title"), c.Locale)
- page.Render(c, w)
-}
-
-func docs(c *api.Context, w http.ResponseWriter, r *http.Request) {
- params := mux.Vars(r)
- doc := params["doc"]
-
- var user *model.User
- if len(c.Session.UserId) != 0 {
- userChan := api.Srv.Store.User().Get(c.Session.UserId)
- if userChan := <-userChan; userChan.Err == nil {
- user = userChan.Data.(*model.User)
- }
- }
-
- page := NewHtmlTemplatePage("docs", c.T("web.doc.title"), c.Locale)
- page.Props["Site"] = doc
- page.User = user
- page.Render(c, w)
-}
-
-func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) {
- isResetLink := true
- hash := r.URL.Query().Get("h")
- data := r.URL.Query().Get("d")
- params := mux.Vars(r)
- teamName := params["team"]
-
- if len(hash) == 0 || len(data) == 0 {
- isResetLink = false
- } else {
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) {
- c.Err = model.NewLocAppError("resetPassword", "web.reset_password.invalid_link.app_error", nil, "")
- return
- }
-
- props := model.MapFromJson(strings.NewReader(data))
-
- t, err := strconv.ParseInt(props["time"], 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour
- c.Err = model.NewLocAppError("resetPassword", "web.reset_password.expired_link.app_error", nil, "")
- return
- }
- }
-
- teamDisplayName := "Developer/Beta"
- var team *model.Team
- if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil {
- c.Err = tResult.Err
- return
- } else {
- team = tResult.Data.(*model.Team)
- }
-
- if team != nil {
- teamDisplayName = team.DisplayName
- }
-
- page := NewHtmlTemplatePage("password_reset", "", c.Locale)
- page.Props["Title"] = "Reset Password " + page.ClientCfg["SiteName"]
- page.Props["TeamDisplayName"] = teamDisplayName
- page.Props["TeamName"] = teamName
- page.Props["Hash"] = hash
- page.Props["Data"] = data
- page.Props["TeamName"] = teamName
- page.Props["IsReset"] = strconv.FormatBool(isResetLink)
- page.Render(c, w)
-}
-
-func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
- params := mux.Vars(r)
- service := params["service"]
- teamName := params["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 := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team = result.Data.(*model.Team)
- }
-
- if api.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 := api.GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil {
- c.Err = err
- return
- } else {
- http.Redirect(w, r, authUrl, http.StatusFound)
- }
-}
-
-func completeOAuth(c *api.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() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8)
-
- if body, team, props, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil {
- c.Err = err
- return
- } else {
- action := props["action"]
- switch action {
- case model.OAUTH_ACTION_SIGNUP:
- api.CreateOAuthUser(c, w, r, service, body, team)
- if c.Err == nil {
- root(c, w, r)
- }
- break
- case model.OAUTH_ACTION_LOGIN:
- api.LoginByOAuth(c, w, r, service, body, team)
- if c.Err == nil {
- root(c, w, r)
- }
- break
- case model.OAUTH_ACTION_EMAIL_TO_SSO:
- api.CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"])
- if c.Err == nil {
- http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect)
- }
- break
- case model.OAUTH_ACTION_SSO_TO_EMAIL:
- api.LoginByOAuth(c, w, r, service, body, team)
- if c.Err == nil {
- http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect)
- }
- break
- default:
- api.LoginByOAuth(c, w, r, service, body, team)
- if c.Err == nil {
- root(c, w, r)
- }
- break
- }
- }
-}
-
-func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
- params := mux.Vars(r)
- service := params["service"]
- teamName := params["team"]
- loginHint := r.URL.Query().Get("login_hint")
-
- 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 := <-api.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 := api.GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil {
- c.Err = err
- return
- } else {
- http.Redirect(w, r, authUrl, http.StatusFound)
- }
-}
-
-func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) {
-
- if !c.HasSystemAdminPermissions("adminConsole") {
- return
- }
-
- teamChan := api.Srv.Store.Team().Get(c.Session.TeamId)
- userChan := api.Srv.Store.User().Get(c.Session.UserId)
-
- var team *model.Team
- if tr := <-teamChan; tr.Err != nil {
- c.Err = tr.Err
- return
- } else {
- team = tr.Data.(*model.Team)
-
- }
-
- var user *model.User
- if ur := <-userChan; ur.Err != nil {
- c.Err = ur.Err
- return
- } else {
- user = ur.Data.(*model.User)
- }
-
- params := mux.Vars(r)
- activeTab := params["tab"]
- teamId := params["team"]
-
- page := NewHtmlTemplatePage("admin_console", c.T("web.admin_console.title"), c.Locale)
- page.User = user
- page.Team = team
- page.Props["ActiveTab"] = activeTab
- page.Props["TeamId"] = teamId
- page.Render(c, w)
-}
-
-func authorizeOAuth(c *api.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
- }
-
- if !CheckBrowserCompatability(c, r) {
- 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 := <-api.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 := <-api.Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team = result.Data.(*model.Team)
+ page := utils.NewHTMLTemplate("root", c.Locale)
+ page.Props["Title"] = c.T("web.root.home_title")
+ if err := page.RenderToWriter(w); err != nil {
+ c.SetUnknownError(page.TemplateName, err.Error())
}
-
- page := NewHtmlTemplatePage("authorize", c.T("web.authorize_oauth.title"), c.Locale)
- 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
- page.Render(c, w)
-}
-
-func getAccessToken(c *api.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 := api.Srv.Store.OAuth().GetApp(clientId)
- tchan := api.Srv.Store.OAuth().GetAccessDataByAuthCode(code)
-
- authData := api.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 := api.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 := api.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 := <-api.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)
- api.AddSessionToCache(session)
- }
-
- accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback}
-
- if result := <-api.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 incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
- if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.disabled.app_error", nil, "")
- c.Err.StatusCode = http.StatusNotImplemented
- return
- }
-
- params := mux.Vars(r)
- id := params["id"]
-
- hchan := api.Srv.Store.Webhook().GetIncoming(id)
-
- r.ParseForm()
-
- var parsedRequest *model.IncomingWebhookRequest
- contentType := r.Header.Get("Content-Type")
- if strings.Split(contentType, "; ")[0] == "application/json" {
- parsedRequest = model.IncomingWebhookRequestFromJson(r.Body)
- } else {
- parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload")))
- }
-
- if parsedRequest == nil {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.parse.app_error", nil, "")
- return
- }
-
- text := parsedRequest.Text
- if len(text) == 0 && parsedRequest.Attachments == nil {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.text.app_error", nil, "")
- return
- }
-
- channelName := parsedRequest.ChannelName
- webhookType := parsedRequest.Type
-
- //attachments is in here for slack compatibility
- if parsedRequest.Attachments != nil {
- if len(parsedRequest.Props) == 0 {
- parsedRequest.Props = make(model.StringInterface)
- }
- parsedRequest.Props["attachments"] = parsedRequest.Attachments
- webhookType = model.POST_SLACK_ATTACHMENT
- }
-
- var hook *model.IncomingWebhook
- if result := <-hchan; result.Err != nil {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.invalid.app_error", nil, "err="+result.Err.Message)
- return
- } else {
- hook = result.Data.(*model.IncomingWebhook)
- }
-
- var channel *model.Channel
- var cchan store.StoreChannel
-
- if len(channelName) != 0 {
- if channelName[0] == '@' {
- if result := <-api.Srv.Store.User().GetByUsername(hook.TeamId, channelName[1:]); result.Err != nil {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.user.app_error", nil, "err="+result.Err.Message)
- return
- } else {
- channelName = model.GetDMNameFromIds(result.Data.(*model.User).Id, hook.UserId)
- }
- } else if channelName[0] == '#' {
- channelName = channelName[1:]
- }
-
- cchan = api.Srv.Store.Channel().GetByName(hook.TeamId, channelName)
- } else {
- cchan = api.Srv.Store.Channel().Get(hook.ChannelId)
- }
-
- overrideUsername := parsedRequest.Username
- overrideIconUrl := parsedRequest.IconURL
-
- if result := <-cchan; result.Err != nil {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message)
- return
- } else {
- channel = result.Data.(*model.Channel)
- }
-
- pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId)
-
- // create a mock session
- c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false}
-
- if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "")
- return
- }
-
- if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil {
- c.Err = err
- return
- }
-
- w.Header().Set("Content-Type", "text/plain")
- w.Write([]byte("ok"))
-}
-
-func claimAccount(c *api.Context, w http.ResponseWriter, r *http.Request) {
- if !CheckBrowserCompatability(c, r) {
- return
- }
-
- params := mux.Vars(r)
- teamName := params["team"]
- email := r.URL.Query().Get("email")
- newType := r.URL.Query().Get("new_type")
-
- var team *model.Team
- if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil {
- l4g.Error(utils.T("web.claim_account.team.error"), teamName, tResult.Err.Message)
- http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
- return
- } else {
- team = tResult.Data.(*model.Team)
- }
-
- authType := ""
- if len(email) != 0 {
- if uResult := <-api.Srv.Store.User().GetByEmail(team.Id, email); uResult.Err != nil {
- l4g.Error(utils.T("web.claim_account.user.error"), team.Id, email, uResult.Err.Message)
- http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
- return
- } else {
- user := uResult.Data.(*model.User)
- authType = user.AuthService
-
- // if user is not logged in to their SSO account, ask them to log in
- if len(authType) != 0 && user.Id != c.Session.UserId {
- stateProps := map[string]string{}
- stateProps["action"] = model.OAUTH_ACTION_SSO_TO_EMAIL
- stateProps["email"] = email
-
- if authUrl, err := api.GetAuthorizationCode(c, authType, team.Name, stateProps, ""); err != nil {
- c.Err = err
- return
- } else {
- http.Redirect(w, r, authUrl, http.StatusFound)
- }
- }
- }
- }
-
- page := NewHtmlTemplatePage("claim_account", c.T("web.claim_account.title"), c.Locale)
- page.Props["Email"] = email
- page.Props["CurrentType"] = authType
- page.Props["NewType"] = newType
- page.Props["TeamDisplayName"] = team.DisplayName
- page.Props["TeamName"] = team.Name
-
- page.Render(c, w)
}