diff options
37 files changed, 380 insertions, 134 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f1f491e..7a4335eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,50 @@ # Mattermost Changelog -## UNDER DEVELOPMENT - Release v1.0.0-RC2 +## Release v1.0.0 -The "UNDER DEVELOPMENT" section of the Mattermost changelog appears in the product's `master` branch to note key changes committed to master and are on their way to the next stable release. When a stable release is pushed the "UNDER DEVELOPMENT" heading is removed from the final changelog of the release. - -- **Final release anticipated:** October 2, 2015 +Released 2015-10-02 ### Release Highlights -- System Console - UI for configuring deployments, managing teams, resetting user passwords and other admin features -- Markdown - Markdown support in messages, comments and channel descriptions - Including font formatting, emoticons, headings and tables -- Themes - Preset themes and detailed theme color options, plus ability to import themes from Slack -- Performance - Numerous performance improvements and optimizations +#### Markdown + +Markdown support is now available across messages, comments and channel descriptions for: + +- **Headings** - in five different sizes to help organize your thoughts +- **Lists** - both numbered and bullets +- **Font formatting** - including **bold**, _italics_, ~~strikethrough~~, `code`, links, and block quotes) +- **In-line images** - useful for creating buttons and status messages +- **Tables** - for keeping things organized +- **Emoticons** - translation of emoji codes to images like :sheep: :boom: :rage1: :+1: + +See [documentation](doc/help/enduser/markdown.md) for full details. + +#### Themes + +Themes as been significantly upgraded in this release with: + +- 4 pre-set themes, two light and two dark, to customize your experience +- 18 detailed color setting options to precisely match the colors of your other tools or preferences +- Ability to import themes from Slack + +#### System console and command line tools + +Added new web-based System Console for managing instance level configuration. This lets IT admins conveniently: + +- _access core settings_, like server, database, email, rate limiting, file store, SSO, and log settings, +- _monitor operations_, by quickly accessing log files and user roles, and +- _manage teams_, with essential functions such as team role assignment and password reset + +In addition new command line tools are available for managing Mattermost system roles, creating users, resetting passwords, getting version info and other basic tasks. + ### New Features Messaging, Comments and Notifications -- Support for emoji codes rendering to image files - Full markdown support in messages, comments, and channel description +- Support for emoji codes rendering to image files + Files and Images @@ -56,12 +82,13 @@ Documentation Performance - Enabled Javascript optimizations +- Numerous improvements in center channel and mobile web Code Quality - Reformatted Javascript per Mattermost Style Guide -UI +User Interface - Added version, build number, build date and build hash under Account Settings -> Security @@ -71,7 +98,6 @@ Licensing ### Bug Fixes -- Numerous performance improvements - Fixed issue so that SSO option automatically set EmailVerified=true (it was false previously) ### Contributors @@ -140,11 +140,11 @@ check: install test: install @mkdir -p logs - @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s ./api || exit 1 - @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=12s ./model || exit 1 - @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./store || exit 1 - @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./utils || exit 1 - @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./web || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./api || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s ./model || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./store || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./utils || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./web || exit 1 benchmark: install @mkdir -p logs diff --git a/api/channel.go b/api/channel.go index 6494e3528..5e13fa18a 100644 --- a/api/channel.go +++ b/api/channel.go @@ -281,7 +281,7 @@ func getChannels(c *Context, w http.ResponseWriter, r *http.Request) { // lets make sure the user is valid if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { c.Err = result.Err - c.RemoveSessionCookie(w) + c.RemoveSessionCookie(w, r) l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId) return } diff --git a/api/context.go b/api/context.go index 02c3dc902..e80582b2a 100644 --- a/api/context.go +++ b/api/context.go @@ -137,7 +137,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } if session == nil || session.IsExpired() { - c.RemoveSessionCookie(w) + c.RemoveSessionCookie(w, r) c.Err = model.NewAppError("ServeHTTP", "Invalid or expired session, please login again.", "token="+token) c.Err.StatusCode = http.StatusUnauthorized } else if !session.IsOAuth && isTokenFromQueryString { @@ -303,7 +303,6 @@ func (c *Context) HasSystemAdminPermissions(where string) bool { } func (c *Context) IsSystemAdmin() bool { - // TODO XXX FIXME && IsPrivateIpAddress(c.IpAddress) if model.IsInRole(c.Session.Roles, model.ROLE_SYSTEM_ADMIN) { return true } @@ -317,7 +316,7 @@ func (c *Context) IsTeamAdmin() bool { return false } -func (c *Context) RemoveSessionCookie(w http.ResponseWriter) { +func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) { sessionCache.Remove(c.Session.Token) @@ -330,6 +329,21 @@ func (c *Context) RemoveSessionCookie(w http.ResponseWriter) { } http.SetCookie(w, cookie) + + multiToken := "" + if oldMultiCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil { + multiToken = oldMultiCookie.Value + } + + multiCookie := &http.Cookie{ + Name: model.MULTI_SESSION_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) } func (c *Context) SetInvalidParam(where string, name string) { @@ -346,7 +360,7 @@ func (c *Context) setTeamURL(url string, valid bool) { c.teamURLValid = valid } -func (c *Context) setTeamURLFromSession() { +func (c *Context) SetTeamURLFromSession() { if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err == nil { c.setTeamURL(c.GetSiteURL()+"/"+result.Data.(*model.Team).Name, true) } @@ -362,7 +376,7 @@ func (c *Context) GetTeamURLFromTeam(team *model.Team) string { func (c *Context) GetTeamURL() string { if !c.teamURLValid { - c.setTeamURLFromSession() + c.SetTeamURLFromSession() if !c.teamURLValid { l4g.Debug("TeamURL accessed when not valid. Team URL should not be used in api functions or those that are team independent") } diff --git a/api/user.go b/api/user.go index ed3576a30..2d7dd9ab1 100644 --- a/api/user.go +++ b/api/user.go @@ -394,6 +394,41 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, http.SetCookie(w, sessionCookie) + multiToken := "" + if originalMultiSessionCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil { + multiToken = originalMultiSessionCookie.Value + } + + // Attempt to clean all the old tokens or duplicate tokens + if len(multiToken) > 0 { + tokens := strings.Split(multiToken, " ") + + multiToken = "" + seen := make(map[string]string) + seen[session.TeamId] = session.TeamId + for _, token := range tokens { + if sr := <-Srv.Store.Session().Get(token); sr.Err == nil { + s := sr.Data.(*model.Session) + if !s.IsExpired() && seen[s.TeamId] == "" { + multiToken += " " + token + seen[s.TeamId] = s.TeamId + } + } + } + } + + multiToken = strings.TrimSpace(session.Token + " " + multiToken) + + multiSessionCookie := &http.Cookie{ + Name: model.MULTI_SESSION_TOKEN, + Value: multiToken, + Path: "/", + MaxAge: maxAge, + HttpOnly: true, + } + + http.SetCookie(w, multiSessionCookie) + c.Session = *session c.LogAuditWithUserId(user.Id, "success") } @@ -514,7 +549,7 @@ func logout(c *Context, w http.ResponseWriter, r *http.Request) { func Logout(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("") - c.RemoveSessionCookie(w) + c.RemoveSessionCookie(w, r) if result := <-Srv.Store.Session().Remove(c.Session.Id); result.Err != nil { c.Err = result.Err return @@ -529,7 +564,7 @@ func getMe(c *Context, w http.ResponseWriter, r *http.Request) { if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { c.Err = result.Err - c.RemoveSessionCookie(w) + c.RemoveSessionCookie(w, r) l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId) return } else if HandleEtag(result.Data.(*model.User).Etag(), w, r) { diff --git a/config/config.json b/config/config.json index 38acee85a..48514e1a4 100644 --- a/config/config.json +++ b/config/config.json @@ -75,7 +75,8 @@ }, "PrivacySettings": { "ShowEmailAddress": true, - "ShowFullName": true + "ShowFullName": true, + "EnableDiagnostic": false }, "GitLabSettings": { "Enable": false, diff --git a/doc/install/single-container-install.md b/doc/install/single-container-install.md index fa5265773..304467678 100644 --- a/doc/install/single-container-install.md +++ b/doc/install/single-container-install.md @@ -5,7 +5,7 @@ The following install instructions are for single-container installs of Mattermo ### Mac OSX ### 1. Install Docker Toolbox using instructions at: http://docs.docker.com/installation/mac/ - 1. Start Docker Toolbox from the command line and run: `docker-machine create -d virtualbox dev”` + 1. Start Docker Toolbox from the command line and run: `docker-machine create -d virtualbox dev` 2. Get your Docker IP address with: `docker-machine ip dev` 3. Use `sudo nano /etc/hosts` to add `<Docker IP> dockerhost` to your /etc/hosts file 4. Run: `docker-machine env dev` and copy the export statements to your ~/.bash\_profile by running `sudo nano ~/.bash_profile`. Then run: `source ~/.bash_profile` diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json index 733267f74..2611a63ce 100644 --- a/docker/dev/config_docker.json +++ b/docker/dev/config_docker.json @@ -75,7 +75,8 @@ }, "PrivacySettings": { "ShowEmailAddress": true, - "ShowFullName": true + "ShowFullName": true, + "EnableDiagnostic": false }, "GitLabSettings": { "Enable": false, diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json index 733267f74..2611a63ce 100644 --- a/docker/local/config_docker.json +++ b/docker/local/config_docker.json @@ -75,7 +75,8 @@ }, "PrivacySettings": { "ShowEmailAddress": true, - "ShowFullName": true + "ShowFullName": true, + "EnableDiagnostic": false }, "GitLabSettings": { "Enable": false, diff --git a/mattermost.go b/mattermost.go index 94bbe344d..e78e8d04a 100644 --- a/mattermost.go +++ b/mattermost.go @@ -8,6 +8,8 @@ import ( "fmt" "os" "os/signal" + "runtime" + "strconv" "strings" "syscall" "time" @@ -61,6 +63,8 @@ func main() { manualtesting.InitManualTesting() } + diagnosticsJob() + // wait for kill signal before attempting to gracefully shutdown // the running service c := make(chan os.Signal) @@ -71,6 +75,53 @@ func main() { } } +func diagnosticsJob() { + go func() { + for { + if utils.Cfg.PrivacySettings.EnableDiagnostic && !model.IsOfficalBuild() { + if result := <-api.Srv.Store.System().Get(); result.Err == nil { + props := result.Data.(model.StringMap) + lastTime, _ := strconv.ParseInt(props["LastDiagnosticTime"], 10, 0) + currentTime := model.GetMillis() + + if (currentTime - lastTime) > 1000*60*60*24*7 { + l4g.Info("Sending error and diagnostic information to mattermost") + + id := props["DiagnosticId"] + if len(id) == 0 { + id = model.NewId() + systemId := &model.System{Name: "DiagnosticId", Value: id} + <-api.Srv.Store.System().Save(systemId) + } + + systemLastTime := &model.System{Name: "LastDiagnosticTime", Value: strconv.FormatInt(currentTime, 10)} + if lastTime == 0 { + <-api.Srv.Store.System().Save(systemLastTime) + } else { + <-api.Srv.Store.System().Update(systemLastTime) + } + + m := make(map[string]string) + m[utils.PROP_DIAGNOSTIC_ID] = id + m[utils.PROP_DIAGNOSTIC_BUILD] = model.CurrentVersion + "." + model.BuildNumber + m[utils.PROP_DIAGNOSTIC_DATABASE] = utils.Cfg.SqlSettings.DriverName + m[utils.PROP_DIAGNOSTIC_OS] = runtime.GOOS + m[utils.PROP_DIAGNOSTIC_CATEGORY] = utils.VAL_DIAGNOSTIC_CATEGORY_DEFALUT + + if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil { + m[utils.PROP_DIAGNOSTIC_USER_COUNT] = strconv.FormatInt(ucr.Data.(int64), 10) + } + + utils.SendDiagnostic(m) + } + } + } + + time.Sleep(time.Hour * 24) + } + }() +} + func parseCmds() { flag.Usage = func() { fmt.Fprintln(os.Stderr, usage) diff --git a/model/config.go b/model/config.go index 5d822e263..35ceb7f4a 100644 --- a/model/config.go +++ b/model/config.go @@ -110,6 +110,7 @@ type RateLimitSettings struct { type PrivacySettings struct { ShowEmailAddress bool ShowFullName bool + EnableDiagnostic bool } type TeamSettings struct { diff --git a/model/session.go b/model/session.go index 3c7c75eb4..bb4d987a7 100644 --- a/model/session.go +++ b/model/session.go @@ -10,6 +10,7 @@ import ( const ( SESSION_TOKEN = "MMSID" + MULTI_SESSION_TOKEN = "MMSIDMU" SESSION_TIME_WEB_IN_DAYS = 30 SESSION_TIME_WEB_IN_SECS = 60 * 60 * 24 * SESSION_TIME_WEB_IN_DAYS SESSION_TIME_MOBILE_IN_DAYS = 30 diff --git a/model/version.go b/model/version.go index 233fc3747..efa1697db 100644 --- a/model/version.go +++ b/model/version.go @@ -67,6 +67,10 @@ func GetPreviousVersion(currentVersion string) (int64, int64) { return 0, 0 } +func IsOfficalBuild() bool { + return BuildNumber != "_BUILD_NUMBER_" +} + func IsCurrentVersion(versionToCheck string) bool { currentMajor, currentMinor, _ := SplitVersion(CurrentVersion) toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck) diff --git a/utils/diagnostic.go b/utils/diagnostic.go new file mode 100644 index 000000000..9a61ae934 --- /dev/null +++ b/utils/diagnostic.go @@ -0,0 +1,45 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "net/http" + + l4g "code.google.com/p/log4go" + + "github.com/mattermost/platform/model" +) + +const ( + PROP_DIAGNOSTIC_ID = "id" + PROP_DIAGNOSTIC_CATEGORY = "c" + VAL_DIAGNOSTIC_CATEGORY_DEFALUT = "d" + PROP_DIAGNOSTIC_BUILD = "b" + PROP_DIAGNOSTIC_DATABASE = "db" + PROP_DIAGNOSTIC_OS = "os" + PROP_DIAGNOSTIC_USER_COUNT = "uc" +) + +func SendDiagnostic(data model.StringMap) *model.AppError { + if Cfg.PrivacySettings.EnableDiagnostic && !model.IsOfficalBuild() { + + query := "?" + for name, value := range data { + if len(query) > 1 { + query += "&" + } + + query += name + "=" + UrlEncode(value) + } + + res, err := http.Get("http://d7zmvsa9e04kk.cloudfront.net/i" + query) + if err != nil { + l4g.Error("Failed to send diagnostics %v", err.Error()) + } + + res.Body.Close() + } + + return nil +} diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx index affd8ae11..c74d321e6 100644 --- a/web/react/components/admin_console/privacy_settings.jsx +++ b/web/react/components/admin_console/privacy_settings.jsx @@ -30,6 +30,7 @@ export default class PrivacySettings extends React.Component { var config = this.props.config; config.PrivacySettings.ShowEmailAddress = React.findDOMNode(this.refs.ShowEmailAddress).checked; config.PrivacySettings.ShowFullName = React.findDOMNode(this.refs.ShowFullName).checked; + config.PrivacySettings.EnableDiagnostic = React.findDOMNode(this.refs.EnableDiagnostic).checked; Client.saveConfig( config, @@ -137,6 +138,39 @@ export default class PrivacySettings extends React.Component { </div> <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='EnableDiagnostic' + > + {'Send Error and Diagnostic: '} + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='EnableDiagnostic' + value='true' + ref='EnableDiagnostic' + defaultChecked={this.props.config.PrivacySettings.EnableDiagnostic} + onChange={this.handleChange} + /> + {'true'} + </label> + <label className='radio-inline'> + <input + type='radio' + name='EnableDiagnostic' + value='false' + defaultChecked={!this.props.config.PrivacySettings.EnableDiagnostic} + onChange={this.handleChange} + /> + {'false'} + </label> + <p className='help-text'>{'When true, The server will periodically send error and diagnostic information to Mattermost.'}</p> + </div> + </div> + + <div className='form-group'> <div className='col-sm-12'> {serverError} <button diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 57a78a0d4..78057d10b 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -9,7 +9,7 @@ var TeamStore = require('../stores/team_store.jsx'); var Constants = require('../utils/constants.jsx'); function getStateFromStores() { - return {teams: UserStore.getTeams(), currentTeam: TeamStore.getCurrent()}; + return {teams: UserStore.getTeams()}; } export default class NavbarDropdown extends React.Component { @@ -142,10 +142,10 @@ export default class NavbarDropdown extends React.Component { > </li> ); - if (this.state.teams.length > 1 && this.state.currentTeam) { - var curTeamName = this.state.currentTeam.name; + + if (this.state.teams.length > 1) { this.state.teams.forEach((teamName) => { - if (teamName !== curTeamName) { + if (teamName !== this.props.teamName) { teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>{'Switch to ' + teamName}</a></li>); } }); @@ -234,5 +234,7 @@ NavbarDropdown.defaultProps = { teamType: '' }; NavbarDropdown.propTypes = { - teamType: React.PropTypes.string + teamType: React.PropTypes.string, + teamDisplayName: React.PropTypes.string, + teamName: React.PropTypes.string }; diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 6d4b56b7b..c0841a508 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -512,6 +512,7 @@ export default class Sidebar extends React.Component { /> <SidebarHeader teamDisplayName={this.props.teamDisplayName} + teamName={this.props.teamName} teamType={this.props.teamType} /> <SearchBox /> @@ -591,5 +592,6 @@ Sidebar.defaultProps = { }; Sidebar.propTypes = { teamType: React.PropTypes.string, - teamDisplayName: React.PropTypes.string + teamDisplayName: React.PropTypes.string, + teamName: React.PropTypes.string }; diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index 072c14e0a..33de35064 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -52,6 +52,8 @@ export default class SidebarHeader extends React.Component { <NavbarDropdown ref='dropdown' teamType={this.props.teamType} + teamDisplayName={this.props.teamDisplayName} + teamName={this.props.teamName} /> </div> ); @@ -64,5 +66,6 @@ SidebarHeader.defaultProps = { }; SidebarHeader.propTypes = { teamDisplayName: React.PropTypes.string, + teamName: React.PropTypes.string, teamType: React.PropTypes.string }; diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx index f8f916bd0..8116bffcc 100644 --- a/web/react/components/user_settings/premade_theme_chooser.jsx +++ b/web/react/components/user_settings/premade_theme_chooser.jsx @@ -24,7 +24,7 @@ export default class PremadeThemeChooser extends React.Component { premadeThemes.push( <div - className='col-sm-3 premade-themes' + className='col-xs-6 col-sm-3 premade-themes' key={'premade-theme-key' + k} > <div diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 74259194a..c333fd57d 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -36,11 +36,14 @@ var RemovedFromChannelModal = require('../components/removed_from_channel_modal. var FileUploadOverlay = require('../components/file_upload_overlay.jsx'); var RegisterAppModal = require('../components/register_app_modal.jsx'); var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx'); +var TeamStore = require('../stores/team_store.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; function setupChannelPage(props) { + TeamStore.setCurrentId(props.TeamId); + AppDispatcher.handleViewAction({ type: ActionTypes.CLICK_CHANNEL, name: props.ChannelName, @@ -71,6 +74,7 @@ function setupChannelPage(props) { React.render( <Sidebar teamDisplayName={props.TeamDisplayName} + teamName={props.TeamName} teamType={props.TeamType} />, document.getElementById('sidebar-left') diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index da59f8e5a..67414dc3b 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -179,7 +179,7 @@ module.exports = { centerChannelColor: '#DDDDDD', newMessageSeparator: '#5de5da', linkColor: '#A4FFEB', - buttonBg: '#1dacfc', + buttonBg: '#4CBBA4', buttonColor: '#FFFFFF' }, windows10: { diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 8b20e2adf..1bc082175 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -395,35 +395,39 @@ export function toTitleCase(str) { export function applyTheme(theme) { if (theme.sidebarBg) { - changeCss('.sidebar--left', 'background:' + theme.sidebarBg, 1); + changeCss('.sidebar--left, .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1); } if (theme.sidebarText) { - changeCss('.sidebar--left .nav li>a, .sidebar--right', 'color:' + theme.sidebarText, 1); - changeCss('.sidebar--left .nav li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1); + changeCss('.sidebar--left .nav-pills__container li>a, .sidebar--right, .settings-modal .nav-pills>li a, .sidebar--menu', 'color:' + theme.sidebarText, 1); + changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1); + changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1); changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1); changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 1); changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1); + changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2); } if (theme.sidebarUnreadText) { - changeCss('.sidebar--left .nav li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 1); + changeCss('.sidebar--left .nav-pills__container li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 2); } if (theme.sidebarTextHoverBg) { - changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'background:' + theme.sidebarTextHoverBg, 1); + changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .settings-modal .nav-pills>li:hover a, .settings-modal .nav-pills>li:focus a', 'background:' + theme.sidebarTextHoverBg, 1); + changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1); } if (theme.sidebarTextHoverColor) { - changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'color:' + theme.sidebarTextHoverColor, 2); + changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .settings-modal .nav-pills>li:hover a, .settings-modal .nav-pills>li:focus a', 'color:' + theme.sidebarTextHoverColor, 2); + changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'color:' + theme.sidebarTextHoverColor, 2); } if (theme.sidebarTextActiveBg) { - changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + theme.sidebarTextActiveBg, 1); + changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'background:' + theme.sidebarTextActiveBg, 1); } if (theme.sidebarTextActiveColor) { - changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'color:' + theme.sidebarTextActiveColor, 2); + changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2); } if (theme.sidebarHeaderBg) { @@ -458,45 +462,51 @@ export function applyTheme(theme) { } if (theme.centerChannelBg) { - changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .command-box', 'background:' + theme.centerChannelBg, 1); + changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .command-box, .modal .modal-content, .mentions-name, .mentions--top .mentions-box', 'background:' + theme.centerChannelBg, 1); changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1); changeCss('#post-create', 'background:' + theme.centerChannelBg, 1); changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1); changeCss('.post-image__column .post-image__details', 'background:' + theme.centerChannelBg, 1); - changeCss('.sidebar--right', 'background:' + theme.centerChannelBg, 1); + changeCss('.sidebar--right, .dropdown-menu, .popover', 'background:' + theme.centerChannelBg, 1); } if (theme.centerChannelColor) { - changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .loading-screen .loading__content .round, .command-name', 'color:' + theme.centerChannelColor, 1); + changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .loading-screen .loading__content .round, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name', 'color:' + theme.centerChannelColor, 1); changeCss('#post-create', 'color:' + theme.centerChannelColor, 2); changeCss('.mentions--top, .command-box', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3); changeCss('.mentions--top, .command-box', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2); changeCss('.mentions--top, .command-box', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 1); + changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3); + changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2); + changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1); changeCss('.post-body hr', 'background:' + theme.centerChannelColor, 1); changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1); changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); - changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); - changeCss('.command-name', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.command-name, .popover .popover-title', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.dropdown-menu .divider', 'background:' + theme.centerChannelColor, 1); changeCss('.custom-textarea', 'color:' + theme.centerChannelColor, 1); changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2); changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2); changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1); - changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.search-bar__container .search__form .search-bar', 'background: transparent; color:' + theme.centerChannelColor, 1); + changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1); changeCss('.search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1); changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2); - changeCss('.date-separator .separator__hr, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); - changeCss('.channel-intro', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1); + changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); - changeCss('.post:hover, .sidebar--right .sidebar--right__header', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); - changeCss('.date-separator.hovered--before:after, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); - changeCss('.date-separator.hovered--after:before, .new-separator.hovered--after:before, .command-name:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); + changeCss('.post:hover, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); + changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); + changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); changeCss('.post.current--user:hover .post-body ', 'background: none;', 1); changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2); } @@ -507,7 +517,7 @@ export function applyTheme(theme) { } if (theme.linkColor) { - changeCss('a, a:focus, a:hover', 'color:' + theme.linkColor, 1); + changeCss('a, a:focus, a:hover, .btn, .btn:focus, .btn:hover', 'color:' + theme.linkColor, 1); changeCss('.post .comment-icon__container', 'fill:' + theme.linkColor, 1); } diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss index 11794a269..5037da415 100644 --- a/web/sass-files/sass/partials/_admin-console.scss +++ b/web/sass-files/sass/partials/_admin-console.scss @@ -144,7 +144,6 @@ } .popover { border-radius: 3px; - border: 1px solid #ccc; width: 100%; font-size: 0.95em; } diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index 87d9b8200..fa465ff91 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -42,6 +42,21 @@ body { color: $primary-color; } } + .popover-title { + background: rgba(black, 0.1); + } +} + +.dropdown-menu { + .divider { + @include opacity(0.15); + } + > li > a { + color: inherit; + &:focus, &:hover { + color: inherit; + } + } } .word-break--all { @@ -68,6 +83,19 @@ a:focus, a:hover { margin: 0; } +.text-danger { + color: #E05F5D; +} + +.btn { + &.btn-danger { + color: #fff; + &:hover, &:active { + color: #fff; + } + } +} + .form-control { @include border-radius(2px); &.no-resize { diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index b5fcb6145..9b9e5f573 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -114,23 +114,6 @@ } } } - &.theme--black { - &:hover { - &:before { - background: rgba(white, 0.2); - } - } - } - &.theme--gray { - &:hover { - &:before { - background: rgba(white, 0.1); - } - } - } - a { - color: #fff; - } .navbar-right { font-size: 0.85em; position: absolute; @@ -145,7 +128,6 @@ .dropdown-menu { li a { padding: 3px 20px; - color: #555; text-overflow: ellipsis; overflow: hidden; } diff --git a/web/sass-files/sass/partials/_mentions.scss b/web/sass-files/sass/partials/_mentions.scss index 48746ba01..aff31e418 100644 --- a/web/sass-files/sass/partials/_mentions.scss +++ b/web/sass-files/sass/partials/_mentions.scss @@ -33,13 +33,6 @@ line-height: 36px; font-size: 13px; cursor: pointer; - &:hover { - background-color: #E6F2FA; - } -} - -.mentions-focus { - background-color: #E6F2FA; } .mentions-text { @@ -54,7 +47,6 @@ display: block; font-size: 20px; text-align: center; - color: #555; @include border-radius(32px); } diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss index e4e8b20b6..96b26f251 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -9,12 +9,6 @@ @include opacity(0.7); } } - a, a:focus, a:hover { - color: #2389D7; - &.text-danger { - color: #a94442; - } - } .custom-textarea { color: inherit; border-color: #ccc; @@ -25,12 +19,9 @@ } .btn { font-size: 13px; - &.btn-primary { - background: #4285f4; - &:hover, &:focus, &:active { - background: $primary-color--hover; - color: #fff; - } + &.btn-default { + border: none; + background: transparent; } } .info__label { @@ -70,7 +61,7 @@ background: $primary-color; color: #FFF; padding: 15px 15px 11px; - border: none; + border: 1px solid #ddd; min-height: 56px; @include clearfix; .modal-title { diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 9e8d0dc7d..82ec1811a 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -402,12 +402,6 @@ margin-left: 7px; } } - &.active, &:hover { - a { - color: #555; - background: #fff; - } - } } } } diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index 3aab05d70..1f785f63c 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -2,7 +2,6 @@ @import "activity-log"; .user-settings { - background: #fff; min-height:300px; .table-responsive { max-width: 560px; @@ -71,7 +70,7 @@ } .section-max { - background: #f2f2f2; + background: rgba(black, 0.05); padding: 1em 0 1.3em; margin-bottom: 0; @include clearfix; @@ -121,7 +120,7 @@ .fa { margin-right: 7px; font-size: 12px; - color: #aaa; + @include opacity(0.5); display: none; } } @@ -131,7 +130,7 @@ } .section-describe { - color:grey; + @include opacity(0.7); } .divider-dark { @@ -167,15 +166,11 @@ } } .control-label { - color: #555; font-weight: 600; &.text-left { text-align: left; } } - hr { - border-color: #ccc; - } } .file-status { diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss index 6a418e270..73d702fef 100644 --- a/web/sass-files/sass/partials/_sidebar--left.scss +++ b/web/sass-files/sass/partials/_sidebar--left.scss @@ -16,12 +16,6 @@ overflow-y: auto; max-width: 200px; width: 200px; - a { - color: #262626 !important; - &:hover, &:focus { - background: #f5f5f5 !important; - } - } } .search__form { margin: 0; @@ -75,7 +69,7 @@ top: 66px; } .nav-pills__unread-indicator-bottom { - bottom: 0px; + bottom: 10px; } .nav { @@ -98,7 +92,6 @@ padding: 3px 10px 3px 25px; line-height: 1.5; border-radius: 0; - color: #999; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -109,12 +102,8 @@ text-decoration: underline; } &.unread-title { - color: #333; font-weight: 600; } - &:hover, &:focus { - background: #e6f2fa; - } } &.active { a, a:hover, a:focus { diff --git a/web/sass-files/sass/partials/_sidebar--menu.scss b/web/sass-files/sass/partials/_sidebar--menu.scss index 4366b1a6c..6f4a0cc38 100644 --- a/web/sass-files/sass/partials/_sidebar--menu.scss +++ b/web/sass-files/sass/partials/_sidebar--menu.scss @@ -54,7 +54,7 @@ > a { font-size: 15px; background: none !important; - color: #444; + color: inherit; line-height: 40px; padding: 0 15px; .glyphicon { diff --git a/web/static/images/themes/mattermost dark.png b/web/static/images/themes/mattermost dark.png Binary files differindex 832f64d2e..1ab44e104 100644 --- a/web/static/images/themes/mattermost dark.png +++ b/web/static/images/themes/mattermost dark.png diff --git a/web/static/images/themes/mattermost.png b/web/static/images/themes/mattermost.png Binary files differindex 4a321adcb..8dea37ff6 100644 --- a/web/static/images/themes/mattermost.png +++ b/web/static/images/themes/mattermost.png diff --git a/web/static/images/themes/organization.png b/web/static/images/themes/organization.png Binary files differindex 1a38bfb34..3066cca0b 100644 --- a/web/static/images/themes/organization.png +++ b/web/static/images/themes/organization.png diff --git a/web/static/images/themes/slack.png b/web/static/images/themes/slack.png Binary files differdeleted file mode 100644 index dc70c7dc2..000000000 --- a/web/static/images/themes/slack.png +++ /dev/null diff --git a/web/static/images/themes/windows dark.png b/web/static/images/themes/windows dark.png Binary files differindex d65304820..1f665d882 100644 --- a/web/static/images/themes/windows dark.png +++ b/web/static/images/themes/windows dark.png diff --git a/web/web.go b/web/web.go index bf985a5a0..e440699b2 100644 --- a/web/web.go +++ b/web/web.go @@ -189,9 +189,40 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) { return } + // We still might be able to switch to this team because we've logged in before + if multiCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil { + multiToken := multiCookie.Value + + if len(multiToken) > 0 { + tokens := strings.Split(multiToken, " ") + + for _, token := range tokens { + if sr := <-api.Srv.Store.Session().Get(token); sr.Err == nil { + s := sr.Data.(*model.Session) + + if !s.IsExpired() && s.TeamId == team.Id { + w.Header().Set(model.HEADER_TOKEN, s.Token) + sessionCookie := &http.Cookie{ + Name: model.SESSION_TOKEN, + Value: s.Token, + Path: "/", + MaxAge: model.SESSION_TIME_WEB_IN_SECS, + HttpOnly: true, + } + + http.SetCookie(w, sessionCookie) + + http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/town-square", http.StatusTemporaryRedirect) + return + } + } + } + } + } + page := NewHtmlTemplatePage("login", "Login") page.Props["TeamDisplayName"] = team.DisplayName - page.Props["TeamName"] = teamName + page.Props["TeamName"] = team.Name page.Render(c, w) } @@ -288,6 +319,10 @@ func logout(c *api.Context, w http.ResponseWriter, r *http.Request) { func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) name := params["channelname"] + teamName := params["team"] + + var team *model.Team + teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) var channelId string if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil { @@ -297,6 +332,19 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { channelId = result.Data.(string) } + if tResult := <-teamChan; tResult.Err != nil { + c.Err = tResult.Err + return + } else { + team = tResult.Data.(*model.Team) + } + + if team.Name != teamName { + l4g.Error("It appears you are logged into " + team.Name + ", but are trying to access " + teamName) + http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/town-square", http.StatusFound) + return + } + if len(channelId) == 0 { if strings.Index(name, "__") > 0 { // It's a direct message channel that doesn't exist yet so let's create it @@ -319,7 +367,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { // lets make sure the user is valid if result := <-api.Srv.Store.User().Get(c.Session.UserId); result.Err != nil { c.Err = result.Err - c.RemoveSessionCookie(w) + c.RemoveSessionCookie(w, r) l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId) return } @@ -332,18 +380,10 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { } } - var team *model.Team - - if tResult := <-api.Srv.Store.Team().Get(c.Session.TeamId); tResult.Err != nil { - c.Err = tResult.Err - return - } else { - team = tResult.Data.(*model.Team) - } - page := NewHtmlTemplatePage("channel", "") page.Props["Title"] = name + " - " + team.DisplayName + " " + page.ClientProps["SiteName"] page.Props["TeamDisplayName"] = team.DisplayName + page.Props["TeamName"] = team.Name page.Props["TeamType"] = team.Type page.Props["TeamId"] = team.Id page.Props["ChannelName"] = name @@ -451,6 +491,7 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { page := NewHtmlTemplatePage("password_reset", "") page.Props["Title"] = "Reset Password " + page.ClientProps["SiteName"] page.Props["TeamDisplayName"] = teamDisplayName + page.Props["TeamName"] = teamName page.Props["Hash"] = hash page.Props["Data"] = data page.Props["TeamName"] = teamName |