diff options
Diffstat (limited to 'web/react')
112 files changed, 2678 insertions, 2736 deletions
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/about_build_modal.jsx b/web/react/components/about_build_modal.jsx index fe48bb48e..34b1fdccf 100644 --- a/web/react/components/about_build_modal.jsx +++ b/web/react/components/about_build_modal.jsx @@ -21,29 +21,38 @@ export default class AboutBuildModal extends React.Component { let title = ( <FormattedMessage - id='about.teamEdtion' - defaultMessage='Team Edition' + id='about.teamEditiont0' + defaultMessage='Team Edition T0' /> ); + let licensee; - if (config.BuildEnterpriseReady === 'true' && license.IsLicensed === 'true') { + if (config.BuildEnterpriseReady === 'true') { title = ( <FormattedMessage - id='about.enterpriseEdition' - defaultMessage='Enterprise Edition' + id='about.teamEditiont1' + defaultMessage='Team Edition T1' /> ); - licensee = ( - <div className='row form-group'> - <div className='col-sm-3 info__label'> - <FormattedMessage - id='about.licensed' - defaultMessage='Licensed by:' - /> + if (license.IsLicensed === 'true') { + title = ( + <FormattedMessage + id='about.enterpriseEditione1' + defaultMessage='Enterprise Edition E1' + /> + ); + licensee = ( + <div className='row form-group'> + <div className='col-sm-3 info__label'> + <FormattedMessage + id='about.licensed' + defaultMessage='Licensed by:' + /> + </div> + <div className='col-sm-9'>{license.Company}</div> </div> - <div className='col-sm-9'>{license.Company}</div> - </div> - ); + ); + } } return ( 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/ldap_settings.jsx b/web/react/components/admin_console/ldap_settings.jsx index 535c264dd..4cd19c886 100644 --- a/web/react/components/admin_console/ldap_settings.jsx +++ b/web/react/components/admin_console/ldap_settings.jsx @@ -20,7 +20,7 @@ var holders = defineMessages({ }, baseEx: { id: 'admin.ldap.baseEx', - defaultMessage: 'Ex "dc=mydomain,dc=com"' + defaultMessage: 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"' }, firstnameAttrEx: { id: 'admin.ldap.firstnameAttrEx', @@ -32,7 +32,7 @@ var holders = defineMessages({ }, emailAttrEx: { id: 'admin.ldap.emailAttrEx', - defaultMessage: 'Ex "mail"' + defaultMessage: 'Ex "mail" or "userPrincipalName"' }, usernameAttrEx: { id: 'admin.ldap.usernameAttrEx', @@ -581,4 +581,4 @@ LdapSettings.propTypes = { config: React.PropTypes.object }; -export default injectIntl(LdapSettings);
\ No newline at end of file +export default injectIntl(LdapSettings); diff --git a/web/react/components/admin_console/license_settings.jsx b/web/react/components/admin_console/license_settings.jsx index d4dfa13f2..9d2ec8030 100644 --- a/web/react/components/admin_console/license_settings.jsx +++ b/web/react/components/admin_console/license_settings.jsx @@ -27,6 +27,7 @@ class LicenseSettings extends React.Component { this.state = { fileSelected: false, + fileName: null, serverError: null }; } @@ -34,7 +35,7 @@ class LicenseSettings extends React.Component { handleChange() { const element = $(ReactDOM.findDOMNode(this.refs.fileInput)); if (element.prop('files').length > 0) { - this.setState({fileSelected: true}); + this.setState({fileSelected: true, fileName: element.prop('files')[0].name}); } } @@ -56,13 +57,13 @@ class LicenseSettings extends React.Component { () => { Utils.clearFileInput(element[0]); $('#upload-button').button('reset'); - this.setState({serverError: null}); + this.setState({fileSelected: false, fileName: null, serverError: null}); window.location.reload(true); }, (error) => { Utils.clearFileInput(element[0]); $('#upload-button').button('reset'); - this.setState({serverError: error.message}); + this.setState({fileSelected: false, fileName: null, serverError: error.message}); } ); } @@ -75,12 +76,12 @@ class LicenseSettings extends React.Component { Client.removeLicenseFile( () => { $('#remove-button').button('reset'); - this.setState({serverError: null}); + this.setState({fileSelected: false, fileName: null, serverError: null}); window.location.reload(true); }, (error) => { $('#remove-button').button('reset'); - this.setState({serverError: error.message}); + this.setState({fileSelected: false, fileName: null, serverError: error.message}); } ); } @@ -172,17 +173,36 @@ class LicenseSettings extends React.Component { /> ); + let fileName; + if (this.state.fileName) { + fileName = this.state.fileName; + } else { + fileName = ( + <FormattedMessage + id='admin.license.noFile' + defaultMessage='No file uploaded' + /> + ); + } + licenseKey = ( <div className='col-sm-8'> - <input - className='pull-left' - ref='fileInput' - type='file' - accept='.mattermost-license' - onChange={this.handleChange} - /> + <div className='file__upload'> + <button className='btn btn-default'> + <FormattedMessage + id='admin.license.choose' + defaultMessage='Choose File' + /> + </button> + <input + ref='fileInput' + type='file' + accept='.mattermost-license' + onChange={this.handleChange} + /> + </div> <button - className={btnClass + ' pull-left'} + className={btnClass} disabled={!this.state.fileSelected} onClick={this.handleSubmit} id='upload-button' @@ -193,11 +213,12 @@ class LicenseSettings extends React.Component { defaultMessage='Upload' /> </button> - <br/> - <br/> + <div className='help-text no-margin'> + {fileName} + </div> <br/> {serverError} - <p className='help-text'> + <p className='help-text no-margin'> <FormattedHTMLMessage id='admin.license.uploadDesc' defaultMessage='Upload a license key for Mattermost Enterprise Edition to upgrade this server. <a href="http://mattermost.com" target="_blank">Visit us online</a> to learn more about the benefits of Enterprise Edition or to purchase a key.' 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 c719c6c7d..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) { @@ -185,6 +185,7 @@ class FileAttachment extends React.Component { data-toggle='tooltip' title={this.props.intl.formatMessage(holders.download) + ' \"' + filenameString + '\"'} className='post-image__name' + target='_blank' > {trimmedFilename} </a> @@ -193,6 +194,7 @@ class FileAttachment extends React.Component { href={fileUrl} download={filenameString} className='post-image__download' + target='_blank' > <span className='fa fa-download' 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..d3ee35082 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -6,82 +6,120 @@ 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; + const ldapEnabled = global.window.mm_config.EnableLdap === 'true'; + const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true'; 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 +129,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 +163,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 +172,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,22 +206,23 @@ 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> + if (ldapEnabled && (loginMessage.length > 0 || emailSignup || usernameSigninEnabled)) { + ldapLogin = ( + <div> + <div className='or__container'> + <FormattedMessage + id='login.or' + defaultMessage='or' + /> + </div> + <LoginLdap + teamName={teamName} + /> </div> ); } @@ -192,49 +231,72 @@ export default class Login extends React.Component { 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 - }} + if (usernameSigninEnabled && (loginMessage.length > 0 || emailSignup || ldapEnabled)) { + usernameLogin = ( + <div> + <div className='or__container'> + <FormattedMessage + id='login.or' + defaultMessage='or' + /> + </div> + <LoginUsername + teamName={teamName} /> - </h2> - {extraBox} - {loginMessage} - {emailSignup} - {usernameLogin} - {ldapLogin} - {userSignUp} - {findTeams} - {forgotPassword} - {teamSignUp} + </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'> + <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 889d4311e..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'; @@ -98,7 +97,7 @@ export default class Post extends React.Component { return true; } - if (nextProps.hasProfiles !== this.props.hasProfiles) { + if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) { return true; } @@ -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} @@ -226,7 +227,6 @@ export default class Post extends React.Component { posts={posts} handleCommentClick={this.handleCommentClick} retryPost={this.retryPost} - hasProfiles={this.props.hasProfiles} /> </div> </div> @@ -247,5 +247,6 @@ Post.propTypes = { isLastComment: React.PropTypes.bool, shouldHighlight: React.PropTypes.bool, displayNameType: React.PropTypes.string, - hasProfiles: React.PropTypes.bool + 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 70cf86748..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 @@ -215,8 +213,7 @@ PostBody.propTypes = { post: React.PropTypes.object.isRequired, parentPost: React.PropTypes.object, retryPost: React.PropTypes.func.isRequired, - handleCommentClick: React.PropTypes.func.isRequired, - hasProfiles: React.PropTypes.bool + handleCommentClick: React.PropTypes.func.isRequired }; export default injectIntl(PostBody); diff --git a/web/react/components/post_body_additional_content.jsx b/web/react/components/post_body_additional_content.jsx index c2a928f3b..70b3c8dbf 100644 --- a/web/react/components/post_body_additional_content.jsx +++ b/web/react/components/post_body_additional_content.jsx @@ -112,24 +112,32 @@ export default class PostBodyAdditionalContent extends React.Component { } render() { - var generateEmbed = this.generateEmbed(); + const generateEmbed = this.generateEmbed(); + if (generateEmbed) { - return ( - <div> + let toggle; + if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_TOGGLE)) { + toggle = ( <a className='post__embed-visibility' data-expanded={this.state.embedVisible} aria-label='Toggle Embed Visibility' onClick={this.toggleEmbedVisibility} - > - </a> + /> + ); + } + + return ( + <div> + {toggle} <div className='post__embed-container' hidden={!this.state.embedVisible} > {generateEmbed} </div> </div> - ); + ); } + return null; } } 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 9a1673483..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,10 +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} - hasProfiles={profiles && Object.keys(profiles).length > 1} user={profile} + currentUser={this.props.currentUser} /> ); @@ -526,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, @@ -536,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 92d658b55..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) { @@ -149,11 +157,15 @@ export default class PostsViewContainer extends React.Component { } } shouldComponentUpdate(nextProps, nextState) { - if (Utils.areObjectsEqual(this.state, nextState)) { - return false; + if (!Utils.areObjectsEqual(this.state, nextState)) { + return true; + } + + if (!Utils.areObjectsEqual(this.props, nextProps)) { + return true; } - return true; + return false; } render() { const postLists = this.state.postLists; @@ -161,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); @@ -181,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 05292b7b3..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' /> @@ -123,6 +122,7 @@ export default class SearchResultsItem extends React.Component { </ul> <div className='search-item-snippet'> <span + onClick={TextFormatting.handleClick} dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, formattingOptions)}} /> </div> 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/textbox.jsx b/web/react/components/textbox.jsx index 46900e436..d4eb60676 100644 --- a/web/react/components/textbox.jsx +++ b/web/react/components/textbox.jsx @@ -61,7 +61,7 @@ export default class Textbox extends React.Component { onRecievedError() { const errorCount = ErrorStore.getConnectionErrorCount(); - if (errorCount > 0) { + if (errorCount > 1) { this.setState({connection: 'bad-connection'}); } else { this.setState({connection: ''}); @@ -194,7 +194,6 @@ export default class Textbox extends React.Component { defaultMessage='>quote' /> </span> - {previewLink} </div> ); @@ -230,16 +229,19 @@ export default class Textbox extends React.Component { > </div> {helpText} - <a - target='_blank' - href='http://docs.mattermost.com/help/getting-started/messaging-basics.html' - className='textbox-help-link' - > - <FormattedMessage - id='textbox.help' - defaultMessage='Help' - /> - </a> + <div className='help__text'> + {previewLink} + <a + target='_blank' + href='http://docs.mattermost.com/help/getting-started/messaging-basics.html' + className='textbox-help-link' + > + <FormattedMessage + id='textbox.help' + defaultMessage='Help' + /> + </a> + </div> </div> ); } 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/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx index 1e724bb6e..4ee9fd0e2 100644 --- a/web/react/components/user_settings/custom_theme_chooser.jsx +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -253,6 +253,8 @@ class CustomThemeChooser extends React.Component { </div> </div> ); + + colors += theme[element.id] + ','; } else if (element.group === 'sidebarElements') { sidebarElements.push( <div 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/components/view_image_popover_bar.jsx b/web/react/components/view_image_popover_bar.jsx index 819df76d8..18be5a3c5 100644 --- a/web/react/components/view_image_popover_bar.jsx +++ b/web/react/components/view_image_popover_bar.jsx @@ -51,6 +51,7 @@ export default class ViewImagePopoverBar extends React.Component { href={this.props.fileURL} download={this.props.filename} className='text' + target='_blank' > <FormattedMessage id='view_image_popover.download' 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 eb3254cfe..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) { 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/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/socket_store.jsx b/web/react/stores/socket_store.jsx index 9b2b049b7..181de53d7 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; @@ -31,6 +31,7 @@ class SocketStoreClass extends EventEmitter { this.close = this.close.bind(this); this.failCount = 0; + this.isInitialize = false; this.translations = this.getDefaultTranslations(); @@ -42,10 +43,6 @@ class SocketStoreClass extends EventEmitter { return; } - if (!global.window.hasOwnProperty('mm_session_token_index')) { - return; - } - this.setMaxListeners(0); if (window.WebSocket && !conn) { @@ -54,28 +51,27 @@ 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 - if (ErrorStore.getConnectionErrorCount() > 0) { - ErrorStore.setConnectionErrorCount(0); - ErrorStore.emitChange(); - } } + conn = new WebSocket(connUrl); conn.onopen = () => { if (this.failCount > 0) { console.log('websocket re-established connection'); //eslint-disable-line no-console + AsyncClient.getChannels(); + AsyncClient.getPosts(ChannelStore.getCurrentId()); + } + if (this.isInitialize) { ErrorStore.clearLastError(); ErrorStore.emitChange(); - - AsyncClient.getChannels(); - AsyncClient.getPosts(ChannelStore.getCurrentId()); } + this.isInitialize = true; this.failCount = 0; }; @@ -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/team_store.jsx b/web/react/stores/team_store.jsx index 7a1a2ef42..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,11 +127,14 @@ 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: } }); diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index 75a87d424..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(); 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..3de562b7b 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, @@ -267,7 +270,7 @@ export default { buttonColor: '#FFFFFF', mentionHighlightBg: '#984063', mentionHighlightLink: '#A4FFEB', - codeTheme: 'solarized_dark' + codeTheme: 'solarized-dark' }, windows10: { type: 'Windows Dark', @@ -371,21 +374,6 @@ export default { uiName: 'New Message Separator' }, { - group: 'linkAndButtonElements', - id: 'linkColor', - uiName: 'Link Color' - }, - { - group: 'linkAndButtonElements', - id: 'buttonBg', - uiName: 'Button BG' - }, - { - group: 'linkAndButtonElements', - id: 'buttonColor', - uiName: 'Button Text' - }, - { group: 'centerChannelElements', id: 'mentionHighlightBg', uiName: 'Mention Highlight BG' @@ -401,11 +389,11 @@ export default { uiName: 'Code Theme', themes: [ { - id: 'solarized_dark', + id: 'solarized-dark', uiName: 'Solarized Dark' }, { - id: 'solarized_light', + id: 'solarized-light', uiName: 'Solarized Light' }, { @@ -417,6 +405,21 @@ export default { uiName: 'Monokai' } ] + }, + { + group: 'linkAndButtonElements', + id: 'linkColor', + uiName: 'Link Color' + }, + { + group: 'linkAndButtonElements', + id: 'buttonBg', + uiName: 'Button BG' + }, + { + group: 'linkAndButtonElements', + id: 'buttonColor', + uiName: 'Button Text' } ], DEFAULT_CODE_THEME: 'github', diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index 493916058..2b1aed9c0 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -193,6 +193,16 @@ class MattermostMarkdownRenderer extends marked.Renderer { outHref = outHref.substring(1, outHref.length - 1); } + try { + const unescaped = decodeURIComponent(unescape(href)).replace(/[^\w:]/g, '').toLowerCase(); + + if (unescaped.indexOf('javascript:') === 0 || unescaped.indexOf('vbscript:') === 0) { // eslint-disable-line no-script-url + return ''; + } + } catch (e) { + return ''; + } + if (!(/[a-z+.-]+:/i).test(outHref)) { outHref = `http://${outHref}`; } @@ -548,3 +558,18 @@ export function format(text, options) { return new MattermostParser(markdownOptions).parse(tokens); } +// Marked helper functions that should probably just be exported + +function unescape(html) { + return html.replace(/&([#\w]+);/g, (_, m) => { + const n = m.toLowerCase(); + if (n === 'colon') { + return ':'; + } else if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' ? + String.fromCharCode(parseInt(n.substring(2), 16)) : + String.fromCharCode(+n.substring(1)); + } + return ''; + }); +} 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; +} |