From d7cdcf082fab6c0cb7c2fe4bed821bd1a8000e69 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Mon, 8 Feb 2016 07:26:10 -0500 Subject: Convering client to react-router. --- web/react/action_creators/global_actions.jsx | 252 +++++ web/react/components/activity_log_modal.jsx | 36 +- .../components/admin_console/admin_controller.jsx | 11 +- .../admin_console/admin_navbar_dropdown.jsx | 19 +- .../components/admin_console/admin_sidebar.jsx | 4 - .../admin_console/admin_sidebar_header.jsx | 5 +- web/react/components/admin_console/user_item.jsx | 2 +- web/react/components/audit_table.jsx | 21 +- web/react/components/center_panel.jsx | 47 +- web/react/components/channel_header.jsx | 32 +- web/react/components/channel_invite_modal.jsx | 37 +- web/react/components/channel_loader.jsx | 204 ---- .../components/channel_notifications_modal.jsx | 107 +- web/react/components/channel_view.jsx | 26 +- web/react/components/claim/claim_account.jsx | 87 +- web/react/components/claim/sso_to_email.jsx | 2 +- web/react/components/create_post.jsx | 6 +- web/react/components/delete_channel_modal.jsx | 4 +- web/react/components/do_verify_email.jsx | 82 ++ web/react/components/docs.jsx | 41 - web/react/components/edit_post_modal.jsx | 4 +- web/react/components/email_verify.jsx | 108 -- web/react/components/file_attachment.jsx | 4 +- web/react/components/find_team.jsx | 135 --- web/react/components/invite_member_modal.jsx | 4 +- web/react/components/logged_in.jsx | 224 ++++ web/react/components/login.jsx | 222 ++-- web/react/components/login_email.jsx | 11 +- web/react/components/navbar.jsx | 20 +- web/react/components/navbar_dropdown.jsx | 88 +- web/react/components/needs_team.jsx | 20 + web/react/components/not_logged_in.jsx | 70 ++ web/react/components/password_reset.jsx | 47 - web/react/components/password_reset_form.jsx | 105 +- web/react/components/password_reset_send_link.jsx | 186 ++-- web/react/components/popover_list_members.jsx | 2 +- web/react/components/post.jsx | 19 +- web/react/components/post_body.jsx | 10 +- web/react/components/post_focus_view.jsx | 22 +- web/react/components/post_header.jsx | 17 +- web/react/components/post_info.jsx | 56 +- web/react/components/posts_view.jsx | 17 +- web/react/components/posts_view_container.jsx | 19 +- web/react/components/rhs_comment.jsx | 8 +- web/react/components/rhs_root_post.jsx | 8 +- web/react/components/root.jsx | 90 ++ web/react/components/search_results_item.jsx | 9 +- web/react/components/should_verify_email.jsx | 111 ++ web/react/components/sidebar.jsx | 30 +- web/react/components/sidebar_header.jsx | 14 +- web/react/components/sidebar_right.jsx | 9 +- web/react/components/sidebar_right_menu.jsx | 39 +- web/react/components/signup_team.jsx | 159 +-- web/react/components/signup_team_complete.jsx | 121 -- .../components/signup_team_complete.jsx | 79 ++ .../components/team_signup_display_name_page.jsx | 136 +++ .../components/team_signup_email_item.jsx | 86 ++ .../components/team_signup_finished.jsx | 15 + .../components/team_signup_password_page.jsx | 215 ++++ .../components/team_signup_send_invites_page.jsx | 210 ++++ .../components/team_signup_url_page.jsx | 205 ++++ .../components/team_signup_username_page.jsx | 164 +++ .../components/team_signup_welcome_page.jsx | 234 ++++ web/react/components/signup_team_confirm.jsx | 47 +- web/react/components/signup_user_complete.jsx | 331 +++--- .../components/suggestion/at_mention_provider.jsx | 2 +- web/react/components/suggestion/suggestion_box.jsx | 12 +- .../components/suggestion/suggestion_list.jsx | 4 +- web/react/components/team_members_modal.jsx | 32 +- web/react/components/team_settings.jsx | 3 + .../components/team_signup_display_name_page.jsx | 136 --- web/react/components/team_signup_email_item.jsx | 86 -- web/react/components/team_signup_password_page.jsx | 214 ---- .../components/team_signup_send_invites_page.jsx | 210 ---- web/react/components/team_signup_url_page.jsx | 205 ---- web/react/components/team_signup_username_page.jsx | 164 --- web/react/components/team_signup_welcome_page.jsx | 232 ---- web/react/components/team_signup_with_email.jsx | 7 +- web/react/components/user_list_row.jsx | 2 +- web/react/components/user_profile.jsx | 29 +- .../components/user_settings/manage_languages.jsx | 3 +- .../user_settings/user_settings_developer.jsx | 4 +- .../user_settings/user_settings_general.jsx | 15 +- .../user_settings/user_settings_modal.jsx | 16 +- .../user_settings/user_settings_security.jsx | 29 +- web/react/dispatcher/event_helpers.jsx | 222 ---- web/react/package.json | 2 + web/react/pages/admin_console.jsx | 71 -- web/react/pages/channel.jsx | 97 -- web/react/pages/claim_account.jsx | 68 -- web/react/pages/docs.jsx | 64 -- web/react/pages/find_team.jsx | 62 -- web/react/pages/home.jsx | 16 - web/react/pages/login.jsx | 66 -- web/react/pages/password_reset.jsx | 68 -- web/react/pages/root.jsx | 290 +++++ web/react/pages/signup_team.jsx | 76 -- web/react/pages/signup_team_complete.jsx | 66 -- web/react/pages/signup_team_confirm.jsx | 64 -- web/react/pages/signup_user_complete.jsx | 69 -- web/react/pages/verify.jsx | 67 -- web/react/stores/admin_store.jsx | 10 +- web/react/stores/analytics_store.jsx | 4 - web/react/stores/browser_store.jsx | 6 +- web/react/stores/channel_store.jsx | 4 - web/react/stores/file_store.jsx | 7 +- web/react/stores/localization_store.jsx | 60 + web/react/stores/modal_store.jsx | 4 - web/react/stores/post_store.jsx | 4 - web/react/stores/search_store.jsx | 4 - web/react/stores/socket_store.jsx | 14 +- web/react/stores/suggestion_store.jsx | 7 +- web/react/stores/team_store.jsx | 42 +- web/react/stores/user_store.jsx | 66 +- web/react/utils/async_client.jsx | 55 +- web/react/utils/channel_intro_messages.jsx | 49 +- web/react/utils/client.jsx | 179 ++- web/react/utils/constants.jsx | 5 +- web/react/utils/utils.jsx | 33 +- web/sass-files/sass/partials/_sidebar--left.scss | 6 - web/static/help/Messaging_en.md | 47 - web/static/help/Messaging_es.md | 37 - web/static/i18n/en.json | 17 +- web/static/i18n/es.json | 14 +- web/static/i18n/pt.json | 5 +- web/templates/admin_console.html | 21 - web/templates/authorize.html | 12 - web/templates/channel.html | 21 - web/templates/claim_account.html | 30 - web/templates/docs.html | 27 - web/templates/find_team.html | 30 - web/templates/footer.html | 39 - web/templates/head.html | 191 ---- web/templates/home.html | 24 - web/templates/login.html | 27 - web/templates/password_reset.html | 30 - web/templates/signup_team.html | 29 - web/templates/signup_team_complete.html | 29 - web/templates/signup_team_confirm.html | 26 - web/templates/signup_user_complete.html | 29 - web/templates/verify.html | 30 - web/web.go | 1158 +------------------- 142 files changed, 3971 insertions(+), 5989 deletions(-) create mode 100644 web/react/action_creators/global_actions.jsx delete mode 100644 web/react/components/channel_loader.jsx create mode 100644 web/react/components/do_verify_email.jsx delete mode 100644 web/react/components/docs.jsx delete mode 100644 web/react/components/email_verify.jsx delete mode 100644 web/react/components/find_team.jsx create mode 100644 web/react/components/logged_in.jsx create mode 100644 web/react/components/needs_team.jsx create mode 100644 web/react/components/not_logged_in.jsx delete mode 100644 web/react/components/password_reset.jsx create mode 100644 web/react/components/root.jsx create mode 100644 web/react/components/should_verify_email.jsx delete mode 100644 web/react/components/signup_team_complete.jsx create mode 100644 web/react/components/signup_team_complete/components/signup_team_complete.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_email_item.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_finished.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_password_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_url_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_username_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx delete mode 100644 web/react/components/team_signup_display_name_page.jsx delete mode 100644 web/react/components/team_signup_email_item.jsx delete mode 100644 web/react/components/team_signup_password_page.jsx delete mode 100644 web/react/components/team_signup_send_invites_page.jsx delete mode 100644 web/react/components/team_signup_url_page.jsx delete mode 100644 web/react/components/team_signup_username_page.jsx delete mode 100644 web/react/components/team_signup_welcome_page.jsx delete mode 100644 web/react/dispatcher/event_helpers.jsx delete mode 100644 web/react/pages/admin_console.jsx delete mode 100644 web/react/pages/channel.jsx delete mode 100644 web/react/pages/claim_account.jsx delete mode 100644 web/react/pages/docs.jsx delete mode 100644 web/react/pages/find_team.jsx delete mode 100644 web/react/pages/home.jsx delete mode 100644 web/react/pages/login.jsx delete mode 100644 web/react/pages/password_reset.jsx create mode 100644 web/react/pages/root.jsx delete mode 100644 web/react/pages/signup_team.jsx delete mode 100644 web/react/pages/signup_team_complete.jsx delete mode 100644 web/react/pages/signup_team_confirm.jsx delete mode 100644 web/react/pages/signup_user_complete.jsx delete mode 100644 web/react/pages/verify.jsx create mode 100644 web/react/stores/localization_store.jsx delete mode 100644 web/static/help/Messaging_en.md delete mode 100644 web/static/help/Messaging_es.md delete mode 100644 web/templates/admin_console.html delete mode 100644 web/templates/authorize.html delete mode 100644 web/templates/channel.html delete mode 100644 web/templates/claim_account.html delete mode 100644 web/templates/docs.html delete mode 100644 web/templates/find_team.html delete mode 100644 web/templates/footer.html delete mode 100644 web/templates/head.html delete mode 100644 web/templates/home.html delete mode 100644 web/templates/login.html delete mode 100644 web/templates/password_reset.html delete mode 100644 web/templates/signup_team.html delete mode 100644 web/templates/signup_team_complete.html delete mode 100644 web/templates/signup_team_confirm.html delete mode 100644 web/templates/signup_user_complete.html delete mode 100644 web/templates/verify.html (limited to 'web') diff --git a/web/react/action_creators/global_actions.jsx b/web/react/action_creators/global_actions.jsx new file mode 100644 index 000000000..4375d6c87 --- /dev/null +++ b/web/react/action_creators/global_actions.jsx @@ -0,0 +1,252 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import ChannelStore from '../stores/channel_store.jsx'; +import PostStore from '../stores/post_store.jsx'; +import SearchStore from '../stores/search_store.jsx'; +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; +import * as AsyncClient from '../utils/async_client.jsx'; +import * as Client from '../utils/client.jsx'; +import * as Utils from '../utils/utils.jsx'; + +export function emitChannelClickEvent(channel) { + AsyncClient.getChannels(true); + AsyncClient.getChannelExtraInfo(channel.id); + AsyncClient.updateLastViewedAt(channel.id); + AsyncClient.getPosts(channel.id); + + AppDispatcher.handleViewAction({ + type: ActionTypes.CLICK_CHANNEL, + name: channel.name, + id: channel.id, + prev: ChannelStore.getCurrentId() + }); +} + +export function emitPostFocusEvent(postId) { + Client.getPostById( + postId, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_FOCUSED_POST, + postId, + post_list: data + }); + + AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); + AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); + } + ); +} + +export function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) { + Client.getPost( + post.channel_id, + post.id, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POSTS, + id: post.channel_id, + numRequested: 0, + post_list: data + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POST_SELECTED, + postId: Utils.getRootId(post), + from_search: SearchStore.getSearchTerm() + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_SEARCH, + results: null, + is_mention_search: isMentionSearch + }); + }, + (err) => { + AsyncClient.dispatchError(err, 'getPost'); + } + ); +} + +export function emitLoadMorePostsEvent() { + const id = ChannelStore.getCurrentId(); + loadMorePostsTop(id); +} + +export function emitLoadMorePostsFocusedTopEvent() { + const id = PostStore.getFocusedPostId(); + loadMorePostsTop(id); +} + +export function loadMorePostsTop(id) { + const earliestPostId = PostStore.getEarliestPost(id).id; + if (PostStore.requestVisibilityIncrease(id, Constants.POST_CHUNK_SIZE)) { + AsyncClient.getPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE); + } +} + +export function emitLoadMorePostsFocusedBottomEvent() { + const id = PostStore.getFocusedPostId(); + const latestPostId = PostStore.getLatestPost(id).id; + AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE); +} + +export function emitPostRecievedEvent(post) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POST, + post + }); +} + +export function emitUserPostedEvent(post) { + AppDispatcher.handleServerAction({ + type: ActionTypes.CREATE_POST, + post + }); +} + +export function emitPostDeletedEvent(post) { + AppDispatcher.handleServerAction({ + type: ActionTypes.POST_DELETED, + post + }); +} + +export function showDeletePostModal(post, commentCount = 0) { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_DELETE_POST_MODAL, + value: true, + post, + commentCount + }); +} + +export function showGetPostLinkModal(post) { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_GET_POST_LINK_MODAL, + value: true, + post + }); +} + +export function showGetTeamInviteLinkModal() { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, + value: true + }); +} + +export function showInviteMemberModal() { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, + value: true + }); +} + +export function showRegisterAppModal() { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_REGISTER_APP_MODAL, + value: true + }); +} + +export function emitSuggestionPretextChanged(suggestionId, pretext) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_PRETEXT_CHANGED, + id: suggestionId, + pretext + }); +} + +export function emitSelectNextSuggestion(suggestionId) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_SELECT_NEXT, + id: suggestionId + }); +} + +export function emitSelectPreviousSuggestion(suggestionId) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_SELECT_PREVIOUS, + id: suggestionId + }); +} + +export function emitCompleteWordSuggestion(suggestionId, term = '') { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.SUGGESTION_COMPLETE_WORD, + id: suggestionId, + term + }); +} + +export function emitClearSuggestions(suggestionId) { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS, + id: suggestionId + }); +} + +export function emitPreferenceChangedEvent(preference) { + AppDispatcher.handleServerAction({ + type: Constants.ActionTypes.RECEIVED_PREFERENCE, + preference + }); +} + +export function emitRemovePost(post) { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.REMOVE_POST, + post + }); +} + +export function sendEphemeralPost(message, channelId) { + const timestamp = Utils.getTimestamp(); + const post = { + id: Utils.generateId(), + user_id: '0', + channel_id: channelId || ChannelStore.getCurrentId(), + message, + type: Constants.POST_TYPE_EPHEMERAL, + create_at: timestamp, + update_at: timestamp, + filenames: [], + props: {} + }; + + emitPostRecievedEvent(post); +} + +export function loadTeamRequiredPage() { + AsyncClient.getAllTeams(); +} + +export function newLocalizationSelected(locale) { + Client.getTranslations( + locale, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_LOCALE, + locale, + translations: data + }); + }, + (err) => { + AsyncClient.dispatchError(err, 'getTranslations'); + } + ); +} + +export function viewLoggedIn() { + AsyncClient.getChannels(); + AsyncClient.getChannelExtraInfo(); + AsyncClient.getMyTeam(); + AsyncClient.getMe(); + + // Clear pending posts (shouldn't have pending posts if we are loading) + PostStore.clearPendingPosts(); +} diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index 95b4caa12..db366f8ed 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -8,7 +8,7 @@ const Modal = ReactBootstrap.Modal; import LoadingScreen from './loading_screen.jsx'; import * as Utils from '../utils/utils.jsx'; -import {FormattedMessage} from 'mm-intl'; +import {FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl'; export default class ActivityLogModal extends React.Component { constructor(props) { @@ -144,8 +144,21 @@ export default class ActivityLogModal extends React.Component { id='activity_log.firstTime' defaultMessage='First time active: {date}, {time}' values={{ - date: firstAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}) + date: ( + + ), + time: ( + + ) }} /> @@ -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: ( + + ), + time: ( + + ) }} /> 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 ( -
+
+ ); } } @@ -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 (
); + } let content; - if (this.props.email === '') { + if (this.state.email === '') { content = (

); - } else if (this.props.currentType === '' && this.props.newType !== '') { + } else if (this.state.oldType === '' && this.state.newType !== '') { content = ( ); } else { content = ( ); } - return content; + return ( +
+ +
+
+ +
+ {content} +
+
+
+
+ ); } } 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 (); + } + + return ( +
+ +
+
+

+ +

+
+

+ +

+

+ + {this.state.serverError} +

+
+
+
+
+ ); + } +} + +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 ( -
-
- ); - } -} - -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 = ( - - ); - body = ( - - ); - body = ( -

- -

- ); - resend = ( - - ); - if (this.props.resendSuccess) { - resendConfirm = ( -

- -

); - } - } - - return ( -
-
-

{title}

-
- {body} - {resend} - {resendConfirm} -
-
-
- ); - } -} - -EmailVerify.defaultProps = { - isVerified: 'false', - teamURL: '', - userEmail: '', - resendSuccess: 'false' -}; -EmailVerify.propTypes = { - isVerified: React.PropTypes.string, - teamURL: React.PropTypes.string, - userEmail: React.PropTypes.string, - resendSuccess: React.PropTypes.string -}; diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index 2f6067b86..8abcac8c3 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -43,7 +43,7 @@ class FileAttachment extends React.Component { if (type === 'image') { var self = this; // Need this reference since we use the given "this" - $('').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) { + $('').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) { return function loader() { $(this).remove(); if (name in self.refs) { @@ -114,7 +114,7 @@ class FileAttachment extends React.Component { var re3 = new RegExp('\\)', 'g'); var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); - $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')'); + $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)'); } } removeBackgroundImage(name) { diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx deleted file mode 100644 index 3ff9787ad..000000000 --- a/web/react/components/find_team.jsx +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as utils from '../utils/utils.jsx'; -import * as client from '../utils/client.jsx'; - -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; - -var holders = defineMessages({ - submitError: { - id: 'find_team.submitError', - defaultMessage: 'Please enter a valid email address' - }, - placeholder: { - id: 'find_team.placeholder', - defaultMessage: 'you@domain.com' - } -}); - -class FindTeam extends React.Component { - constructor(props) { - super(props); - this.state = {}; - - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - - var state = { }; - - var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase(); - if (!email || !utils.isEmail(email)) { - state.email_error = this.props.intl.formatMessage(holders.submitError); - this.setState(state); - return; - } - - state.email_error = ''; - - client.findTeamsSendEmail(email, - function success() { - state.sent = true; - this.setState(state); - }.bind(this), - function fail(err) { - state.email_error = err.message; - this.setState(state); - }.bind(this) - ); - } - - render() { - var emailError = null; - var emailErrorClass = 'form-group'; - - if (this.state.email_error) { - emailError = ; - emailErrorClass = 'form-group has-error'; - } - - if (this.state.sent) { - return ( -
-

- -

-

- -

-
- ); - } - - return ( -
-

- -

-
-

- -

-
- -
- - {emailError} -
-
- -
-
- ); - } -} - -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 ( +
+ +
+ + + {this.props.sidebar} + {this.props.center} + + + + + + + + + + + + +
+
+ ); + } +} + +LoggedIn.defaultProps = { +}; + +LoggedIn.propTypes = { + children: React.PropTypes.object, + sidebar: React.PropTypes.object, + center: React.PropTypes.object, + params: React.PropTypes.object +}; diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 581b8e0b5..30c8ffe4f 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -6,82 +6,118 @@ import LoginUsername from './login_username.jsx'; import LoginLdap from './login_ldap.jsx'; import * as Utils from '../utils/utils.jsx'; +import * as Client from '../utils/client.jsx'; import Constants from '../utils/constants.jsx'; +import TeamStore from '../stores/team_store.jsx'; import {FormattedMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; export default class Login extends React.Component { constructor(props) { super(props); - this.state = {}; + this.getStateFromStores = this.getStateFromStores.bind(this); + this.onTeamChange = this.onTeamChange.bind(this); + + this.state = this.getStateFromStores(); + } + componentDidMount() { + TeamStore.addChangeListener(this.onTeamChange); + Client.getMeLoggedIn((data) => { + if (data && data.logged_in !== 'false') { + browserHistory.push('/' + this.props.params.team + '/channels/town-square'); + } + }); + } + componentWillUnmount() { + TeamStore.removeChangeListener(this.onTeamChange); + } + getStateFromStores() { + return { + currentTeam: TeamStore.getByName(this.props.params.team) + }; + } + onTeamChange() { + this.setState(this.getStateFromStores()); } render() { - const teamDisplayName = this.props.teamDisplayName; - const teamName = this.props.teamName; + const currentTeam = this.state.currentTeam; + if (currentTeam == null) { + return
; + } + + const teamDisplayName = currentTeam.display_name; + const teamName = currentTeam.name; let loginMessage = []; if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { loginMessage.push( - - - - - - + + + + + + ); } if (global.window.mm_config.EnableSignUpWithGoogle === 'true') { loginMessage.push( - - - - - - - ); + + + + + + + ); } const extraParam = Utils.getUrlParameter('extra'); let extraBox = ''; if (extraParam) { - let msg; if (extraParam === Constants.SIGNIN_CHANGE) { - msg = ( - + extraBox = ( +
+ + +
); } else if (extraParam === Constants.SIGNIN_VERIFIED) { - msg = ( - - ); - } - - if (msg != null) { extraBox = (
- {msg} + +
+ ); + } else if (extraParam === Constants.SESSION_EXPIRED) { + extraBox = ( +
+ +
); } @@ -91,7 +127,7 @@ export default class Login extends React.Component { if (global.window.mm_config.EnableSignInWithEmail === 'true') { emailSignup = ( ); } @@ -125,7 +161,7 @@ export default class Login extends React.Component { } let userSignUp = null; - if (this.props.inviteId) { + if (currentTeam.allow_open_invite) { userSignUp = (
@@ -134,7 +170,7 @@ export default class Login extends React.Component { defaultMessage="Don't have an account? " /> ); } - let findTeams = null; - if (!Utils.isMobileApp()) { - findTeams = ( - - ); - } - let usernameLogin = null; if (global.window.mm_config.EnableSignInWithUsername === 'true') { usernameLogin = ( ); } return ( -
-
- -
-

{teamDisplayName}

-

- -

- {extraBox} - {loginMessage} - {emailSignup} - {usernameLogin} - {ldapLogin} - {userSignUp} - {findTeams} - {forgotPassword} - {teamSignUp} +
+ +
+
+
+ +
+

{teamDisplayName}

+

+ +

+ {extraBox} + {loginMessage} + {emailSignup} + {usernameLogin} + {ldapLogin} + {userSignUp} + {forgotPassword} + {teamSignUp} +
+
); } } 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 { { $('.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 {
  • 1) { - teams.push( -
  • -
  • - ); - - this.state.teams.forEach((team) => { - if (team.name !== this.props.teamName) { - teams.push( -
  • - -
  • ); - } - }); - } - if (global.window.mm_config.EnableTeamCreation === 'true') { teams.push(
  • @@ -283,15 +225,12 @@ export default class NavbarDropdown extends React.Component { {inviteLink} {teamLink}
  • - + - +
  • {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 ( +
    +
    + {this.props.children} +
    +
    +
    +
    +
    + {global.window.mm_config.SiteName} +
    +
    + {'© 2015 Mattermost, Inc.'} + + + + + + + + + + + + +
    +
    +
    +
    + ); + } +} + +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 ( - - ); - } - - return ( - - ); - } -} - -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: ( + + ) + }); 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 = (

    ); - } - var error = null; if (this.state.error) { - error =
    ; + error = ( +
    + +
    + ); } var formClass = 'form-group'; @@ -84,7 +70,6 @@ class PasswordResetForm extends React.Component { formClass += ' has-error'; } - const {formatMessage} = this.props.intl; return (
    @@ -98,9 +83,8 @@ class PasswordResetForm extends React.Component {

    @@ -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' />

    @@ -125,7 +112,6 @@ class PasswordResetForm extends React.Component { defaultMessage='Change my password' /> - {updateText}
    @@ -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: '

    A password reset link has been sent to {email} for your {teamDisplayName} team on {hostname}.

    ' - }, - 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: ( + + ) + }); 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: ( +
    + + +
    + ) + }); + $(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 = ( -
    -
    - ); - } - var error = null; if (this.state.error) { error =
    ; @@ -83,51 +80,60 @@ class PasswordResetSendLink extends React.Component { formClass += ' has-error'; } - const {formatMessage} = this.props.intl; return ( -
    - +
    +
    +

    -

    -
    - -
    - {error} - - +

    + +

    +
    + +
    + {error} + + +

    ); @@ -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}`} />
    ); 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 = ; + let userProfile = ; let botIndicator; if (post.props && post.props.from_webhook) { if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { userProfile = ( @@ -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} /> @@ -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( -
  • - - - -
  • +
  • + + + +
  • ); } @@ -93,7 +92,7 @@ export default class PostInfo extends React.Component { EventHelpers.showDeletePostModal(post, dataComments)} + onClick={() => GlobalActions.showDeletePostModal(post, dataComments)} > EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func + onClick={() => GlobalActions.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func displayNameType={this.state.displayNameType} user={profile} + currentUser={this.props.currentUser} /> ); @@ -525,7 +525,7 @@ PostsView.defaultProps = { PostsView.propTypes = { isActive: React.PropTypes.bool, postList: React.PropTypes.object, - profiles: React.PropTypes.object, + profiles: React.PropTypes.object.isRequired, scrollPostId: React.PropTypes.string, scrollType: React.PropTypes.number, postViewScrolled: React.PropTypes.func.isRequired, @@ -535,7 +535,8 @@ PostsView.propTypes = { showMoreMessagesBottom: React.PropTypes.bool, introText: React.PropTypes.element, messageSeparatorTime: React.PropTypes.number, - postsToHighlight: React.PropTypes.object + postsToHighlight: React.PropTypes.object, + currentUser: React.PropTypes.object.isRequired }; function FloatingTimestamp({isScrolling, post}) { diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx index 976e03fab..b361779d2 100644 --- a/web/react/components/posts_view_container.jsx +++ b/web/react/components/posts_view_container.jsx @@ -6,9 +6,10 @@ import LoadingScreen from './loading_screen.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import PostStore from '../stores/post_store.jsx'; +import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; @@ -21,6 +22,7 @@ export default class PostsViewContainer extends React.Component { this.onChannelChange = this.onChannelChange.bind(this); this.onChannelLeave = this.onChannelLeave.bind(this); this.onPostsChange = this.onPostsChange.bind(this); + this.onUserChange = this.onUserChange.bind(this); this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this); this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this); @@ -28,7 +30,8 @@ export default class PostsViewContainer extends React.Component { const currentChannelId = ChannelStore.getCurrentId(); const state = { scrollType: PostsView.SCROLL_TYPE_BOTTOM, - scrollPost: null + scrollPost: null, + currentUser: UserStore.getCurrentUser() }; if (currentChannelId) { Object.assign(state, { @@ -54,12 +57,17 @@ export default class PostsViewContainer extends React.Component { ChannelStore.addLeaveListener(this.onChannelLeave); PostStore.addChangeListener(this.onPostsChange); PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest); + UserStore.addChangeListener(this.onUserChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChannelChange); ChannelStore.removeLeaveListener(this.onChannelLeave); PostStore.removeChangeListener(this.onPostsChange); PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest); + UserStore.removeChangeListener(this.onUserChange); + } + onUserChange() { + this.setState({currentUser: UserStore.getCurrentUser()}); } handlePostsViewJumpRequest(type, post) { switch (type) { @@ -139,7 +147,7 @@ export default class PostsViewContainer extends React.Component { return PostStore.getVisiblePosts(id); } loadMorePostsTop() { - EventHelpers.emitLoadMorePostsEvent(); + GlobalActions.emitLoadMorePostsEvent(); } handlePostsViewScroll(atBottom) { if (atBottom) { @@ -165,6 +173,10 @@ export default class PostsViewContainer extends React.Component { const currentChannelId = channels[this.state.currentChannelIndex]; const channel = ChannelStore.get(currentChannelId); + if (!this.state.currentUser || !channel) { + return null; + } + const postListCtls = []; for (let i = 0; i < channels.length; i++) { const isActive = (channels[i] === currentChannelId); @@ -185,6 +197,7 @@ export default class PostsViewContainer extends React.Component { introText={channel ? createChannelIntroMessage(channel) : null} messageSeparatorTime={this.state.currentLastViewed} profiles={this.props.profiles} + currentUser={this.state.currentUser} /> ); if (!postLists[i] && isActive) { diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index 9588809eb..9183b761f 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -14,7 +14,7 @@ import * as AsyncClient from '../utils/async_client.jsx'; var ActionTypes = Constants.ActionTypes; import * as TextFormatting from '../utils/text_formatting.jsx'; import twemoji from 'twemoji'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl'; @@ -70,7 +70,7 @@ class RhsComment extends React.Component { } handlePermalink(e) { e.preventDefault(); - EventHelpers.showGetPostLinkModal(this.props.post); + GlobalActions.showGetPostLinkModal(this.props.post); } componentDidMount() { this.parseEmojis(); @@ -151,7 +151,7 @@ class RhsComment extends React.Component { EventHelpers.showDeletePostModal(post, 0)} + onClick={() => GlobalActions.showDeletePostModal(post, 0)} >
    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 { EventHelpers.showDeletePostModal(post, this.props.commentCount)} + onClick={() => GlobalActions.showDeletePostModal(post, this.props.commentCount)} > { + 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; + } + + return ( + + {this.props.children} + + ); + } +} +Root.defaultProps = { +}; + +Root.propTypes = { + children: React.PropTypes.object +}; diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 5ab864b7c..3a091bdd1 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -3,8 +3,7 @@ import UserStore from '../stores/user_store.jsx'; import UserProfile from './user_profile.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; -import * as utils from '../utils/utils.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import * as TextFormatting from '../utils/text_formatting.jsx'; import Constants from '../utils/constants.jsx'; @@ -22,7 +21,7 @@ export default class SearchResultsItem extends React.Component { handleClick(e) { e.preventDefault(); - EventHelpers.emitPostFocusEvent(this.props.post.id); + GlobalActions.emitPostFocusEvent(this.props.post.id); if ($(window).width() < 768) { $('.sidebar--right').removeClass('move--left'); @@ -32,7 +31,7 @@ export default class SearchResultsItem extends React.Component { handleFocusRHSClick(e) { e.preventDefault(); - EventHelpers.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch); + GlobalActions.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch); } render() { @@ -78,7 +77,7 @@ export default class SearchResultsItem extends React.Component {
    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 = ( +
    +
    +

    + + +

    +
    + ); + } + + if (this.state.resendStatus === 'failure') { + resendConfirm = ( +
    +
    +

    + + +

    +
    + ); + } + + return ( +
    + +
    +
    +

    + +

    +
    +

    + +

    + + {resendConfirm} +
    +
    +
    +
    + ); + } +} + +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 (
    ); + } + 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 ( -
    + ); @@ -114,7 +119,7 @@ UserProfile.defaultProps = { disablePopover: false }; UserProfile.propTypes = { - user: React.PropTypes.object.isRequired, + user: React.PropTypes.object, overwriteName: React.PropTypes.string, overwriteImage: React.PropTypes.string, disablePopover: React.PropTypes.bool diff --git a/web/react/components/user_settings/manage_languages.jsx b/web/react/components/user_settings/manage_languages.jsx index 2d1c74717..6b00a65c7 100644 --- a/web/react/components/user_settings/manage_languages.jsx +++ b/web/react/components/user_settings/manage_languages.jsx @@ -5,6 +5,7 @@ import SettingItemMax from '../setting_item_max.jsx'; import * as Client from '../../utils/client.jsx'; import * as Utils from '../../utils/utils.jsx'; +import * as GlobalActions from '../../action_creators/global_actions.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -41,7 +42,7 @@ export default class ManageLanguage extends React.Component { submitUser(user) { Client.updateUser(user, () => { - window.location.reload(true); + GlobalActions.newLocalizationSelected(user.locale); }, (err) => { let serverError; diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx index 0acfd4a16..1dd564c8d 100644 --- a/web/react/components/user_settings/user_settings_developer.jsx +++ b/web/react/components/user_settings/user_settings_developer.jsx @@ -3,7 +3,7 @@ import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; -import * as EventHelpers from '../../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../../action_creators/global_actions.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; @@ -28,7 +28,7 @@ class DeveloperTab extends React.Component { } register() { this.props.closeModal(); - EventHelpers.showRegisterAppModal(); + GlobalActions.showRegisterAppModal(); } render() { var appSection; diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index b0b1c414e..235892819 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -13,7 +13,7 @@ import Constants from '../../utils/constants.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; import * as Utils from '../../utils/utils.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl'; const holders = defineMessages({ usernameReserved: { @@ -712,7 +712,7 @@ class UserSettingsGeneralTab extends React.Component { + ) }); } 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 (
    ); + } + 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: ( + + ), + time: ( + + ) }); updateSectionStatus = function updateSection() { @@ -251,7 +264,7 @@ class SecurityTab extends React.Component {
    { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_FOCUSED_POST, - postId, - post_list: data - }); - - AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); - AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); - } - ); -} - -export function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) { - Client.getPost( - post.channel_id, - post.id, - (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POSTS, - id: post.channel_id, - numRequested: 0, - post_list: data - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POST_SELECTED, - postId: Utils.getRootId(post), - from_search: SearchStore.getSearchTerm() - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_SEARCH, - results: null, - is_mention_search: isMentionSearch - }); - }, - (err) => { - AsyncClient.dispatchError(err, 'getPost'); - } - ); -} - -export function emitLoadMorePostsEvent() { - const id = ChannelStore.getCurrentId(); - loadMorePostsTop(id); -} - -export function emitLoadMorePostsFocusedTopEvent() { - const id = PostStore.getFocusedPostId(); - loadMorePostsTop(id); -} - -export function loadMorePostsTop(id) { - const earliestPostId = PostStore.getEarliestPost(id).id; - if (PostStore.requestVisibilityIncrease(id, Constants.POST_CHUNK_SIZE)) { - AsyncClient.getPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE); - } -} - -export function emitLoadMorePostsFocusedBottomEvent() { - const id = PostStore.getFocusedPostId(); - const latestPostId = PostStore.getLatestPost(id).id; - AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE); -} - -export function emitPostRecievedEvent(post) { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POST, - post - }); -} - -export function emitUserPostedEvent(post) { - AppDispatcher.handleServerAction({ - type: ActionTypes.CREATE_POST, - post - }); -} - -export function emitPostDeletedEvent(post) { - AppDispatcher.handleServerAction({ - type: ActionTypes.POST_DELETED, - post - }); -} - -export function showDeletePostModal(post, commentCount = 0) { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_DELETE_POST_MODAL, - value: true, - post, - commentCount - }); -} - -export function showGetPostLinkModal(post) { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_GET_POST_LINK_MODAL, - value: true, - post - }); -} - -export function showGetTeamInviteLinkModal() { - AppDispatcher.handleViewAction({ - type: Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, - value: true - }); -} - -export function showInviteMemberModal() { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, - value: true - }); -} - -export function showRegisterAppModal() { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_REGISTER_APP_MODAL, - value: true - }); -} - -export function emitSuggestionPretextChanged(suggestionId, pretext) { - AppDispatcher.handleViewAction({ - type: ActionTypes.SUGGESTION_PRETEXT_CHANGED, - id: suggestionId, - pretext - }); -} - -export function emitSelectNextSuggestion(suggestionId) { - AppDispatcher.handleViewAction({ - type: ActionTypes.SUGGESTION_SELECT_NEXT, - id: suggestionId - }); -} - -export function emitSelectPreviousSuggestion(suggestionId) { - AppDispatcher.handleViewAction({ - type: ActionTypes.SUGGESTION_SELECT_PREVIOUS, - id: suggestionId - }); -} - -export function emitCompleteWordSuggestion(suggestionId, term = '') { - AppDispatcher.handleViewAction({ - type: Constants.ActionTypes.SUGGESTION_COMPLETE_WORD, - id: suggestionId, - term - }); -} - -export function emitClearSuggestions(suggestionId) { - AppDispatcher.handleViewAction({ - type: Constants.ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS, - id: suggestionId - }); -} - -export function emitPreferenceChangedEvent(preference) { - AppDispatcher.handleServerAction({ - type: Constants.ActionTypes.RECEIVED_PREFERENCE, - preference - }); -} - -export function emitRemovePost(post) { - AppDispatcher.handleViewAction({ - type: Constants.ActionTypes.REMOVE_POST, - post - }); -} - -export function sendEphemeralPost(message, channelId) { - const timestamp = Utils.getTimestamp(); - const post = { - id: Utils.generateId(), - user_id: '0', - channel_id: channelId || ChannelStore.getCurrentId(), - message, - type: Constants.POST_TYPE_EPHEMERAL, - create_at: timestamp, - update_at: timestamp, - filenames: [], - props: {} - }; - - emitPostRecievedEvent(post); -} 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
    ; - } - - return ( - -
    - - - -
    -
    - ); - } -} - -global.window.setup_admin_console_page = function setup(props) { - ReactDOM.render( - , - 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
    ; - } - - return ( - -
    - - - - - - - - - - - - - -
    -
    - ); - } -} - -global.window.setup_channel_page = function setup(props, team, channel) { - if (props.PostId === '') { - EventHelpers.emitChannelClickEvent(channel); - } else { - EventHelpers.emitPostFocusEvent(props.PostId); - } - - ReactDOM.render( - , - 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
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_claim_account_page = function setup(props) { - ReactDOM.render( - , - 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
    ; - } - - return ( - - - - ); - } -} - -global.window.mm_user = global.window.mm_user || {}; - -global.window.setup_documentation_page = function setup(props) { - ReactDOM.render( - , - 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
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_find_team_page = function setup(props) { - ReactDOM.render( - , - 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
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_login_page = function setup(props) { - ReactDOM.render( - , - 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
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_password_reset_page = function setup(props) { - ReactDOM.render( - , - 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(( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + 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
    ; - } - - return ( - - - - ); - } -} - -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( - , - 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
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_team_complete_page = function setup(props) { - ReactDOM.render( - , - 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
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_team_confirm_page = function setup(props) { - ReactDOM.render( - , - 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
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_user_complete_page = function setup(props) { - ReactDOM.render( - , - 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
    ; - } - - return ( - - - - ); - } -} - -global.window.setupVerifyPage = function setup(props) { - ReactDOM.render( - , - document.getElementById('verify') - ); -}; diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx index 5c911e94b..9f7f6e7ff 100644 --- a/web/react/stores/admin_store.jsx +++ b/web/react/stores/admin_store.jsx @@ -121,7 +121,11 @@ class AdminStoreClass extends EventEmitter { } getSelectedTeams() { - return BrowserStore.getItem('seleted_teams'); + const result = BrowserStore.getItem('seleted_teams'); + if (!result) { + return {}; + } + return result; } saveSelectedTeams(teams) { @@ -156,7 +160,3 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => { }); export default AdminStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.AdminStore = AdminStore; -} diff --git a/web/react/stores/analytics_store.jsx b/web/react/stores/analytics_store.jsx index 0ad989206..ec827f6d7 100644 --- a/web/react/stores/analytics_store.jsx +++ b/web/react/stores/analytics_store.jsx @@ -83,7 +83,3 @@ AnalyticsStore.dispatchToken = AppDispatcher.register((payload) => { }); export default AnalyticsStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.AnalyticsStore = AnalyticsStore; -} diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index 3417faaaf..3b35916b3 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -4,8 +4,8 @@ import {generateId} from '../utils/utils.jsx'; function getPrefix() { - if (global.window.mm_user) { - return global.window.mm_user.id + '_'; + if (global.window.mm_current_user_id) { + return global.window.mm_current_user_id + '_'; } return 'unknown_'; @@ -31,7 +31,9 @@ class BrowserStoreClass { this.isSignallingLogout = this.isSignallingLogout.bind(this); this.signalLogin = this.signalLogin.bind(this); this.isSignallingLogin = this.isSignallingLogin.bind(this); + } + checkVersion() { var currentVersion = sessionStorage.getItem('storage_version'); if (currentVersion !== global.window.mm_config.Version) { sessionStorage.clear(); diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index eac24b071..60cb10de7 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -350,7 +350,3 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { }); export default ChannelStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.ChannelStore = ChannelStore; -} diff --git a/web/react/stores/file_store.jsx b/web/react/stores/file_store.jsx index c1fd0ef74..6d7e0f354 100644 --- a/web/react/stores/file_store.jsx +++ b/web/react/stores/file_store.jsx @@ -57,9 +57,4 @@ class FileStore extends EventEmitter { } } -const instance = new FileStore(); -export default instance; - -if (window.mm_config.EnableDeveloper === 'true') { - window.FileStore = instance; -} +export default new FileStore(); diff --git a/web/react/stores/localization_store.jsx b/web/react/stores/localization_store.jsx new file mode 100644 index 000000000..0e3a63724 --- /dev/null +++ b/web/react/stores/localization_store.jsx @@ -0,0 +1,60 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'change'; + +class LocalizationStoreClass extends EventEmitter { + constructor() { + super(); + + this.currentLocale = 'en'; + this.currentTranslations = null; + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + setCurrentLocale(locale, translations) { + this.currentLocale = locale; + this.currentTranslations = translations; + } + + getLocale() { + return this.currentLocale; + } + + getTranslations() { + return this.currentTranslations; + } +} + +var LocalizationStore = new LocalizationStoreClass(); +LocalizationStore.setMaxListeners(0); + +LocalizationStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_LOCALE: + LocalizationStore.setCurrentLocale(action.locale, action.translations); + LocalizationStore.emitChange(); + break; + default: + } +}); + +export default LocalizationStore; diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx index 1819fffc0..5ea38030b 100644 --- a/web/react/stores/modal_store.jsx +++ b/web/react/stores/modal_store.jsx @@ -45,7 +45,3 @@ class ModalStoreClass extends EventEmitter { const ModalStore = new ModalStoreClass(); export default ModalStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.ModalStore = ModalStore; -} diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 5cc3f300d..a6dfcd46f 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -608,7 +608,3 @@ function isPostListNull(pl) { return false; } - -if (window.mm_config.EnableDeveloper === 'true') { - window.PostStore = PostStore; -} diff --git a/web/react/stores/search_store.jsx b/web/react/stores/search_store.jsx index 96071665c..549f355ef 100644 --- a/web/react/stores/search_store.jsx +++ b/web/react/stores/search_store.jsx @@ -135,7 +135,3 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => { }); export default SearchStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.SearchStore = SearchStore; -} diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 9b2b049b7..ad24a04cd 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -10,7 +10,7 @@ import EventEmitter from 'events'; import * as Utils from '../utils/utils.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; const SocketEvents = Constants.SocketEvents; @@ -42,10 +42,6 @@ class SocketStoreClass extends EventEmitter { return; } - if (!global.window.hasOwnProperty('mm_session_token_index')) { - return; - } - this.setMaxListeners(0); if (window.WebSocket && !conn) { @@ -54,7 +50,7 @@ class SocketStoreClass extends EventEmitter { protocol = 'wss://'; } - var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket?' + Utils.getSessionIndex(); + var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket'; if (this.failCount === 0) { console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console @@ -204,7 +200,7 @@ class SocketStoreClass extends EventEmitter { function handleNewPostEvent(msg, translations) { // Store post const post = JSON.parse(msg.props.post); - EventHelpers.emitPostRecievedEvent(post); + GlobalActions.emitPostRecievedEvent(post); // Update channel state if (ChannelStore.getCurrentId() === msg.channel_id) { @@ -291,7 +287,7 @@ function handlePostEditEvent(msg) { function handlePostDeleteEvent(msg) { const post = JSON.parse(msg.props.post); - EventHelpers.emitPostDeletedEvent(post); + GlobalActions.emitPostDeletedEvent(post); } function handleNewUserEvent() { @@ -337,7 +333,7 @@ function handleChannelViewedEvent(msg) { function handlePreferenceChangedEvent(msg) { const preference = JSON.parse(msg.props.preference); - EventHelpers.emitPreferenceChangedEvent(preference); + GlobalActions.emitPreferenceChangedEvent(preference); } var SocketStore = new SocketStoreClass(); diff --git a/web/react/stores/suggestion_store.jsx b/web/react/stores/suggestion_store.jsx index 487bae843..efd2b76ed 100644 --- a/web/react/stores/suggestion_store.jsx +++ b/web/react/stores/suggestion_store.jsx @@ -258,9 +258,4 @@ class SuggestionStore extends EventEmitter { } } -const instance = new SuggestionStore(); -export default instance; - -if (window.mm_config.EnableDeveloper === 'true') { - window.SuggestionStore = instance; -} +export default new SuggestionStore(); diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx index 493d6bc4d..354a07b72 100644 --- a/web/react/stores/team_store.jsx +++ b/web/react/stores/team_store.jsx @@ -6,7 +6,6 @@ import EventEmitter from 'events'; import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; -import BrowserStore from '../stores/browser_store.jsx'; const CHANGE_EVENT = 'change'; @@ -33,6 +32,9 @@ class TeamStoreClass extends EventEmitter { this.getCurrentTeamUrl = this.getCurrentTeamUrl.bind(this); this.getCurrentInviteLink = this.getCurrentInviteLink.bind(this); this.saveTeam = this.saveTeam.bind(this); + + this.teams = {}; + this.currentTeamId = ''; } emitChange() { @@ -65,11 +67,11 @@ class TeamStoreClass extends EventEmitter { } getAll() { - return BrowserStore.getItem('user_teams', {}); + return this.teams; } getCurrentId() { - var team = global.window.mm_team; + var team = this.get(this.currentTeamId); if (team) { return team.id; @@ -79,11 +81,13 @@ class TeamStoreClass extends EventEmitter { } getCurrent() { - if (global.window.mm_team != null && this.get(global.window.mm_team.id) == null) { - this.saveTeam(global.window.mm_team); + const team = this.teams[this.currentTeamId]; + + if (team) { + return team; } - return global.window.mm_team; + return null; } getCurrentTeamUrl() { @@ -104,9 +108,16 @@ class TeamStoreClass extends EventEmitter { } saveTeam(team) { - var teams = this.getAll(); - teams[team.id] = team; - BrowserStore.setItem('user_teams', teams); + this.teams[team.id] = team; + } + + saveTeams(teams) { + this.teams = teams; + } + + saveMyTeam(team) { + this.saveTeam(team); + this.currentTeamId = team.id; } } @@ -116,17 +127,16 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; switch (action.type) { - case ActionTypes.RECEIVED_TEAM: - TeamStore.saveTeam(action.team); + case ActionTypes.RECEIVED_MY_TEAM: + TeamStore.saveMyTeam(action.team); + TeamStore.emitChange(); + break; + case ActionTypes.RECEIVED_ALL_TEAMS: + TeamStore.saveTeams(action.teams); TeamStore.emitChange(); break; - default: } }); export default TeamStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.TeamStore = TeamStore; -} diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index 9fcd2440e..c1e5c75dc 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -11,13 +11,13 @@ import BrowserStore from './browser_store.jsx'; const CHANGE_EVENT = 'change'; const CHANGE_EVENT_SESSIONS = 'change_sessions'; const CHANGE_EVENT_AUDITS = 'change_audits'; -const CHANGE_EVENT_TEAMS = 'change_teams'; const CHANGE_EVENT_STATUSES = 'change_statuses'; class UserStoreClass extends EventEmitter { constructor() { super(); this.profileCache = null; + this.currentUserId = ''; } emitChange(userId) { @@ -56,18 +56,6 @@ class UserStoreClass extends EventEmitter { this.removeListener(CHANGE_EVENT_AUDITS, callback); } - emitTeamsChange() { - this.emit(CHANGE_EVENT_TEAMS); - } - - addTeamsChangeListener(callback) { - this.on(CHANGE_EVENT_TEAMS, callback); - } - - removeTeamsChangeListener(callback) { - this.removeListener(CHANGE_EVENT_TEAMS, callback); - } - emitStatusesChange() { this.emit(CHANGE_EVENT_STATUSES); } @@ -81,26 +69,17 @@ class UserStoreClass extends EventEmitter { } getCurrentUser() { - if (this.getProfiles()[global.window.mm_user.id] == null) { - this.saveProfile(global.window.mm_user); - } - - return global.window.mm_user; + return this.getProfiles()[this.currentUserId]; } setCurrentUser(user) { - var oldUser = global.window.mm_user; - - if (oldUser.id === user.id) { - global.window.mm_user = user; - this.saveProfile(user); - } else { - throw new Error('Problem with setCurrentUser old_user_id=' + oldUser.id + ' new_user_id=' + user.id); - } + this.saveProfile(user); + this.currentUserId = user.id; + global.window.mm_current_user_id = this.currentUserId; } getCurrentId() { - var user = global.window.mm_user; + var user = this.getCurrentUser(); if (user) { return user.id; @@ -200,11 +179,22 @@ class UserStoreClass extends EventEmitter { saveProfiles(profiles) { const currentId = this.getCurrentId(); - if (currentId in profiles) { - delete profiles[currentId]; + if (this.profileCache) { + const currentUser = this.profileCache[currentId]; + if (currentUser) { + if (currentId in profiles) { + delete profiles[currentId]; + } + + this.profileCache = profiles; + this.profileCache[currentId] = currentUser; + } else { + this.profileCache = profiles; + } + } else { + this.profileCache = profiles; } - this.profileCache = profiles; BrowserStore.setItem('profiles', profiles); } @@ -224,14 +214,6 @@ class UserStoreClass extends EventEmitter { return BrowserStore.getItem('audits', {loading: true}); } - setTeams(teams) { - BrowserStore.setItem('teams', teams); - } - - getTeams() { - return BrowserStore.getItem('teams', []); - } - getCurrentMentionKeys() { return this.getMentionKeys(this.getCurrentId()); } @@ -312,10 +294,6 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { UserStore.setAudits(action.audits); UserStore.emitAuditsChange(); break; - case ActionTypes.RECEIVED_TEAMS: - UserStore.setTeams(action.teams); - UserStore.emitTeamsChange(); - break; case ActionTypes.RECEIVED_STATUSES: UserStore.pSetStatuses(action.statuses); UserStore.emitStatusesChange(); @@ -325,7 +303,3 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { }); export {UserStore as default}; - -if (window.mm_config.EnableDeveloper === 'true') { - window.UserStore = UserStore; -} diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index 7d5e1bd0f..b9770a6e9 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import * as client from './client.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import BrowserStore from '../stores/browser_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; @@ -44,15 +45,19 @@ function isCallInProgress(callName) { export function getChannels(checkVersion) { if (isCallInProgress('getChannels')) { - return; + return null; } callTracker.getChannels = utils.getTimestamp(); - client.getChannels( + return client.getChannels( (data, textStatus, xhr) => { callTracker.getChannels = 0; + if (xhr.status === 304 || !data) { + return; + } + if (checkVersion) { var serverVersion = xhr.getResponseHeader('X-Version-ID'); @@ -67,10 +72,6 @@ export function getChannels(checkVersion) { } } - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_CHANNELS, channels: data.channels, @@ -392,36 +393,6 @@ export function getAllTeams() { ); } -export function findTeams(email) { - if (isCallInProgress('findTeams_' + email)) { - return; - } - - var user = UserStore.getCurrentUser(); - if (user) { - callTracker['findTeams_' + email] = utils.getTimestamp(); - client.findTeams( - user.email, - function findTeamsSuccess(data, textStatus, xhr) { - callTracker['findTeams_' + email] = 0; - - if (xhr.status === 304 || !data) { - return; - } - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_TEAMS, - teams: data - }); - }, - function findTeamsFailure(err) { - callTracker['findTeams_' + email] = 0; - dispatchError(err, 'findTeams'); - } - ); - } -} - export function search(terms) { if (isCallInProgress('search_' + String(terms))) { return; @@ -645,11 +616,11 @@ export function getPostsAfter(postId, offset, numPost) { export function getMe() { if (isCallInProgress('getMe')) { - return; + return null; } callTracker.getMe = utils.getTimestamp(); - client.getMe( + return client.getMe( (data, textStatus, xhr) => { callTracker.getMe = 0; @@ -661,6 +632,8 @@ export function getMe() { type: ActionTypes.RECEIVED_ME, me: data }); + + GlobalActions.newLocalizationSelected(data.locale); }, (err) => { callTracker.getMe = 0; @@ -706,11 +679,11 @@ export function getStatuses() { export function getMyTeam() { if (isCallInProgress('getMyTeam')) { - return; + return null; } callTracker.getMyTeam = utils.getTimestamp(); - client.getMyTeam( + return client.getMyTeam( function getMyTeamSuccess(data, textStatus, xhr) { callTracker.getMyTeam = 0; @@ -719,7 +692,7 @@ export function getMyTeam() { } AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_TEAM, + type: ActionTypes.RECEIVED_MY_TEAM, team: data }); }, diff --git a/web/react/utils/channel_intro_messages.jsx b/web/react/utils/channel_intro_messages.jsx index ed94f94b8..94f3f0ce0 100644 --- a/web/react/utils/channel_intro_messages.jsx +++ b/web/react/utils/channel_intro_messages.jsx @@ -8,8 +8,7 @@ import ToggleModalButton from '../components/toggle_modal_button.jsx'; import UserProfile from '../components/user_profile.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import Constants from '../utils/constants.jsx'; -import TeamStore from '../stores/team_store.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import {FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'mm-intl'; @@ -40,7 +39,7 @@ export function createDMIntroMessage(channel) {
    @@ -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 = ( - - - - - ); - } else { - inviteModalLink = ( - - - - - ); - } + const inviteModalLink = ( + + + + + ); return (
    diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 76d42137a..e00f28a14 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -1,8 +1,8 @@ // See License.txt for license information. import BrowserStore from '../stores/browser_store.jsx'; -import TeamStore from '../stores/team_store.jsx'; -import ErrorStore from '../stores/error_store.jsx'; + +import {browserHistory} from 'react-router'; let translations = { connectionError: 'There appears to be a problem with your internet connection.', @@ -50,10 +50,10 @@ function handleError(methodName, xhr, status, err) { if (xhr.status === 401) { if (window.location.href.indexOf('/channels') === 0) { - window.location.pathname = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); + browserHistory.push('/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); } else { - var teamURL = window.location.href.split('/channels')[0]; - window.location.href = teamURL + '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); + var teamURL = window.location.pathname.split('/channels')[0]; + browserHistory.push(teamURL + '/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); } } @@ -289,13 +289,17 @@ export function switchToEmail(data, success, error) { track('api', 'api_users_switch_to_email'); } -export function logout() { +export function logout(success, error) { track('api', 'api_users_logout'); - var currentTeamUrl = TeamStore.getCurrentTeamUrl(); - BrowserStore.signalLogout(); - BrowserStore.clear(); - ErrorStore.clearLastError(); - window.location.href = currentTeamUrl + '/logout'; + $.ajax({ + url: '/api/v1/users/logout', + type: 'POST', + success, + error: function onError(xhr, status, err) { + var e = handleError('logout', xhr, status, err); + error(e); + } + }); } export function loginByEmail(name, email, password, success, error) { @@ -437,7 +441,7 @@ export function getServerAudits(success, error) { } export function getConfig(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/admin/config', dataType: 'json', contentType: 'application/json', @@ -457,7 +461,6 @@ export function getAnalytics(name, teamId, success, error) { } else { url += teamId + '/' + name; } - $.ajax({ url, dataType: 'json', @@ -471,6 +474,34 @@ export function getAnalytics(name, teamId, success, error) { }); } +export function getClientConfig(success, error) { + return $.ajax({ + url: '/api/v1/admin/client_props', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getClientConfig', xhr, status, err); + error(e); + } + }); +} + +export function getTeamAnalytics(teamId, name, success, error) { + $.ajax({ + url: '/api/v1/admin/analytics/' + teamId + '/' + name, + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: (xhr, status, err) => { + var e = handleError('getTeamAnalytics', xhr, status, err); + error(e); + } + }); +} + export function saveConfig(config, success, error) { $.ajax({ url: '/api/v1/admin/save_config', @@ -529,6 +560,21 @@ export function getAllTeams(success, error) { }); } +export function getMeLoggedIn(success, error) { + return $.ajax({ + cache: false, + url: '/api/v1/users/me_logged_in', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getMeLoggedIn', xhr, status, err); + error(e); + } + }); +} + export function getMe(success, error) { var currentUser = null; $.ajax({ @@ -635,38 +681,6 @@ export function findTeamByName(teamName, success, error) { }); } -export function findTeamsSendEmail(email, success, error) { - $.ajax({ - url: '/api/v1/teams/email_teams', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({email: email}), - success, - error: function onError(xhr, status, err) { - var e = handleError('findTeamsSendEmail', xhr, status, err); - error(e); - } - }); - - track('api', 'api_teams_email_teams'); -} - -export function findTeams(email, success, error) { - $.ajax({ - url: '/api/v1/teams/find_teams', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({email: email}), - success, - error: function onError(xhr, status, err) { - var e = handleError('findTeams', xhr, status, err); - error(e); - } - }); -} - export function createChannel(channel, success, error) { $.ajax({ url: '/api/v1/channels/create', @@ -835,7 +849,7 @@ export function updateLastViewedAt(channelId, success, error) { } export function getChannels(success, error) { - $.ajax({ + return $.ajax({ cache: false, url: '/api/v1/channels/', dataType: 'json', @@ -901,7 +915,7 @@ export function getChannelExtraInfo(id, memberLimit, success, error) { url += '/' + memberLimit; } - $.ajax({ + return $.ajax({ url, dataType: 'json', contentType: 'application/json', @@ -1018,7 +1032,7 @@ export function getPostsPage(channelId, offset, limit, success, error, complete) } export function getPosts(channelId, since, success, error, complete) { - $.ajax({ + return $.ajax({ url: '/api/v1/channels/' + channelId + '/posts/' + since, dataType: 'json', type: 'GET', @@ -1347,7 +1361,7 @@ export function getStatuses(ids, success, error) { } export function getMyTeam(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/teams/me', dataType: 'json', type: 'GET', @@ -1437,7 +1451,7 @@ export function listIncomingHooks(success, error) { } export function getAllPreferences(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/preferences/', dataType: 'json', type: 'GET', @@ -1569,3 +1583,68 @@ export function removeLicenseFile(success, error) { track('api', 'api_license_upload'); } + +export function getClientLicenceConfig(success, error) { + return $.ajax({ + url: '/api/v1/license/client_config', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getClientLicenceConfig', xhr, status, err); + error(e); + } + }); +} + +export function getInviteInfo(success, error, id) { + $.ajax({ + url: '/api/v1/teams/get_invite_info', + type: 'POST', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify({invite_id: id}), + success, + error: function onError(xhr, status, err) { + var e = handleError('getInviteInfo', xhr, status, err); + if (error) { + error(e); + } + } + }); +} + +export function verifyEmail(success, error, uid, hid) { + $.ajax({ + url: '/api/v1/users/verify_email', + type: 'POST', + contentType: 'application/json', + dataType: 'text', + data: JSON.stringify({uid, hid}), + success, + error: function onError(xhr, status, err) { + var e = handleError('verifyEmail', xhr, status, err); + if (error) { + error(e); + } + } + }); +} + +export function resendVerification(success, error, teamName, email) { + $.ajax({ + url: '/api/v1/users/resend_verification', + type: 'POST', + contentType: 'application/json', + dataType: 'text', + data: JSON.stringify({team_name: teamName, email}), + success, + error: function onError(xhr, status, err) { + var e = handleError('resendVerification', xhr, status, err); + if (error) { + error(e); + } + } + }); +} diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index daea9f43e..2cff4dbed 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -42,13 +42,15 @@ export default { RECEIVED_MSG: null, - RECEIVED_TEAM: null, + RECEIVED_MY_TEAM: null, RECEIVED_CONFIG: null, RECEIVED_LOGS: null, RECEIVED_SERVER_AUDITS: null, RECEIVED_ALL_TEAMS: null, + RECEIVED_LOCALE: null, + SHOW_SEARCH: null, TOGGLE_IMPORT_THEME_MODAL: null, @@ -143,6 +145,7 @@ export default { EMAIL_SERVICE: 'email', SIGNIN_CHANGE: 'signin_change', SIGNIN_VERIFIED: 'verified', + SESSION_EXPIRED: 'expired', POST_CHUNK_SIZE: 60, MAX_POST_CHUNKS: 3, POST_FOCUS_CONTEXT_RADIUS: 10, diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 6942a8e08..88777164b 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -2,9 +2,10 @@ // See License.txt for license information. import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import UserStore from '../stores/user_store.jsx'; +import LocalizationStore from '../stores/localization_store.jsx'; import PreferenceStore from '../stores/preference_store.jsx'; import TeamStore from '../stores/team_store.jsx'; import Constants from '../utils/constants.jsx'; @@ -941,7 +942,7 @@ export function updateAddressBar(channelName) { } export function switchChannel(channel) { - EventHelpers.emitChannelClickEvent(channel); + GlobalActions.emitChannelClickEvent(channel); updateAddressBar(channel.name); @@ -1130,8 +1131,8 @@ export function fileSizeToString(bytes) { // Converts a filename (like those attached to Post objects) to a url that can be used to retrieve attachments from the server. export function getFileUrl(filename, isDownload) { - const downloadParam = isDownload ? '&download=1' : ''; - return getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + getSessionIndex() + downloadParam; + const downloadParam = isDownload ? '?download=1' : ''; + return getWindowLocationOrigin() + '/api/v1/files/get' + filename + downloadParam; } // Gets the name of a file (including extension) from a given url or file path. @@ -1151,14 +1152,6 @@ export function getWebsocketPort(protocol) { return ''; } -export function getSessionIndex() { - if (global.window.mm_session_token_index >= 0) { - return 'session_token_index=' + global.window.mm_session_token_index; - } - - return ''; -} - // Generates a RFC-4122 version 4 compliant globally unique identifier. export function generateId() { // implementation taken from http://stackoverflow.com/a/2117523 @@ -1405,3 +1398,19 @@ export function isPostEphemeral(post) { export function getRootId(post) { return post.root_id === '' ? post.id : post.root_id; } + +export function localizeMessage(id, defaultMessage) { + const translations = LocalizationStore.getTranslations(); + if (translations) { + const value = translations[id]; + if (value) { + return value; + } + } + + if (defaultMessage) { + return defaultMessage; + } + + return id; +} diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss index 5e7f04724..44681291c 100644 --- a/web/sass-files/sass/partials/_sidebar--left.scss +++ b/web/sass-files/sass/partials/_sidebar--left.scss @@ -24,12 +24,6 @@ padding: 1em 1em 0; display: none; } - > div { - height: 100%; - position: absolute; - padding-bottom: 70px; - width: 100%; - } .badge { background-color: $primary-color; position: absolute; diff --git a/web/static/help/Messaging_en.md b/web/static/help/Messaging_en.md deleted file mode 100644 index 2063ad41c..000000000 --- a/web/static/help/Messaging_en.md +++ /dev/null @@ -1,47 +0,0 @@ -# Messaging - -### Writing Messages - -You can write messages using the input box with the text "Write a message..." at the bottom of Mattermost. - -Press **ENTER** to send a message. Use **Shift+ENTER** to create a new line without sending a message. - -### Formatting Messages - -Mattermost messages are formatted using a standard called "markdown". Here are examples: - -| Text Entered | How it appears | -|:---------------|:---------------| -|`**bold**`| **bold** | -| `_italic_`|_italic_| -|`[hyperlink](http://mattermost.org)`|[hyperlink](http://mattermost.org)| -|`![embedded image](https://travis-ci.org/mattermost/platform.svg)`|![embedded image](https://travis-ci.org/mattermost/platform.svg)| -|`:smile:` `:sheep:` `:alien:`|:smile: :sheep: :alien:| - -Emojis provided free from [Emoji One](http://emojione.com/). Check out a full list of Emojis [here](http://emoji.codes/). - - -### Mentioning Teammates - -You can mention a teammate by using the `@` symbol plus their username to send them a special notification to draw their attention. - -For example, you might write: - -``` -@alice how did your interview go with the new candidate? -``` - -Which sends a special mention notification to **alice** to check your message. - -To mention a teammate, press `@` and you should see a list of team members who can be messaged. You can either type their username or use the **Up** and **Down** arrow keys and then **ENTER** to select them to be mentioned. - -You can configure how you'd like to be alerted about mentions of your username, your first name, your nickname, or other keywords from **Account Settings** > **Notifications** and you can set channel-specific preferences from **[Channel Name]** > **Notification Preferences** - -### Messages Dropdown Menu - -To get to the Messages Dropdown Menu, hover over a message and click on the [...] menu. This shows a dropdown list containing additional actions you can perform on a message: - -- **Reply:** Opens up the sidebar so you can reply to a message in a comment thread. -- **Permalink:** Creates a link to the message. Sharing this link with other users in the channel lets them view the linked message in the Message Archives. -- **Delete:** Deletes the message so it is no longer visible. Team Administrators and System Administrators can also delete another user's message. -- **Edit:** Lets you edit your own message. diff --git a/web/static/help/Messaging_es.md b/web/static/help/Messaging_es.md deleted file mode 100644 index d3947f36a..000000000 --- a/web/static/help/Messaging_es.md +++ /dev/null @@ -1,37 +0,0 @@ -# Mensajes - -## Escribiendo Mensajes - -Puedes escribir mensajes utilizando el cuadro de texto que dice "Escribe un mensaje..." al final de Mattermost. - -Presiona **RETORNO** para enviar un mensaje. Utiliza **Shift+RETORNO** para crear una nueva linea sin enviar el mensaje. - -## Darle formato a los Mensajes - -Los mensajes de Mattermost se les asigna formato utilizando un estándard que se llama "markdown". Aquí algunos ejemplos: - -| Texto escrito | Como aparece | -|:--------------|:-------------| -|`**negrita**`| **negrita** | -| `_italica_`|_italica_| -|`[hipervinculo](http://mattermost.org)`|[hipervinculo](http://mattermost.org)| -|`![imagen embebida](https://travis-ci.org/mattermost/platform.svg)`|![imagen embebida](https://travis-ci.org/mattermost/platform.svg)| -|`:smile:` `:sheep:` `:alien:`|:smile: :sheep: :alien:| - -Revisa la lista completa de Emojis [aquí](http://www.emoji-cheat-sheet.com/). - -## Mencionando a compañeros - -Puedes mencionar a un compañero al utilizar el simbolo `@` más el nombre de usuario para enviarles una notificación especial que llame su atención. - -Por ejemplo, podrías escribir: - -``` -@alicia como te fue con la entrevista del nuevo candidato? -``` - -Lo cual enviará una notificación especial de mención a **alicia** para que lea tu mensaje. - -Para mencionar un compañero, presiona `@` y podrás ver una lista de los miembros de equipo a quienes puedes mandarles un mensaje. Puedes escribir su nombre de usuario o utilizar las flechas de **Arriba** y **Abajo** y presionar **RETORNO** para seleccionarlos. - -Puedes configurar como te gustaría ser notificado cuando alguien te menciona por nombre de usuario, tu primer nombre, sobrenombre o cualquier otra palabra clave en **Configurar Cuenta** > **Notificaciones** y puedes asignar preferencias especificas para un canal en **[Nombre del Canal]** > **Preferencias de Notificación** diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index d2e340641..2a536925c 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -661,8 +661,10 @@ "email_signup.find": "Find my teams", "email_verify.almost": "{siteName}: You are almost done", "email_verify.notVerifiedBody": "Please verify your email address. Check your inbox for an email.", + "email_verify.verifyFailed": "Failed to verify your email.", "email_verify.resend": "Resend Email", "email_verify.sent": " Verification email sent.", + "email_verify.failed": " Failed to send verification email.", "email_verify.verified": "{siteName} Email Verified", "email_verify.verifiedBody": "

    Your email has been verified! Click here to log in.

    ", "error_bar.preview_mode": "Preview Mode: Email notifications have not been configured", @@ -758,6 +760,7 @@ "login.or": "or", "login.signTo": "Sign in to:", "login.verified": " Email Verified", + "login.session_expired": " Your session has expired. Please login again.", "login_email.badTeam": "Bad team name", "login_email.email": "Email", "login_email.emailReq": "An email is required", @@ -822,16 +825,16 @@ "navbar_dropdown.teamSettings": "Team Settings", "password_form.change": "Change my password", "password_form.click": "Click here to log in.", - "password_form.enter": "Enter a new password for your {teamDisplayName} {siteName} account.", + "password_form.enter": "Enter a new password for your {siteName} account.", "password_form.error": "Please enter at least {chars} characters.", "password_form.pwd": "Password", "password_form.title": "Password Reset", "password_form.update": "Your password has been updated successfully.", "password_send.checkInbox": "Please check your inbox.", - "password_send.description": "To reset your password, enter the email address you used to sign up for {teamName}.", + "password_send.description": "To reset your password, enter the email address you used to sign up.", "password_send.email": "Email", "password_send.error": "Please enter a valid email address.", - "password_send.link": "

    A password reset link has been sent to {email} for your {teamDisplayName} team on {hostname}.

    ", + "password_send.link": "

    A password reset link has been sent to {email}

    ", "password_send.reset": "Reset my password", "password_send.title": "Password Reset", "post_attachment.collapse": "▲ collapse text", @@ -1303,5 +1306,11 @@ "view_image.loading": "Loading ", "view_image_popover.download": "Download", "view_image_popover.file": "File {count} of {total}", - "view_image_popover.publicLink": "Get Public Link" + "view_image_popover.publicLink": "Get Public Link", + "web.footer.about": "About", + "web.footer.help": "Help", + "web.footer.privacy": "Privacy", + "web.footer.terms": "Terms", + "web.header.back": "Back", + "web.root.singup_info": "All team communication in one place, searchable and accessible anywhere" } diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index c6b16a293..f42dc879a 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -822,16 +822,16 @@ "navbar_dropdown.teamSettings": "Configurar Equipo", "password_form.change": "Cambiar mi contraseña", "password_form.click": " Pincha aquí para iniciar sesión.", - "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {teamDisplayName} {siteName}.", + "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {siteName}.", "password_form.error": "Por favor ingresa al menos {chars} caracteres.", "password_form.pwd": "Contraseña", "password_form.title": "Restablecer Contraseña", "password_form.update": "Tu contraseña ha sido actualizada satisfactoriamente.", "password_send.checkInbox": "Por favor revisa tu bandeja de entrada.", - "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte en {teamName}.", + "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte.", "password_send.email": "Correo electrónico", "password_send.error": "Por favor ingresa una dirección correo electrónico válida.", - "password_send.link": "

    Se ha enviado un enlace para restablecer la contraseña a {email} para tu equipo {teamDisplayName} en {hostname}.

    ", + "password_send.link": "

    Se ha enviado un enlace para restablecer la contraseña a {email}

    ", "password_send.reset": "Restablecer mi contraseña", "password_send.title": "Restablecer Contraseña", "post_attachment.collapse": "▲ colapsar texto", @@ -1303,5 +1303,11 @@ "view_image.loading": "Cargando ", "view_image_popover.download": "Descargar", "view_image_popover.file": "Archivo {count} de {total}", - "view_image_popover.publicLink": "Obtener Enlace Público" + "view_image_popover.publicLink": "Obtener Enlace Público", + "web.footer.about": "Acerca", + "web.footer.help": "Ayuda", + "web.footer.privacy": "Privacidad", + "web.footer.terms": "Términos", + "web.header.back": "Atrás", + "web.root.singup_info": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte" } diff --git a/web/static/i18n/pt.json b/web/static/i18n/pt.json index b9b8f4c07..d276e339a 100644 --- a/web/static/i18n/pt.json +++ b/web/static/i18n/pt.json @@ -820,16 +820,15 @@ "navbar_dropdown.teamSettings": "Configurações da Equipe", "password_form.change": "Alterar minha senha", "password_form.click": "Clique aqui para logar.", - "password_form.enter": "Entre uma nova senha para sua conta {teamDisplayName} {siteName}.", + "password_form.enter": "Entre uma nova senha para sua conta {siteName}.", "password_form.error": "Por favor, insira pelo menos {chars} caracteres.", "password_form.pwd": "Senha", "password_form.title": "Resetar Senha", "password_form.update": "Sua senha foi atualizada com sucesso.", "password_send.checkInbox": "Por favor verifique sua caixa de entrada.", - "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever em {teamName}.", + "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever.", "password_send.email": "E-mail", "password_send.error": "Por favor entre um endereço de e-mail válido.", - "password_send.link": "

    Um link para resetar a sua senha na equipe {teamDisplayName} em {hostname} foi enviado para {email}.

    ", "password_send.reset": "Resetar minha senha", "password_send.title": "Resetar Senha", "post_attachment.collapse": "▲ recolher texto", diff --git a/web/templates/admin_console.html b/web/templates/admin_console.html deleted file mode 100644 index 08c90493e..000000000 --- a/web/templates/admin_console.html +++ /dev/null @@ -1,21 +0,0 @@ - -{{define "admin_console"}} - - -{{template "head" . }} - - - -
    - - - - -{{end}} diff --git a/web/templates/authorize.html b/web/templates/authorize.html deleted file mode 100644 index 0fa36b0ab..000000000 --- a/web/templates/authorize.html +++ /dev/null @@ -1,12 +0,0 @@ -{{define "authorize"}} - -{{template "head" . }} - -
    -
    - - - -{{end}} diff --git a/web/templates/channel.html b/web/templates/channel.html deleted file mode 100644 index 94d79a022..000000000 --- a/web/templates/channel.html +++ /dev/null @@ -1,21 +0,0 @@ - -{{define "channel"}} - - -{{template "head" . }} - -
    - - - -{{end}} diff --git a/web/templates/claim_account.html b/web/templates/claim_account.html deleted file mode 100644 index 2a9126d1b..000000000 --- a/web/templates/claim_account.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "claim_account"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/docs.html b/web/templates/docs.html deleted file mode 100644 index dc18e5cb6..000000000 --- a/web/templates/docs.html +++ /dev/null @@ -1,27 +0,0 @@ -{{define "docs"}} - - -{{template "head" . }} - -
    -
    -
    - -
    -
    -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/find_team.html b/web/templates/find_team.html deleted file mode 100644 index b7e1d7eca..000000000 --- a/web/templates/find_team.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "find_team"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/footer.html b/web/templates/footer.html deleted file mode 100644 index 5b11328fb..000000000 --- a/web/templates/footer.html +++ /dev/null @@ -1,39 +0,0 @@ -{{define "footer"}} - - -{{end}} diff --git a/web/templates/head.html b/web/templates/head.html deleted file mode 100644 index 61b1aa12b..000000000 --- a/web/templates/head.html +++ /dev/null @@ -1,191 +0,0 @@ -{{define "head"}} - - - - - - {{ .Props.Title }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{{end}} diff --git a/web/templates/home.html b/web/templates/home.html deleted file mode 100644 index 08876d41d..000000000 --- a/web/templates/home.html +++ /dev/null @@ -1,24 +0,0 @@ -{{define "home"}} - - -{{template "head" . }} - -
    - - -
    -
    - -
    -
    - -
    -
    -
    -
    - - - -{{end}} diff --git a/web/templates/login.html b/web/templates/login.html deleted file mode 100644 index 88540a906..000000000 --- a/web/templates/login.html +++ /dev/null @@ -1,27 +0,0 @@ -{{define "login"}} - - -{{template "head" . }} - -
    -
    -
    - -
    -
    -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/password_reset.html b/web/templates/password_reset.html deleted file mode 100644 index e68f8b693..000000000 --- a/web/templates/password_reset.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "password_reset"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_team.html b/web/templates/signup_team.html deleted file mode 100644 index afba58066..000000000 --- a/web/templates/signup_team.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_team"}} - - -{{template "head" . }} - -
    -
    -
    -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_team_complete.html b/web/templates/signup_team_complete.html deleted file mode 100644 index 3873d8978..000000000 --- a/web/templates/signup_team_complete.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_team_complete"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_team_confirm.html b/web/templates/signup_team_confirm.html deleted file mode 100644 index 31f1ba95b..000000000 --- a/web/templates/signup_team_confirm.html +++ /dev/null @@ -1,26 +0,0 @@ -{{define "signup_team_confirm"}} - - -{{template "head" . }} - -
    -
    - - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_user_complete.html b/web/templates/signup_user_complete.html deleted file mode 100644 index 937a89dd2..000000000 --- a/web/templates/signup_user_complete.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_user_complete"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/verify.html b/web/templates/verify.html deleted file mode 100644 index 2e5496d7a..000000000 --- a/web/templates/verify.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "verify"}} - - - {{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/web.go b/web/web.go index 09450b976..2a44ece00 100644 --- a/web/web.go +++ b/web/web.go @@ -4,67 +4,16 @@ package web import ( - "fmt" + "net/http" + "strings" + l4g "github.com/alecthomas/log4go" - "github.com/gorilla/mux" "github.com/mattermost/platform/api" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "github.com/mssola/user_agent" - "gopkg.in/fsnotify.v1" - "html/template" - "net/http" - "net/url" - "strconv" - "strings" ) -var Templates *template.Template - -type HtmlTemplatePage api.Page - -func NewHtmlTemplatePage(templateName string, title string, locale string) *HtmlTemplatePage { - - if len(title) > 0 { - title = utils.Cfg.TeamSettings.SiteName + " - " + title - } - - props := make(map[string]string) - props["Title"] = title - return &HtmlTemplatePage{ - TemplateName: templateName, - Props: props, - ClientCfg: utils.ClientCfg, - ClientLicense: utils.ClientLicense, - Locale: locale, - } -} - -func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) { - if me.Team != nil { - me.Team.Sanitize() - } - - if me.User != nil { - me.User.Sanitize(map[string]bool{}) - me.Locale = me.User.Locale - } - - me.Props["Locale"] = me.Locale - me.SessionTokenIndex = c.SessionTokenIndex - - me.ClientCfg["HeaderBack"] = c.T("web.header.back") - me.ClientCfg["FooterHelp"] = c.T("web.footer.help") - me.ClientCfg["FooterTerms"] = c.T("web.footer.terms") - me.ClientCfg["FooterPrivacy"] = c.T("web.footer.privacy") - me.ClientCfg["FooterAbout"] = c.T("web.footer.about") - - if err := Templates.ExecuteTemplate(w, me.TemplateName, me); err != nil { - c.SetUnknownError(me.TemplateName, err.Error()) - } -} - func InitWeb() { l4g.Debug(utils.T("web.init.debug")) @@ -74,81 +23,7 @@ func InitWeb() { l4g.Debug("Using static directory at %v", staticDir) mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) - mainrouter.Handle("/", api.AppHandlerIndependent(root)).Methods("GET") - mainrouter.Handle("/oauth/authorize", api.UserRequired(authorizeOAuth)).Methods("GET") - mainrouter.Handle("/oauth/access_token", api.ApiAppHandler(getAccessToken)).Methods("POST") - - mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET") - mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET") - mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET") - mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") - mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") - mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") - mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8) - mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8) - mainrouter.Handle("/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") - - mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}/{team:[A-Za-z0-9-]*}", api.UserRequired(adminConsole)).Methods("GET") - - mainrouter.Handle("/hooks/{id:[A-Za-z0-9]+}", api.ApiAppHandler(incomingWebhook)).Methods("POST") - - mainrouter.Handle("/docs/{doc:[A-Za-z0-9]+}", api.AppHandlerIndependent(docs)).Methods("GET") - - // ---------------------------------------------------------------------------------------------- - // *ANYTHING* team specific should go below this line - // ---------------------------------------------------------------------------------------------- - - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/claim", api.AppHandler(claimAccount)).Methods("GET") - mainrouter.Handle("/{team}/pl/{postid}", api.AppHandler(postPermalink)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/channels/{channelname}", api.AppHandler(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - - watchAndParseTemplates() -} - -func watchAndParseTemplates() { - - templatesDir := utils.FindDir("web/templates") - l4g.Debug(utils.T("web.parsing_templates.debug"), templatesDir) - var err error - if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("web.parsing_templates.error"), err) - } - - watcher, err := fsnotify.NewWatcher() - if err != nil { - l4g.Error(utils.T("web.create_dir.error"), err) - } - - go func() { - for { - select { - case event := <-watcher.Events: - if event.Op&fsnotify.Write == fsnotify.Write { - l4g.Info(utils.T("web.reparse_templates.info"), event.Name) - if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("web.parsing_templates.error"), err) - } - } - case err := <-watcher.Errors: - l4g.Error(utils.T("web.dir_fail.error"), err) - } - } - }() - - err = watcher.Add(templatesDir) - if err != nil { - l4g.Error(utils.T("web.watcher_fail.error"), err) - } + mainrouter.Handle("/{anything:.*}", api.AppHandlerIndependent(root)).Methods("GET") } var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7;Safari/8" @@ -177,1026 +52,9 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { return } - if len(c.Session.UserId) == 0 { - page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale) - page.Props["Info"] = c.T("web.root.singup_info") - - if result := <-api.Srv.Store.Team().GetAllTeamListing(); result.Err != nil { - c.Err = result.Err - return - } else { - teams := result.Data.([]*model.Team) - for _, team := range teams { - page.Props[team.Name] = team.DisplayName - } - - if len(teams) == 1 && *utils.Cfg.TeamSettings.EnableTeamListing && !utils.Cfg.TeamSettings.EnableTeamCreation { - http.Redirect(w, r, c.GetSiteURL()+"/"+teams[0].Name, http.StatusTemporaryRedirect) - return - } - } - - page.Render(c, w) - } else { - teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) - userChan := api.Srv.Store.User().Get(c.Session.UserId) - - var team *model.Team - if tr := <-teamChan; tr.Err != nil { - c.Err = tr.Err - return - } else { - team = tr.Data.(*model.Team) - - } - - var user *model.User - if ur := <-userChan; ur.Err != nil { - c.Err = ur.Err - return - } else { - user = ur.Data.(*model.User) - } - - page := NewHtmlTemplatePage("home", c.T("web.root.home_title"), c.Locale) - page.Team = team - page.User = user - page.Render(c, w) - } -} - -func signup(c *api.Context, w http.ResponseWriter, r *http.Request) { - - if !CheckBrowserCompatability(c, r) { - return - } - - page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale) - page.Render(c, w) -} - -func login(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !CheckBrowserCompatability(c, r) { - return - } - params := mux.Vars(r) - teamName := params["team"] - - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - l4g.Error(utils.T("web.login.error"), teamName, tResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - team = tResult.Data.(*model.Team) - } - - // We still might be able to switch to this team because we've logged in before - _, session := api.FindMultiSessionForTeamId(r, team.Id) - if session != nil { - w.Header().Set(model.HEADER_TOKEN, session.Token) - lastViewChannelName := "town-square" - if lastViewResult := <-api.Srv.Store.Preference().Get(session.UserId, model.PREFERENCE_CATEGORY_LAST, model.PREFERENCE_NAME_LAST_CHANNEL); lastViewResult.Err == nil { - if lastViewChannelResult := <-api.Srv.Store.Channel().Get(lastViewResult.Data.(model.Preference).Value); lastViewChannelResult.Err == nil { - lastViewChannelName = lastViewChannelResult.Data.(*model.Channel).Name - } - } - - http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/"+lastViewChannelName, http.StatusTemporaryRedirect) - return - } - - page := NewHtmlTemplatePage("login", c.T("web.login.login_title"), c.Locale) - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["TeamName"] = team.Name - - if team.AllowOpenInvite { - page.Props["InviteId"] = team.InviteId - } - - page.Render(c, w) -} - -func signupTeamConfirm(c *api.Context, w http.ResponseWriter, r *http.Request) { - email := r.FormValue("email") - - page := NewHtmlTemplatePage("signup_team_confirm", c.T("web.signup_team_confirm.title"), c.Locale) - page.Props["Email"] = email - page.Render(c, w) -} - -func signupTeamComplete(c *api.Context, w http.ResponseWriter, r *http.Request) { - data := r.FormValue("d") - hash := r.FormValue("h") - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.invalid_link.app_error", nil, "") - return - } - - props := model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*24*30 { // 30 days - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.link_expired.app_error", nil, "") - return - } - - page := NewHtmlTemplatePage("signup_team_complete", c.T("web.signup_team_complete.title"), c.Locale) - page.Props["Email"] = props["email"] - page.Props["Data"] = data - page.Props["Hash"] = hash - page.Render(c, w) -} - -func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) { - - id := r.FormValue("id") - data := r.FormValue("d") - hash := r.FormValue("h") - var props map[string]string - - if len(id) > 0 { - props = make(map[string]string) - - if result := <-api.Srv.Store.Team().GetByInviteId(id); result.Err != nil { - c.Err = result.Err - return - } else { - team := result.Data.(*model.Team) - if !(team.Type == model.TEAM_OPEN || (team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0)) { - c.Err = model.NewLocAppError("signupUserComplete", "web.signup_user_complete.no_invites.app_error", nil, "id="+id) - return - } - - props["email"] = "" - props["display_name"] = team.DisplayName - props["name"] = team.Name - props["id"] = team.Id - data = model.MapToJson(props) - hash = "" - } - } else { - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_invalid.app_error", nil, "") - return - } - - props = model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hour - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_expired.app_error", nil, "") - return - } - } - - page := NewHtmlTemplatePage("signup_user_complete", c.T("web.signup_user_complete.title"), c.Locale) - page.Props["Email"] = props["email"] - page.Props["TeamDisplayName"] = props["display_name"] - page.Props["TeamName"] = props["name"] - page.Props["TeamId"] = props["id"] - page.Props["Data"] = data - page.Props["Hash"] = hash - page.Render(c, w) -} - -func logout(c *api.Context, w http.ResponseWriter, r *http.Request) { - api.Logout(c, w, r) - http.Redirect(w, r, c.GetTeamURL(), http.StatusTemporaryRedirect) -} - -func postPermalink(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - teamName := params["team"] - postId := params["postid"] - - if len(postId) != 26 { - c.Err = model.NewLocAppError("postPermalink", "web.post_permalink.app_error", nil, "id="+postId) - return - } - - team := checkSessionSwitch(c, w, r, teamName) - if team == nil { - // Error already set by getTeam - return - } - - var post *model.Post - if result := <-api.Srv.Store.Post().Get(postId); result.Err != nil { - c.Err = result.Err - return - } else { - postlist := result.Data.(*model.PostList) - post = postlist.Posts[postlist.Order[0]] - } - - var channel *model.Channel - if result := <-api.Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId); result.Err != nil { - c.Err = result.Err - return - } else { - if result.Data.(int64) == 0 { - if channel = autoJoinChannelId(c, w, r, post.ChannelId); channel == nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return - } - } else { - if result := <-api.Srv.Store.Channel().Get(post.ChannelId); result.Err != nil { - c.Err = result.Err - return - } else { - channel = result.Data.(*model.Channel) - } - } - } - - doLoadChannel(c, w, r, team, channel, post.Id) -} - -func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - name := params["channelname"] - teamName := params["team"] - - team := checkSessionSwitch(c, w, r, teamName) - if team == nil { - // Error already set by getTeam - return - } - - var channel *model.Channel - if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil { - c.Err = result.Err - return - } else { - channelId := result.Data.(string) - if len(channelId) == 0 { - if channel = autoJoinChannelName(c, w, r, name); channel == nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return - } - } else { - if result := <-api.Srv.Store.Channel().Get(channelId); result.Err != nil { - c.Err = result.Err - return - } else { - channel = result.Data.(*model.Channel) - } - } - } - - doLoadChannel(c, w, r, team, channel, "") -} - -func autoJoinChannelName(c *api.Context, w http.ResponseWriter, r *http.Request, channelName string) *model.Channel { - if strings.Index(channelName, "__") > 0 { - // It's a direct message channel that doesn't exist yet so let's create it - ids := strings.Split(channelName, "__") - otherUserId := "" - if ids[0] == c.Session.UserId { - otherUserId = ids[1] - } else { - otherUserId = ids[0] - } - - if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil { - api.Handle404(w, r) - return nil - } else { - return sc - } - } else { - // We will attempt to auto-join open channels - return joinOpenChannel(c, w, r, api.Srv.Store.Channel().GetByName(c.Session.TeamId, channelName)) - } - - return nil -} - -func autoJoinChannelId(c *api.Context, w http.ResponseWriter, r *http.Request, channelId string) *model.Channel { - return joinOpenChannel(c, w, r, api.Srv.Store.Channel().Get(channelId)) -} - -func joinOpenChannel(c *api.Context, w http.ResponseWriter, r *http.Request, channel store.StoreChannel) *model.Channel { - if cr := <-channel; cr.Err != nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return nil - } else { - channel := cr.Data.(*model.Channel) - if channel.Type == model.CHANNEL_OPEN { - api.JoinChannel(c, channel.Id, "") - if c.Err != nil { - return nil - } - } else { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return nil - } - return channel - } -} - -func checkSessionSwitch(c *api.Context, w http.ResponseWriter, r *http.Request, teamName string) *model.Team { - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return nil - } else { - team = result.Data.(*model.Team) - } - - // We are logged into a different team. Lets see if we have another - // session in the cookie that will give us access. - if c.Session.TeamId != team.Id { - index, session := api.FindMultiSessionForTeamId(r, team.Id) - if session == nil { - // redirect to login - http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/?redirect="+url.QueryEscape(r.URL.Path), http.StatusTemporaryRedirect) - } else { - c.Session = *session - c.SessionTokenIndex = index - } - } - - return team -} - -func doLoadChannel(c *api.Context, w http.ResponseWriter, r *http.Request, team *model.Team, channel *model.Channel, postid string) { - userChan := api.Srv.Store.User().Get(c.Session.UserId) - prefChan := api.Srv.Store.Preference().GetAll(c.Session.UserId) - - var user *model.User - if ur := <-userChan; ur.Err != nil { - c.Err = ur.Err - c.RemoveSessionCookie(w, r) - l4g.Error(utils.T("web.do_load_channel.error"), c.Session.UserId) - return - } else { - user = ur.Data.(*model.User) - } - - var preferences model.Preferences - if result := <-prefChan; result.Err != nil { - l4g.Error("Error in getting preferences for id=%v", c.Session.UserId) - } else { - preferences = result.Data.(model.Preferences) - } - - page := NewHtmlTemplatePage("channel", "", c.Locale) - page.Props["Title"] = channel.DisplayName + " - " + team.DisplayName + " " + page.ClientCfg["SiteName"] - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["ChannelName"] = channel.Name - page.Props["ChannelId"] = channel.Id - page.Props["PostId"] = postid - page.Team = team - page.User = user - page.Channel = channel - page.Preferences = &preferences - page.Render(c, w) -} - -func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { - resend := r.URL.Query().Get("resend") - resendSuccess := r.URL.Query().Get("resend_success") - name := r.URL.Query().Get("teamname") - email := r.URL.Query().Get("email") - hashedId := r.URL.Query().Get("hid") - userId := r.URL.Query().Get("uid") - - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(name); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - - if resend == "true" { - if result := <-api.Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { - c.Err = result.Err - return - } else { - user := result.Data.(*model.User) - - if user.LastActivityAt > 0 { - api.SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) - } else { - api.SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) - } - - newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1) - http.Redirect(w, r, newAddress, http.StatusFound) - return - } - } - - if len(userId) == 26 && len(hashedId) != 0 && model.ComparePassword(hashedId, userId) { - if c.Err = (<-api.Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil { - return - } else { - c.LogAudit("Email Verified") - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?extra=verified&email="+url.QueryEscape(email), http.StatusTemporaryRedirect) - return - } - } - - page := NewHtmlTemplatePage("verify", c.T("web.email_verified.title"), c.Locale) - page.Props["TeamURL"] = c.GetTeamURLFromTeam(team) - page.Props["UserEmail"] = email - page.Props["ResendSuccess"] = resendSuccess - page.Render(c, w) -} - -func findTeam(c *api.Context, w http.ResponseWriter, r *http.Request) { - page := NewHtmlTemplatePage("find_team", c.T("web.find_team.title"), c.Locale) - page.Render(c, w) -} - -func docs(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - doc := params["doc"] - - var user *model.User - if len(c.Session.UserId) != 0 { - userChan := api.Srv.Store.User().Get(c.Session.UserId) - if userChan := <-userChan; userChan.Err == nil { - user = userChan.Data.(*model.User) - } - } - - page := NewHtmlTemplatePage("docs", c.T("web.doc.title"), c.Locale) - page.Props["Site"] = doc - page.User = user - page.Render(c, w) -} - -func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { - isResetLink := true - hash := r.URL.Query().Get("h") - data := r.URL.Query().Get("d") - params := mux.Vars(r) - teamName := params["team"] - - if len(hash) == 0 || len(data) == 0 { - isResetLink = false - } else { - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) { - c.Err = model.NewLocAppError("resetPassword", "web.reset_password.invalid_link.app_error", nil, "") - return - } - - props := model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour - c.Err = model.NewLocAppError("resetPassword", "web.reset_password.expired_link.app_error", nil, "") - return - } - } - - teamDisplayName := "Developer/Beta" - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - c.Err = tResult.Err - return - } else { - team = tResult.Data.(*model.Team) - } - - if team != nil { - teamDisplayName = team.DisplayName - } - - page := NewHtmlTemplatePage("password_reset", "", c.Locale) - page.Props["Title"] = "Reset Password " + page.ClientCfg["SiteName"] - page.Props["TeamDisplayName"] = teamDisplayName - page.Props["TeamName"] = teamName - page.Props["Hash"] = hash - page.Props["Data"] = data - page.Props["TeamName"] = teamName - page.Props["IsReset"] = strconv.FormatBool(isResetLink) - page.Render(c, w) -} - -func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - teamName := params["team"] - - if !utils.Cfg.TeamSettings.EnableUserCreation { - c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - if len(teamName) == 0 { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) - c.Err.StatusCode = http.StatusBadRequest - return - } - - hash := r.URL.Query().Get("h") - - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - - if api.IsVerifyHashRequired(nil, team, hash) { - data := r.URL.Query().Get("d") - props := model.MapFromJson(strings.NewReader(data)) - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "") - return - } - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "") - return - } - - if team.Id != props["id"] { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data) - return - } - } - - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_SIGNUP - - if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } -} - -func completeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - - code := r.URL.Query().Get("code") - state := r.URL.Query().Get("state") - - uri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8) - - if body, team, props, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { - c.Err = err - return - } else { - action := props["action"] - switch action { - case model.OAUTH_ACTION_SIGNUP: - api.CreateOAuthUser(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - case model.OAUTH_ACTION_LOGIN: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - case model.OAUTH_ACTION_EMAIL_TO_SSO: - api.CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"]) - if c.Err == nil { - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect) - } - break - case model.OAUTH_ACTION_SSO_TO_EMAIL: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) - } - break - default: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - } - } -} - -func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - teamName := params["team"] - loginHint := r.URL.Query().Get("login_hint") - - if len(teamName) == 0 { - c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) - c.Err.StatusCode = http.StatusBadRequest - return - } - - // Make sure team exists - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return - } - - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_LOGIN - - if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } -} - -func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) { - - if !c.HasSystemAdminPermissions("adminConsole") { - return - } - - teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) - userChan := api.Srv.Store.User().Get(c.Session.UserId) - - var team *model.Team - if tr := <-teamChan; tr.Err != nil { - c.Err = tr.Err - return - } else { - team = tr.Data.(*model.Team) - - } - - var user *model.User - if ur := <-userChan; ur.Err != nil { - c.Err = ur.Err - return - } else { - user = ur.Data.(*model.User) - } - - params := mux.Vars(r) - activeTab := params["tab"] - teamId := params["team"] - - page := NewHtmlTemplatePage("admin_console", c.T("web.admin_console.title"), c.Locale) - page.User = user - page.Team = team - page.Props["ActiveTab"] = activeTab - page.Props["TeamId"] = teamId - page.Render(c, w) -} - -func authorizeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - if !CheckBrowserCompatability(c, r) { - return - } - - responseType := r.URL.Query().Get("response_type") - clientId := r.URL.Query().Get("client_id") - redirect := r.URL.Query().Get("redirect_uri") - scope := r.URL.Query().Get("scope") - state := r.URL.Query().Get("state") - - if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 { - c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "") - return - } - - var app *model.OAuthApp - if result := <-api.Srv.Store.OAuth().GetApp(clientId); result.Err != nil { - c.Err = result.Err - return - } else { - app = result.Data.(*model.OAuthApp) - } - - var team *model.Team - if result := <-api.Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) + page := utils.NewHTMLTemplate("root", c.Locale) + page.Props["Title"] = c.T("web.root.home_title") + if err := page.RenderToWriter(w); err != nil { + c.SetUnknownError(page.TemplateName, err.Error()) } - - page := NewHtmlTemplatePage("authorize", c.T("web.authorize_oauth.title"), c.Locale) - page.Props["TeamName"] = team.Name - page.Props["AppName"] = app.Name - page.Props["ResponseType"] = responseType - page.Props["ClientId"] = clientId - page.Props["RedirectUri"] = redirect - page.Props["Scope"] = scope - page.Props["State"] = state - page.Render(c, w) -} - -func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - c.LogAudit("attempt") - - r.ParseForm() - - grantType := r.FormValue("grant_type") - if grantType != model.ACCESS_TOKEN_GRANT_TYPE { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "") - return - } - - clientId := r.FormValue("client_id") - if len(clientId) != 26 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "") - return - } - - secret := r.FormValue("client_secret") - if len(secret) == 0 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "") - return - } - - code := r.FormValue("code") - if len(code) == 0 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "") - return - } - - redirectUri := r.FormValue("redirect_uri") - - achan := api.Srv.Store.OAuth().GetApp(clientId) - tchan := api.Srv.Store.OAuth().GetAccessDataByAuthCode(code) - - authData := api.GetAuthData(code) - - if authData == nil { - c.LogAudit("fail - invalid auth code") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - uchan := api.Srv.Store.User().Get(authData.UserId) - - if authData.IsExpired() { - c.LogAudit("fail - auth code expired") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - if authData.RedirectUri != redirectUri { - c.LogAudit("fail - redirect uri provided did not match previous redirect uri") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "") - return - } - - if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) { - c.LogAudit("fail - auth code is invalid") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - var app *model.OAuthApp - if result := <-achan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") - return - } else { - app = result.Data.(*model.OAuthApp) - } - - if !model.ComparePassword(app.ClientSecret, secret) { - c.LogAudit("fail - invalid client credentials") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") - return - } - - callback := redirectUri - if len(callback) == 0 { - callback = app.CallbackUrls[0] - } - - if result := <-tchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "") - return - } else if result.Data != nil { - c.LogAudit("fail - auth code has been used previously") - accessData := result.Data.(*model.AccessData) - - // Revoke access token, related auth code, and session from DB as well as from cache - if err := api.RevokeAccessToken(accessData.Token); err != nil { - l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message) - } - - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "") - return - } - - var user *model.User - if result := <-uchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "") - return - } else { - user = result.Data.(*model.User) - } - - session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true} - - if result := <-api.Srv.Store.Session().Save(session); result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "") - return - } else { - session = result.Data.(*model.Session) - api.AddSessionToCache(session) - } - - accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback} - - if result := <-api.Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { - l4g.Error(result.Err) - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "") - return - } - - accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)} - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-store") - w.Header().Set("Pragma", "no-cache") - - c.LogAuditWithUserId(user.Id, "success") - - w.Write([]byte(accessRsp.ToJson())) -} - -func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - params := mux.Vars(r) - id := params["id"] - - hchan := api.Srv.Store.Webhook().GetIncoming(id) - - r.ParseForm() - - var parsedRequest *model.IncomingWebhookRequest - contentType := r.Header.Get("Content-Type") - if strings.Split(contentType, "; ")[0] == "application/json" { - parsedRequest = model.IncomingWebhookRequestFromJson(r.Body) - } else { - parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload"))) - } - - if parsedRequest == nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.parse.app_error", nil, "") - return - } - - text := parsedRequest.Text - if len(text) == 0 && parsedRequest.Attachments == nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.text.app_error", nil, "") - return - } - - channelName := parsedRequest.ChannelName - webhookType := parsedRequest.Type - - //attachments is in here for slack compatibility - if parsedRequest.Attachments != nil { - if len(parsedRequest.Props) == 0 { - parsedRequest.Props = make(model.StringInterface) - } - parsedRequest.Props["attachments"] = parsedRequest.Attachments - webhookType = model.POST_SLACK_ATTACHMENT - } - - var hook *model.IncomingWebhook - if result := <-hchan; result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.invalid.app_error", nil, "err="+result.Err.Message) - return - } else { - hook = result.Data.(*model.IncomingWebhook) - } - - var channel *model.Channel - var cchan store.StoreChannel - - if len(channelName) != 0 { - if channelName[0] == '@' { - if result := <-api.Srv.Store.User().GetByUsername(hook.TeamId, channelName[1:]); result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.user.app_error", nil, "err="+result.Err.Message) - return - } else { - channelName = model.GetDMNameFromIds(result.Data.(*model.User).Id, hook.UserId) - } - } else if channelName[0] == '#' { - channelName = channelName[1:] - } - - cchan = api.Srv.Store.Channel().GetByName(hook.TeamId, channelName) - } else { - cchan = api.Srv.Store.Channel().Get(hook.ChannelId) - } - - overrideUsername := parsedRequest.Username - overrideIconUrl := parsedRequest.IconURL - - if result := <-cchan; result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message) - return - } else { - channel = result.Data.(*model.Channel) - } - - pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId) - - // create a mock session - c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false} - - if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "") - return - } - - if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil { - c.Err = err - return - } - - w.Header().Set("Content-Type", "text/plain") - w.Write([]byte("ok")) -} - -func claimAccount(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !CheckBrowserCompatability(c, r) { - return - } - - params := mux.Vars(r) - teamName := params["team"] - email := r.URL.Query().Get("email") - newType := r.URL.Query().Get("new_type") - - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - l4g.Error(utils.T("web.claim_account.team.error"), teamName, tResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - team = tResult.Data.(*model.Team) - } - - authType := "" - if len(email) != 0 { - if uResult := <-api.Srv.Store.User().GetByEmail(team.Id, email); uResult.Err != nil { - l4g.Error(utils.T("web.claim_account.user.error"), team.Id, email, uResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - user := uResult.Data.(*model.User) - authType = user.AuthService - - // if user is not logged in to their SSO account, ask them to log in - if len(authType) != 0 && user.Id != c.Session.UserId { - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_SSO_TO_EMAIL - stateProps["email"] = email - - if authUrl, err := api.GetAuthorizationCode(c, authType, team.Name, stateProps, ""); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } - } - } - } - - page := NewHtmlTemplatePage("claim_account", c.T("web.claim_account.title"), c.Locale) - page.Props["Email"] = email - page.Props["CurrentType"] = authType - page.Props["NewType"] = newType - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["TeamName"] = team.Name - - page.Render(c, w) } -- cgit v1.2.3-1-g7c22