summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
author=Corey Hulen <corey@hulen.com>2016-03-14 10:23:51 -0700
committer=Corey Hulen <corey@hulen.com>2016-03-14 10:23:51 -0700
commitea3f25924ea64a2dd1e73624c0d30824e1efb240 (patch)
treea4a2b2bdc37bd675fb89837713995ad44a27427b
parent397ebec88c2db3569efd77238bf877e976492d34 (diff)
parentbf7ae0711743926cfbb031675cc3320d7a942465 (diff)
downloadchat-ea3f25924ea64a2dd1e73624c0d30824e1efb240.tar.gz
chat-ea3f25924ea64a2dd1e73624c0d30824e1efb240.tar.bz2
chat-ea3f25924ea64a2dd1e73624c0d30824e1efb240.zip
Merge branch 'master' into PLT-2115
-rw-r--r--CHANGELOG.md125
-rw-r--r--Makefile25
-rw-r--r--api/api.go43
-rw-r--r--api/context.go137
-rw-r--r--api/file.go35
-rw-r--r--api/license.go20
-rw-r--r--api/license_test.go22
-rw-r--r--api/oauth.go343
-rw-r--r--api/post.go25
-rw-r--r--api/team.go114
-rw-r--r--api/team_test.go111
-rw-r--r--api/templates/email_change_subject.html1
-rw-r--r--api/templates/email_change_verify_subject.html1
-rw-r--r--api/templates/error.html37
-rw-r--r--api/templates/find_teams_body.html52
-rw-r--r--api/templates/find_teams_subject.html1
-rw-r--r--api/templates/post_subject.html1
-rw-r--r--api/user.go146
-rw-r--r--api/user_test.go35
-rw-r--r--docker/1.4/Dockerrun.aws.zipbin842 -> 0 bytes
-rw-r--r--docker/2.0/Dockerfile2
-rw-r--r--docker/2.1/Dockerfile (renamed from docker/1.4/Dockerfile)4
-rw-r--r--docker/2.1/Dockerrun.aws.zipbin0 -> 866 bytes
-rw-r--r--docker/2.1/Dockerrun.aws/.ebextensions/01_files.config (renamed from docker/1.4/Dockerrun.aws/.ebextensions/01_files.config)0
-rwxr-xr-xdocker/2.1/Dockerrun.aws/Dockerrun.aws.json (renamed from docker/1.4/Dockerrun.aws/Dockerrun.aws.json)2
-rw-r--r--docker/2.1/README.md (renamed from docker/1.4/README.md)0
-rw-r--r--docker/2.1/config_docker.json (renamed from docker/1.4/config_docker.json)5
-rwxr-xr-xdocker/2.1/docker-entry.sh (renamed from docker/1.4/docker-entry.sh)0
-rw-r--r--i18n/en.json48
-rw-r--r--i18n/es.json40
-rw-r--r--i18n/pt.json64
-rw-r--r--model/client.go67
-rw-r--r--model/session.go2
-rw-r--r--model/team.go7
-rw-r--r--model/version.go1
-rw-r--r--templates/authorize.html (renamed from web/templates/authorize.html)0
-rw-r--r--templates/email_change_body.html (renamed from api/templates/email_change_body.html)0
-rw-r--r--templates/email_change_subject.html1
-rw-r--r--templates/email_change_verify_body.html (renamed from api/templates/email_change_verify_body.html)0
-rw-r--r--templates/email_change_verify_subject.html1
-rw-r--r--templates/email_footer.html (renamed from api/templates/email_footer.html)0
-rw-r--r--templates/email_info.html (renamed from api/templates/email_info.html)0
-rw-r--r--templates/error.html24
-rw-r--r--templates/head.html92
-rw-r--r--templates/invite_body.html (renamed from api/templates/invite_body.html)0
-rw-r--r--templates/invite_subject.html (renamed from api/templates/invite_subject.html)0
-rw-r--r--templates/password_change_body.html (renamed from api/templates/password_change_body.html)0
-rw-r--r--templates/password_change_subject.html (renamed from api/templates/password_change_subject.html)0
-rw-r--r--templates/post_body.html (renamed from api/templates/post_body.html)0
-rw-r--r--templates/post_subject.html1
-rw-r--r--templates/reset_body.html (renamed from api/templates/reset_body.html)0
-rw-r--r--templates/reset_subject.html (renamed from api/templates/reset_subject.html)0
-rw-r--r--templates/root.html12
-rw-r--r--templates/signin_change_body.html (renamed from api/templates/signin_change_body.html)0
-rw-r--r--templates/signin_change_subject.html (renamed from api/templates/signin_change_subject.html)0
-rw-r--r--templates/signup_team_body.html (renamed from api/templates/signup_team_body.html)0
-rw-r--r--templates/signup_team_subject.html (renamed from api/templates/signup_team_subject.html)0
-rw-r--r--templates/verify_body.html (renamed from api/templates/verify_body.html)0
-rw-r--r--templates/verify_subject.html (renamed from api/templates/verify_subject.html)0
-rw-r--r--templates/welcome_body.html (renamed from api/templates/welcome_body.html)0
-rw-r--r--templates/welcome_subject.html (renamed from api/templates/welcome_subject.html)0
-rw-r--r--tests/test-search.md3
-rw-r--r--utils/html.go97
-rw-r--r--utils/license.go17
-rw-r--r--web/react/action_creators/global_actions.jsx (renamed from web/react/dispatcher/event_helpers.jsx)30
-rw-r--r--web/react/components/about_build_modal.jsx39
-rw-r--r--web/react/components/activity_log_modal.jsx36
-rw-r--r--web/react/components/admin_console/admin_controller.jsx11
-rw-r--r--web/react/components/admin_console/admin_navbar_dropdown.jsx19
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx4
-rw-r--r--web/react/components/admin_console/admin_sidebar_header.jsx5
-rw-r--r--web/react/components/admin_console/ldap_settings.jsx6
-rw-r--r--web/react/components/admin_console/license_settings.jsx53
-rw-r--r--web/react/components/admin_console/user_item.jsx2
-rw-r--r--web/react/components/audit_table.jsx21
-rw-r--r--web/react/components/center_panel.jsx47
-rw-r--r--web/react/components/channel_header.jsx32
-rw-r--r--web/react/components/channel_invite_modal.jsx37
-rw-r--r--web/react/components/channel_loader.jsx204
-rw-r--r--web/react/components/channel_notifications_modal.jsx107
-rw-r--r--web/react/components/channel_view.jsx26
-rw-r--r--web/react/components/claim/claim_account.jsx87
-rw-r--r--web/react/components/claim/sso_to_email.jsx2
-rw-r--r--web/react/components/create_post.jsx6
-rw-r--r--web/react/components/delete_channel_modal.jsx4
-rw-r--r--web/react/components/do_verify_email.jsx82
-rw-r--r--web/react/components/docs.jsx41
-rw-r--r--web/react/components/edit_post_modal.jsx4
-rw-r--r--web/react/components/email_verify.jsx108
-rw-r--r--web/react/components/file_attachment.jsx6
-rw-r--r--web/react/components/find_team.jsx135
-rw-r--r--web/react/components/invite_member_modal.jsx4
-rw-r--r--web/react/components/logged_in.jsx224
-rw-r--r--web/react/components/login.jsx248
-rw-r--r--web/react/components/login_email.jsx11
-rw-r--r--web/react/components/navbar.jsx20
-rw-r--r--web/react/components/navbar_dropdown.jsx88
-rw-r--r--web/react/components/needs_team.jsx20
-rw-r--r--web/react/components/not_logged_in.jsx70
-rw-r--r--web/react/components/password_reset.jsx47
-rw-r--r--web/react/components/password_reset_form.jsx105
-rw-r--r--web/react/components/password_reset_send_link.jsx186
-rw-r--r--web/react/components/popover_list_members.jsx2
-rw-r--r--web/react/components/post.jsx21
-rw-r--r--web/react/components/post_body.jsx13
-rw-r--r--web/react/components/post_body_additional_content.jsx20
-rw-r--r--web/react/components/post_focus_view.jsx22
-rw-r--r--web/react/components/post_header.jsx17
-rw-r--r--web/react/components/post_info.jsx56
-rw-r--r--web/react/components/posts_view.jsx18
-rw-r--r--web/react/components/posts_view_container.jsx29
-rw-r--r--web/react/components/rhs_comment.jsx8
-rw-r--r--web/react/components/rhs_root_post.jsx8
-rw-r--r--web/react/components/root.jsx90
-rw-r--r--web/react/components/search_results_item.jsx10
-rw-r--r--web/react/components/should_verify_email.jsx111
-rw-r--r--web/react/components/sidebar.jsx30
-rw-r--r--web/react/components/sidebar_header.jsx14
-rw-r--r--web/react/components/sidebar_right.jsx9
-rw-r--r--web/react/components/sidebar_right_menu.jsx39
-rw-r--r--web/react/components/signup_team.jsx159
-rw-r--r--web/react/components/signup_team_complete.jsx121
-rw-r--r--web/react/components/signup_team_complete/components/signup_team_complete.jsx79
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx (renamed from web/react/components/team_signup_display_name_page.jsx)6
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_email_item.jsx (renamed from web/react/components/team_signup_email_item.jsx)2
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_finished.jsx15
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_password_page.jsx (renamed from web/react/components/team_signup_password_page.jsx)15
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx (renamed from web/react/components/team_signup_send_invites_page.jsx)2
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_url_page.jsx (renamed from web/react/components/team_signup_url_page.jsx)8
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_username_page.jsx (renamed from web/react/components/team_signup_username_page.jsx)8
-rw-r--r--web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx (renamed from web/react/components/team_signup_welcome_page.jsx)12
-rw-r--r--web/react/components/signup_team_confirm.jsx47
-rw-r--r--web/react/components/signup_user_complete.jsx331
-rw-r--r--web/react/components/suggestion/at_mention_provider.jsx2
-rw-r--r--web/react/components/suggestion/suggestion_box.jsx12
-rw-r--r--web/react/components/suggestion/suggestion_list.jsx4
-rw-r--r--web/react/components/team_members_modal.jsx32
-rw-r--r--web/react/components/team_settings.jsx3
-rw-r--r--web/react/components/team_signup_with_email.jsx7
-rw-r--r--web/react/components/textbox.jsx26
-rw-r--r--web/react/components/user_list_row.jsx2
-rw-r--r--web/react/components/user_profile.jsx29
-rw-r--r--web/react/components/user_settings/custom_theme_chooser.jsx2
-rw-r--r--web/react/components/user_settings/manage_languages.jsx3
-rw-r--r--web/react/components/user_settings/user_settings_developer.jsx4
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx15
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx16
-rw-r--r--web/react/components/user_settings/user_settings_security.jsx29
-rw-r--r--web/react/components/view_image_popover_bar.jsx1
-rw-r--r--web/react/package.json2
-rw-r--r--web/react/pages/admin_console.jsx71
-rw-r--r--web/react/pages/channel.jsx97
-rw-r--r--web/react/pages/claim_account.jsx68
-rw-r--r--web/react/pages/docs.jsx64
-rw-r--r--web/react/pages/find_team.jsx62
-rw-r--r--web/react/pages/home.jsx16
-rw-r--r--web/react/pages/login.jsx66
-rw-r--r--web/react/pages/password_reset.jsx68
-rw-r--r--web/react/pages/root.jsx290
-rw-r--r--web/react/pages/signup_team.jsx76
-rw-r--r--web/react/pages/signup_team_complete.jsx66
-rw-r--r--web/react/pages/signup_team_confirm.jsx64
-rw-r--r--web/react/pages/signup_user_complete.jsx69
-rw-r--r--web/react/pages/verify.jsx67
-rw-r--r--web/react/stores/admin_store.jsx6
-rw-r--r--web/react/stores/browser_store.jsx6
-rw-r--r--web/react/stores/localization_store.jsx60
-rw-r--r--web/react/stores/socket_store.jsx28
-rw-r--r--web/react/stores/team_store.jsx38
-rw-r--r--web/react/stores/user_store.jsx62
-rw-r--r--web/react/utils/async_client.jsx55
-rw-r--r--web/react/utils/channel_intro_messages.jsx49
-rw-r--r--web/react/utils/client.jsx179
-rw-r--r--web/react/utils/constants.jsx41
-rw-r--r--web/react/utils/markdown.jsx25
-rw-r--r--web/react/utils/utils.jsx33
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss19
-rw-r--r--web/sass-files/sass/partials/_post.scss31
-rw-r--r--web/sass-files/sass/partials/_responsive.scss3
-rw-r--r--web/sass-files/sass/partials/_sidebar--left.scss6
-rw-r--r--web/static/help/Messaging_en.md47
-rw-r--r--web/static/help/Messaging_es.md37
-rw-r--r--web/static/i18n/en.json28
-rw-r--r--web/static/i18n/es.json25
-rw-r--r--web/static/i18n/pt.json53
-rw-r--r--web/static/images/themes/code_themes/solarized-dark.png (renamed from web/static/images/themes/code_themes/solarized_dark.png)bin81942 -> 81942 bytes
-rw-r--r--web/static/images/themes/code_themes/solarized-light.png (renamed from web/static/images/themes/code_themes/solarized_light.png)bin82868 -> 82868 bytes
-rw-r--r--web/templates/admin_console.html21
-rw-r--r--web/templates/channel.html21
-rw-r--r--web/templates/claim_account.html30
-rw-r--r--web/templates/docs.html27
-rw-r--r--web/templates/find_team.html30
-rw-r--r--web/templates/footer.html39
-rw-r--r--web/templates/head.html191
-rw-r--r--web/templates/home.html24
-rw-r--r--web/templates/login.html27
-rw-r--r--web/templates/password_reset.html30
-rw-r--r--web/templates/signup_team.html29
-rw-r--r--web/templates/signup_team_complete.html29
-rw-r--r--web/templates/signup_team_confirm.html26
-rw-r--r--web/templates/signup_user_complete.html29
-rw-r--r--web/templates/verify.html30
-rw-r--r--web/web.go1158
203 files changed, 3967 insertions, 5201 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8606fc72c..08b5ddbf0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,130 @@
# Mattermost Changelog
+## Release v2.1.0
+
+Expected release date: 2016-03-16
+
+### Highlights
+
+- New Android application now available.
+- New desktop applications for Windows, Mac and Linux now in beta.
+- Brazilian Portuguese translation added.
+
+### New Features
+
+Android Application
+
+- New [Mattermost Android App](https://github.com/mattermost/android) supporting push notifications available for devices running Android 4.4.2+. Requires Mattermost server 2.1 and higher. See [list of tested devices](https://github.com/mattermost/android/blob/master/DEVICES.md).
+
+Desktop Application
+
+- New [Desktop Application](https://github.com/mattermost/desktop) for Windows, Mac, and Linux now available as a beta release.
+
+Languages
+
+- Added Portuguese language translation (Beta) available from **Account Settings** > **Display**.
+
+### Improvements
+
+System Console
+
+- Removed unused “Disable File Storage” option from the System Console as it is no longer relevant.
+- Added a warning message if a system admin demotes themselves.
+- System Console statistics now use a client store instead of fetching data and storing it in state.
+
+Messaging
+
+- Custom slash commands now support temporary messages that appear only to the user that issued the command.
+- Username autocomplete list no longer suggests inactive users.
+
+Mobile
+
+- Significant responsiveness and speed improvements using [fastclick](https://github.com/ftlabs/fastclick).
+- Team name and username are now shown in the LHS header.
+- Added a button to go back to the team URL page from the login page.
+
+Files and Images
+
+- Increased the maximum size of image uploads to 24 megapixels.
+
+User Interface
+
+- Custom theme color selectors are now organized into categories.
+- Add Members and Manage Members dialogs can now be filtered using a search bar.
+- Deactivated members no longer appear in the channel members list.
+- Keyboard focus is set to the text input box in the RHS if a user clicks the reply icon.
+- Permalinks are now displayed in a Copy Permalink dialog instead of a popover.
+- Permalink option is now available from the [...] menu on messages and comments in the RHS.
+- Reply icon now only appears on-hover for messages that don’t have replies.
+- Scroll bar now appears in the center channel.
+
+#### Bug Fixes
+
+- System console user management tab now shows username and email on different lines.
+- Yellow text box error no longer appears when the system is connected.
+- Wildcard search on MySQL databases is now fixed.
+- Usernames in the center channel no longer appear as “...” on login.
+- Deleted messages now delete in the RHS and center channel without requiring a page refresh.
+- Contact us email address in the footer of notification emails now uses the SupportEmail config setting instead of FeedbackEmail.
+- Email addresses are now required to have at least one letter before and after the @ sign.
+- Firefox desktop notifications are now fixed for some users experiencing missed notifications.
+- “User is typing” message containing long usernames no longer causes text wrapping.
+- Usernames appearing as “...” in the RHS when performing a search is fixed.
+- Links that end in image extensions but do not actually link to raw images no longer generate a blank image preview.
+- Channel handle field in the Rename Channel dialog is now visible on themes with dark backgrounds.
+- Autolinked images no longer persist after the post containing the link is deleted.
+- Code theme selector on IE11 now only shows one dropdown arrow and clicking directly on the arrow opens the dropdown.
+- Save/Cancel buttons for language selection in Account Settings are now formatted the same as other settings.
+- Inconsistent field spacing in the Channel Info dialog is fixed.
+- Recent mentions icon no longer jumps to the left of the search bar when the RHS is opened.
+- Custom slash command hints now show up in the autocomplete list.
+- GIF links inside code blocks no longer auto-post the GIFs.
+- Changing usernames no longer adds the old username to “words that trigger mentions”.
+- Notification email footer is now translated based on the sender’s language setting.
+- Slash command `/me` now posts as the user instead of a webhook message.
+- Logout slash command now forces logout.
+- Public links to file attachments on deleted posts no longer work.
+- Error message is now shown in IE11 when uploading more than 5 files or a file over 50 MB.
+
+
+### Compatibility
+Changes from v2.0 to v2.1:
+
+**Android**
+Mattermost Android Application is for use with Mattermost platform v2.1 and higher.
+
+#### Known Issues
+
+- File name tooltip stays open after clicking to download.
+- Unable to paste images into the text box on Firefox, Safari, and IE11.
+- Archived channels are not removed from the "More" menu for the person that archived the channel until after refresh.
+- First load of an empty channel does not display the introduction message.
+- Search results don't highlight searches for @username, non-latin characters, or terms inside Markdown code blocks.
+- Searching for a username or hashtag containing a dot returns a search where the dot is replaced with the "or" operator.
+- Hashtags containing a dash incorrectly highlight in the search results.
+- Emoji smileys ending with a letter at the end of a message do not auto-complete as expected.
+- Incorrect formatting when a new line is added directly after a list.
+- Timestamps are displayed in 12-hour format when set to 24-hour format.
+- Syntax highlighting code block is missing the label for Latex documents.
+- Posts from webhooks do not fire notifications to the user who created the webhook.
+- Theme color vector is not updated after making custom changes to a default theme.
+- Search term highlighting doesn't update on IE11 when search terms change but return the same posts.
+- Team creation via SSO fails when email domain is restricted.
+
+#### Contributors
+
+Many thanks to all our external contributors. In no particular order:
+
+- [rodrigocorsi2](https://github.com/rodrigocorsi2)
+- [enahum](https://github.com/enahum)
+- [khoa-le](https://github.com/khoa-le)
+- [alanmoo](https://github.com/alanmoo)
+- [daizenberg](https://github.com/daizenberg)
+- [GuillaumeAmat](https://github.com/GuillaumeAmat)
+- [kernicPanel](https://github.com/kernicPanel)
+- [timlyo](https://github.com/timlyo)
+- [ttyniwa](https://github.com/ttyniwa)
+
## Release v2.0.0
Expected Release date: 2016-02-16
diff --git a/Makefile b/Makefile
index a7c277e4c..57a28bf3a 100644
--- a/Makefile
+++ b/Makefile
@@ -127,10 +127,9 @@ package:
cp -RL web/static/help $(DIST_PATH)/web/static
cp -RL web/static/images $(DIST_PATH)/web/static
cp -RL web/static/js/jquery-dragster $(DIST_PATH)/web/static/js/
- cp -RL web/templates $(DIST_PATH)/web
+ cp -RL templates $(DIST_PATH)
mkdir -p $(DIST_PATH)/api
- cp -RL api/templates $(DIST_PATH)/api
cp -RL i18n $(DIST_PATH)
cp build/MIT-COMPILED-LICENSE.md $(DIST_PATH)
@@ -140,17 +139,17 @@ package:
mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js
mv $(DIST_PATH)/web/static/js/libs.min.js $(DIST_PATH)/web/static/js/libs-$(BUILD_NUMBER).min.js
- sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html
- sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html
- sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/web/templates/head.html
- sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/web/templates/head.html
- sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/web/templates/head.html
- sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html
- sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/web/templates/head.html
- sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/web/templates/head.html
- sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
- sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
- rm $(DIST_PATH)/web/templates/*.bak
+ sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html
+ sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html
+ sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/templates/head.html
+ sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/templates/head.html
+ sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/templates/head.html
+ sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/templates/head.html
+ sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/templates/head.html
+ sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/templates/head.html
+ sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html
+ sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html
+ rm $(DIST_PATH)/templates/*.bak
sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv'
diff --git a/api/api.go b/api/api.go
index 4fecd3dd4..20f77e558 100644
--- a/api/api.go
+++ b/api/api.go
@@ -4,47 +4,15 @@
package api
import (
- "bytes"
- l4g "github.com/alecthomas/log4go"
+ "net/http"
+
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
- "html/template"
- "net/http"
_ "github.com/cloudfoundry/jibber_jabber"
_ "github.com/nicksnyder/go-i18n/i18n"
)
-var ServerTemplates *template.Template
-
-type ServerTemplatePage Page
-
-func NewServerTemplatePage(templateName, locale string) *ServerTemplatePage {
- return &ServerTemplatePage{
- TemplateName: templateName,
- Props: make(map[string]string),
- Extra: make(map[string]string),
- Html: make(map[string]template.HTML),
- ClientCfg: utils.ClientCfg,
- Locale: locale,
- }
-}
-
-func (me *ServerTemplatePage) Render() string {
- var text bytes.Buffer
-
- T := utils.GetUserTranslations(me.Locale)
- me.Props["Footer"] = T("api.templates.email_footer")
- me.Html["EmailInfo"] = template.HTML(T("api.templates.email_info",
- map[string]interface{}{"SupportEmail": me.ClientCfg["SupportEmail"], "SiteName": me.ClientCfg["SiteName"]}))
-
- if err := ServerTemplates.ExecuteTemplate(&text, me.TemplateName, me); err != nil {
- l4g.Error(utils.T("api.api.render.error"), me.TemplateName, err)
- }
-
- return text.String()
-}
-
func InitApi() {
r := Srv.Router.PathPrefix("/api/v1").Subrouter()
InitUser(r)
@@ -60,12 +28,7 @@ func InitApi() {
InitPreference(r)
InitLicense(r)
- templatesDir := utils.FindDir("api/templates")
- l4g.Debug(utils.T("api.api.init.parsing_templates.debug"), templatesDir)
- var err error
- if ServerTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil {
- l4g.Error(utils.T("api.api.init.parsing_templates.error"), err)
- }
+ utils.InitHTML()
}
func HandleEtag(etag string, w http.ResponseWriter, r *http.Request) bool {
diff --git a/api/context.go b/api/context.go
index edcdcbfef..eed035daf 100644
--- a/api/context.go
+++ b/api/context.go
@@ -5,11 +5,9 @@ package api
import (
"fmt"
- "html/template"
"net"
"net/http"
"net/url"
- "strconv"
"strings"
l4g "github.com/alecthomas/log4go"
@@ -31,33 +29,16 @@ var allowedMethods []string = []string{
}
type Context struct {
- Session model.Session
- RequestId string
- IpAddress string
- Path string
- Err *model.AppError
- teamURLValid bool
- teamURL string
- siteURL string
- SessionTokenIndex int64
- T goi18n.TranslateFunc
- Locale string
-}
-
-type Page struct {
- TemplateName string
- Props map[string]string
- Extra map[string]string
- Html map[string]template.HTML
- ClientCfg map[string]string
- ClientLicense map[string]string
- User *model.User
- Team *model.Team
- Channel *model.Channel
- Preferences *model.Preferences
- PostID string
- SessionTokenIndex int64
- Locale string
+ Session model.Session
+ RequestId string
+ IpAddress string
+ Path string
+ Err *model.AppError
+ teamURLValid bool
+ teamURL string
+ siteURL string
+ T goi18n.TranslateFunc
+ Locale string
}
func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
@@ -121,37 +102,8 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Attempt to parse the token from the cookie
if len(token) == 0 {
- tokens := GetMultiSessionCookieTokens(r)
- if len(tokens) > 0 {
- // If there is only 1 token in the cookie then just use it like normal
- if len(tokens) == 1 {
- token = tokens[0]
- } else {
- // If it is a multi-session token then find the correct session
- sessionTokenIndexStr := r.URL.Query().Get(model.SESSION_TOKEN_INDEX)
- sessionTokenIndex := int64(-1)
- if len(sessionTokenIndexStr) > 0 {
- if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil {
- sessionTokenIndex = index
- }
- } else {
- sessionTokenIndexStr := r.Header.Get(model.HEADER_MM_SESSION_TOKEN_INDEX)
- if len(sessionTokenIndexStr) > 0 {
- if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil {
- sessionTokenIndex = index
- }
- }
- }
-
- if sessionTokenIndex >= 0 && sessionTokenIndex < int64(len(tokens)) {
- token = tokens[sessionTokenIndex]
- c.SessionTokenIndex = sessionTokenIndex
- } else {
- c.SessionTokenIndex = -1
- }
- }
- } else {
- c.SessionTokenIndex = -1
+ if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
+ token = cookie.Value
}
}
@@ -185,8 +137,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if session == nil || session.IsExpired() {
c.RemoveSessionCookie(w, r)
- c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token)
- c.Err.StatusCode = http.StatusUnauthorized
+ if h.requireUser || h.requireSystemAdmin {
+ c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token)
+ c.Err.StatusCode = http.StatusUnauthorized
+ }
} else if !session.IsOAuth && isTokenFromQueryString {
c.Err = model.NewLocAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token)
c.Err.StatusCode = http.StatusUnauthorized
@@ -390,22 +344,6 @@ func (c *Context) IsTeamAdmin() bool {
}
func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
-
- // multiToken := ""
- // if oldMultiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
- // multiToken = oldMultiCookie.Value
- // }
-
- // multiCookie := &http.Cookie{
- // Name: model.SESSION_COOKIE_TOKEN,
- // Value: strings.TrimSpace(strings.Replace(multiToken, c.Session.Token, "", -1)),
- // Path: "/",
- // MaxAge: model.SESSION_TIME_WEB_IN_SECS,
- // HttpOnly: true,
- // }
-
- //http.SetCookie(w, multiCookie)
-
cookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
Value: "",
@@ -538,23 +476,25 @@ func IsPrivateIpAddress(ipAddress string) bool {
}
func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) {
- props := make(map[string]string)
- props["Message"] = err.Message
- props["Details"] = err.DetailedError
+ T, locale := utils.GetTranslationsAndLocale(w, r)
+ page := utils.NewHTMLTemplate("error", locale)
+ page.Props["Message"] = err.Message
+ page.Props["Details"] = err.DetailedError
pathParts := strings.Split(r.URL.Path, "/")
if len(pathParts) > 1 {
- props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1]
+ page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1]
} else {
- props["SiteURL"] = GetProtocol(r) + "://" + r.Host
+ page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host
}
- T, _ := utils.GetTranslationsAndLocale(w, r)
- props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
- props["Link"] = T("api.templates.error.link")
+ page.Props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
+ page.Props["Link"] = T("api.templates.error.link")
w.WriteHeader(err.StatusCode)
- ServerTemplates.ExecuteTemplate(w, "error.html", Page{Props: props, ClientCfg: utils.ClientCfg})
+ if rErr := page.RenderToWriter(w); rErr != nil {
+ l4g.Error("Failed to create error page: " + rErr.Error() + ", Original error: " + err.Error())
+ }
}
func Handle404(w http.ResponseWriter, r *http.Request) {
@@ -588,29 +528,6 @@ func GetSession(token string) *model.Session {
return session
}
-func GetMultiSessionCookieTokens(r *http.Request) []string {
- if multiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
- multiToken := multiCookie.Value
-
- if len(multiToken) > 0 {
- return strings.Split(multiToken, " ")
- }
- }
-
- return []string{}
-}
-
-func FindMultiSessionForTeamId(r *http.Request, teamId string) (int64, *model.Session) {
- for index, token := range GetMultiSessionCookieTokens(r) {
- s := GetSession(token)
- if s != nil && !s.IsExpired() && s.TeamId == teamId {
- return int64(index), s
- }
- }
-
- return -1, nil
-}
-
func AddSessionToCache(session *model.Session) {
sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60))
}
diff --git a/api/file.go b/api/file.go
index 0011afd5b..9150e4bfe 100644
--- a/api/file.go
+++ b/api/file.go
@@ -547,6 +547,41 @@ func writeFile(f []byte, path string) *model.AppError {
return nil
}
+func moveFile(oldPath, newPath string) *model.AppError {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
+ fileData := make(chan []byte)
+ getFileAndForget(oldPath, fileData)
+ fileBytes := <-fileData
+
+ if fileBytes == nil {
+ return model.NewLocAppError("moveFile", "api.file.move_file.get_from_s3.app_error", nil, "")
+ }
+
+ var auth aws.Auth
+ auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey
+
+ s := s3.New(auth, awsRegion())
+ bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket)
+
+ if err := bucket.Del(oldPath); err != nil {
+ return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
+ }
+
+ if err := writeFile(fileBytes, newPath); err != nil {
+ return err
+ }
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
+ if err := os.Rename(utils.Cfg.FileSettings.Directory+oldPath, utils.Cfg.FileSettings.Directory+newPath); err != nil {
+ return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
+ }
+ } else {
+ return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "")
+ }
+
+ return nil
+}
+
func writeFileLocally(f []byte, path string) *model.AppError {
if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil {
return model.NewLocAppError("writeFile", "api.file.write_file_locally.create_dir.app_error", nil, err.Error())
diff --git a/api/license.go b/api/license.go
index 23e7946c8..542b45e26 100644
--- a/api/license.go
+++ b/api/license.go
@@ -20,6 +20,7 @@ func InitLicense(r *mux.Router) {
sr := r.PathPrefix("/license").Subrouter()
sr.Handle("/add", ApiAdminSystemRequired(addLicense)).Methods("POST")
sr.Handle("/remove", ApiAdminSystemRequired(removeLicense)).Methods("POST")
+ sr.Handle("/client_config", ApiAppHandler(getClientLicenceConfig)).Methods("GET")
}
func addLicense(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -130,3 +131,22 @@ func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) {
rdata["status"] = "ok"
w.Write([]byte(model.MapToJson(rdata)))
}
+
+func getClientLicenceConfig(c *Context, w http.ResponseWriter, r *http.Request) {
+ config := utils.ClientLicense
+
+ var etag string
+ if config["IsLicensed"] == "false" {
+ etag = model.Etag(config["IsLicensed"])
+ } else {
+ etag = model.Etag(config["IsLicensed"], config["IssuedAt"])
+ }
+
+ if HandleEtag(etag, w, r) {
+ return
+ }
+
+ w.Header().Set(model.HEADER_ETAG_SERVER, etag)
+
+ w.Write([]byte(model.MapToJson(config)))
+}
diff --git a/api/license_test.go b/api/license_test.go
new file mode 100644
index 000000000..b34aeb7a6
--- /dev/null
+++ b/api/license_test.go
@@ -0,0 +1,22 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "testing"
+)
+
+func TestGetLicenceConfig(t *testing.T) {
+ Setup()
+
+ if result, err := Client.GetClientLicenceConfig(); err != nil {
+ t.Fatal(err)
+ } else {
+ cfg := result.Data.(map[string]string)
+
+ if _, ok := cfg["IsLicensed"]; !ok {
+ t.Fatal(cfg)
+ }
+ }
+}
diff --git a/api/oauth.go b/api/oauth.go
index 1ae3dbf78..9b7f3699d 100644
--- a/api/oauth.go
+++ b/api/oauth.go
@@ -5,12 +5,15 @@ package api
import (
"fmt"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
- "net/http"
- "net/url"
)
func InitOAuth(r *mux.Router) {
@@ -20,6 +23,17 @@ func InitOAuth(r *mux.Router) {
sr.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST")
sr.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET")
+ sr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET")
+ sr.Handle("/{service:[A-Za-z]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET")
+ sr.Handle("/{service:[A-Za-z]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET")
+ sr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET")
+ sr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST")
+
+ // Also handle this a the old routes remove soon apiv2?
+ mr := Srv.Router
+ mr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET")
+ mr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST")
+ mr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET")
}
func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -163,3 +177,328 @@ func GetAuthData(code string) *model.AuthData {
return result.Data.(*model.AuthData)
}
}
+
+func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ service := params["service"]
+
+ code := r.URL.Query().Get("code")
+ state := r.URL.Query().Get("state")
+
+ uri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete"
+
+ if body, team, props, err := AuthorizeOAuthUser(service, code, state, uri); err != nil {
+ c.Err = err
+ return
+ } else {
+ action := props["action"]
+ switch action {
+ case model.OAUTH_ACTION_SIGNUP:
+ CreateOAuthUser(c, w, r, service, body, team)
+ if c.Err == nil {
+ http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect)
+ }
+ break
+ case model.OAUTH_ACTION_LOGIN:
+ LoginByOAuth(c, w, r, service, body, team)
+ if c.Err == nil {
+ http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect)
+ }
+ break
+ case model.OAUTH_ACTION_EMAIL_TO_SSO:
+ CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"])
+ if c.Err == nil {
+ http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect)
+ }
+ break
+ case model.OAUTH_ACTION_SSO_TO_EMAIL:
+ LoginByOAuth(c, w, r, service, body, team)
+ if c.Err == nil {
+ http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect)
+ }
+ break
+ default:
+ LoginByOAuth(c, w, r, service, body, team)
+ if c.Err == nil {
+ http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect)
+ }
+ break
+ }
+ }
+}
+
+func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
+ c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ responseType := r.URL.Query().Get("response_type")
+ clientId := r.URL.Query().Get("client_id")
+ redirect := r.URL.Query().Get("redirect_uri")
+ scope := r.URL.Query().Get("scope")
+ state := r.URL.Query().Get("state")
+
+ if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 {
+ c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "")
+ return
+ }
+
+ var app *model.OAuthApp
+ if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ app = result.Data.(*model.OAuthApp)
+ }
+
+ var team *model.Team
+ if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ page := utils.NewHTMLTemplate("authorize", c.Locale)
+ page.Props["Title"] = c.T("web.authorize_oauth.title")
+ page.Props["TeamName"] = team.Name
+ page.Props["AppName"] = app.Name
+ page.Props["ResponseType"] = responseType
+ page.Props["ClientId"] = clientId
+ page.Props["RedirectUri"] = redirect
+ page.Props["Scope"] = scope
+ page.Props["State"] = state
+ if err := page.RenderToWriter(w); err != nil {
+ c.SetUnknownError(page.TemplateName, err.Error())
+ }
+}
+
+func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ c.LogAudit("attempt")
+
+ r.ParseForm()
+
+ grantType := r.FormValue("grant_type")
+ if grantType != model.ACCESS_TOKEN_GRANT_TYPE {
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "")
+ return
+ }
+
+ clientId := r.FormValue("client_id")
+ if len(clientId) != 26 {
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "")
+ return
+ }
+
+ secret := r.FormValue("client_secret")
+ if len(secret) == 0 {
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "")
+ return
+ }
+
+ code := r.FormValue("code")
+ if len(code) == 0 {
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "")
+ return
+ }
+
+ redirectUri := r.FormValue("redirect_uri")
+
+ achan := Srv.Store.OAuth().GetApp(clientId)
+ tchan := Srv.Store.OAuth().GetAccessDataByAuthCode(code)
+
+ authData := GetAuthData(code)
+
+ if authData == nil {
+ c.LogAudit("fail - invalid auth code")
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
+ return
+ }
+
+ uchan := Srv.Store.User().Get(authData.UserId)
+
+ if authData.IsExpired() {
+ c.LogAudit("fail - auth code expired")
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
+ return
+ }
+
+ if authData.RedirectUri != redirectUri {
+ c.LogAudit("fail - redirect uri provided did not match previous redirect uri")
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "")
+ return
+ }
+
+ if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) {
+ c.LogAudit("fail - auth code is invalid")
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
+ return
+ }
+
+ var app *model.OAuthApp
+ if result := <-achan; result.Err != nil {
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "")
+ return
+ } else {
+ app = result.Data.(*model.OAuthApp)
+ }
+
+ if !model.ComparePassword(app.ClientSecret, secret) {
+ c.LogAudit("fail - invalid client credentials")
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "")
+ return
+ }
+
+ callback := redirectUri
+ if len(callback) == 0 {
+ callback = app.CallbackUrls[0]
+ }
+
+ if result := <-tchan; result.Err != nil {
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "")
+ return
+ } else if result.Data != nil {
+ c.LogAudit("fail - auth code has been used previously")
+ accessData := result.Data.(*model.AccessData)
+
+ // Revoke access token, related auth code, and session from DB as well as from cache
+ if err := RevokeAccessToken(accessData.Token); err != nil {
+ l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message)
+ }
+
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "")
+ return
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "")
+ return
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true}
+
+ if result := <-Srv.Store.Session().Save(session); result.Err != nil {
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "")
+ return
+ } else {
+ session = result.Data.(*model.Session)
+ AddSessionToCache(session)
+ }
+
+ accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback}
+
+ if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil {
+ l4g.Error(result.Err)
+ c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "")
+ return
+ }
+
+ accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)}
+
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Cache-Control", "no-store")
+ w.Header().Set("Pragma", "no-cache")
+
+ c.LogAuditWithUserId(user.Id, "success")
+
+ w.Write([]byte(accessRsp.ToJson()))
+}
+
+func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ service := params["service"]
+ loginHint := r.URL.Query().Get("login_hint")
+ teamName := r.URL.Query().Get("team")
+
+ if len(teamName) == 0 {
+ c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName)
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ // Make sure team exists
+ if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil {
+ c.Err = result.Err
+ return
+ }
+
+ stateProps := map[string]string{}
+ stateProps["action"] = model.OAUTH_ACTION_LOGIN
+
+ if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil {
+ c.Err = err
+ return
+ } else {
+ http.Redirect(w, r, authUrl, http.StatusFound)
+ }
+}
+
+func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ service := params["service"]
+ teamName := r.URL.Query().Get("team")
+
+ if !utils.Cfg.TeamSettings.EnableUserCreation {
+ c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ if len(teamName) == 0 {
+ c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName)
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ hash := r.URL.Query().Get("h")
+
+ var team *model.Team
+ if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ if IsVerifyHashRequired(nil, team, hash) {
+ data := r.URL.Query().Get("d")
+ props := model.MapFromJson(strings.NewReader(data))
+
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
+ c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "")
+ return
+ }
+
+ t, err := strconv.ParseInt(props["time"], 10, 64)
+ if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
+ c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "")
+ return
+ }
+
+ if team.Id != props["id"] {
+ c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data)
+ return
+ }
+ }
+
+ stateProps := map[string]string{}
+ stateProps["action"] = model.OAUTH_ACTION_SIGNUP
+
+ if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil {
+ c.Err = err
+ return
+ } else {
+ http.Redirect(w, r, authUrl, http.StatusFound)
+ }
+}
diff --git a/api/post.go b/api/post.go
index e6560a8e8..d0ec5826a 100644
--- a/api/post.go
+++ b/api/post.go
@@ -419,7 +419,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team
// copy the context and create a mock session for posting the message
mockSession := model.Session{UserId: hook.CreatorId, TeamId: hook.TeamId, IsOAuth: false}
- newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, 0, c.T, c.Locale}
+ newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, c.T, c.Locale}
if text, ok := respProps["text"]; ok {
if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil {
@@ -604,12 +604,13 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
year := fmt.Sprintf("%d", tm.Year())
zone, _ := tm.Zone()
- subjectPage := NewServerTemplatePage("post_subject", profileMap[id].Locale)
+ subjectPage := utils.NewHTMLTemplate("post_subject", profileMap[id].Locale)
subjectPage.Props["Subject"] = userLocale("api.templates.post_subject",
map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
"Month": month[:3], "Day": day, "Year": year})
+ subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName
- bodyPage := NewServerTemplatePage("post_body", profileMap[id].Locale)
+ bodyPage := utils.NewHTMLTemplate("post_body", profileMap[id].Locale)
bodyPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message)
bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name
@@ -1094,6 +1095,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
message.Add("post", post.ToJson())
PublishAndForget(message)
+ DeletePostFilesAndForget(c.Session.TeamId, post)
result := make(map[string]string)
result["id"] = postId
@@ -1101,6 +1103,23 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
+func DeletePostFilesAndForget(teamId string, post *model.Post) {
+ go func() {
+ if len(post.Filenames) == 0 {
+ return
+ }
+
+ prefix := "teams/" + teamId + "/channels/" + post.ChannelId + "/users/" + post.UserId + "/"
+ for _, filename := range post.Filenames {
+ splitUrl := strings.Split(filename, "/")
+ oldPath := prefix + splitUrl[len(splitUrl)-2] + "/" + splitUrl[len(splitUrl)-1]
+ newPath := prefix + splitUrl[len(splitUrl)-2] + "/deleted_" + splitUrl[len(splitUrl)-1]
+ moveFile(oldPath, newPath)
+ }
+
+ }()
+}
+
func getPostsBefore(c *Context, w http.ResponseWriter, r *http.Request) {
getPostsBeforeOrAfter(c, w, r, true)
}
diff --git a/api/team.go b/api/team.go
index 2f680dc76..255982522 100644
--- a/api/team.go
+++ b/api/team.go
@@ -29,13 +29,12 @@ func InitTeam(r *mux.Router) {
sr.Handle("/create_with_ldap", ApiAppHandler(createTeamWithLdap)).Methods("POST")
sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST")
sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST")
- sr.Handle("/all", ApiUserRequired(getAll)).Methods("GET")
+ sr.Handle("/all", ApiAppHandler(getAll)).Methods("GET")
sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST")
- sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST")
- sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST")
sr.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST")
sr.Handle("/update", ApiUserRequired(updateTeam)).Methods("POST")
sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET")
+ sr.Handle("/get_invite_info", ApiAppHandler(getInviteInfo)).Methods("POST")
// These should be moved to the global admain console
sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST")
sr.Handle("/export_team", ApiUserRequired(exportTeam)).Methods("GET")
@@ -60,11 +59,11 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- subjectPage := NewServerTemplatePage("signup_team_subject", c.Locale)
+ subjectPage := utils.NewHTMLTemplate("signup_team_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.signup_team_subject",
map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
- bodyPage := NewServerTemplatePage("signup_team_body", c.Locale)
+ bodyPage := utils.NewHTMLTemplate("signup_team_body", c.Locale)
bodyPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage.Props["Title"] = c.T("api.templates.signup_team_body.title")
bodyPage.Props["Button"] = c.T("api.templates.signup_team_body.button")
@@ -86,7 +85,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
}
if !utils.Cfg.EmailSettings.RequireEmailVerification {
- m["follow_link"] = bodyPage.Props["Link"]
+ m["follow_link"] = fmt.Sprintf("/signup_team_complete/?d=%s&h=%s", url.QueryEscape(data), url.QueryEscape(hash))
}
w.Header().Set("Access-Control-Allow-Origin", " *")
@@ -147,7 +146,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- data := map[string]string{"follow_link": c.GetSiteURL() + "/" + rteam.Name + "/signup/" + service}
+ data := map[string]string{"follow_link": c.GetSiteURL() + "/api/v1/oauth/" + service + "/signup?team=" + rteam.Name}
w.Write([]byte(model.MapToJson(data)))
}
@@ -391,10 +390,6 @@ func isTeamCreationAllowed(c *Context, email string) bool {
}
func getAll(c *Context, w http.ResponseWriter, r *http.Request) {
- if !c.HasSystemAdminPermissions("getLogs") {
- return
- }
-
if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
c.Err = result.Err
return
@@ -403,6 +398,9 @@ func getAll(c *Context, w http.ResponseWriter, r *http.Request) {
m := make(map[string]*model.Team)
for _, v := range teams {
m[v.Id] = v
+ if !c.IsSystemAdmin() {
+ m[v.Id].SanitizeForNotLoggedIn()
+ }
}
w.Write([]byte(model.TeamMapToJson(m)))
@@ -473,74 +471,6 @@ func FindTeamByName(c *Context, name string, all string) bool {
return false
}
-func findTeams(c *Context, w http.ResponseWriter, r *http.Request) {
-
- m := model.MapFromJson(r.Body)
-
- email := strings.ToLower(strings.TrimSpace(m["email"]))
-
- if email == "" {
- c.SetInvalidParam("findTeam", "email")
- return
- }
-
- if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- teams := result.Data.([]*model.Team)
- m := make(map[string]*model.Team)
- for _, v := range teams {
- v.Sanitize()
- m[v.Id] = v
- }
-
- w.Write([]byte(model.TeamMapToJson(m)))
- }
-}
-
-func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) {
-
- m := model.MapFromJson(r.Body)
-
- email := strings.ToLower(strings.TrimSpace(m["email"]))
-
- if email == "" {
- c.SetInvalidParam("findTeam", "email")
- return
- }
-
- siteURL := c.GetSiteURL()
- subjectPage := NewServerTemplatePage("find_teams_subject", c.Locale)
- subjectPage.Props["Subject"] = c.T("api.templates.find_teams_subject",
- map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
-
- bodyPage := NewServerTemplatePage("find_teams_body", c.Locale)
- bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["Title"] = c.T("api.templates.find_teams_body.title")
- bodyPage.Props["Found"] = c.T("api.templates.find_teams_body.found")
- bodyPage.Props["NotFound"] = c.T("api.templates.find_teams_body.not_found")
-
- if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil {
- c.Err = result.Err
- } else {
- teams := result.Data.([]*model.Team)
-
- // the template expects Props to be a map with team names as the keys and the team url as the value
- props := make(map[string]string)
- for _, team := range teams {
- props[team.Name] = c.GetTeamURLFromTeam(team)
- }
- bodyPage.Extra = props
-
- if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
- l4g.Error(utils.T("api.team.email_teams.sending.error"), err)
- }
-
- w.Write([]byte(model.MapToJson(m)))
- }
-}
-
func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) {
invites := model.InvitesFromJson(r.Body)
if len(invites.Invites) == 0 {
@@ -600,11 +530,11 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str
senderRole = c.T("api.team.invite_members.member")
}
- subjectPage := NewServerTemplatePage("invite_subject", c.Locale)
+ subjectPage := utils.NewHTMLTemplate("invite_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.invite_subject",
map[string]interface{}{"SenderName": sender, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]})
- bodyPage := NewServerTemplatePage("invite_body", c.Locale)
+ bodyPage := utils.NewHTMLTemplate("invite_body", c.Locale)
bodyPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage.Props["Title"] = c.T("api.templates.invite_body.title")
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.invite_body.info",
@@ -813,3 +743,25 @@ func exportTeam(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(result)))
}
}
+
+func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) {
+ m := model.MapFromJson(r.Body)
+ inviteId := m["invite_id"]
+
+ if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ team := result.Data.(*model.Team)
+ if !(team.Type == model.TEAM_OPEN) {
+ c.Err = model.NewLocAppError("getInviteInfo", "api.team.get_invite_info.not_open_team", nil, "id="+inviteId)
+ return
+ }
+
+ result := map[string]string{}
+ result["display_name"] = team.DisplayName
+ result["name"] = team.Name
+ result["id"] = team.Id
+ w.Write([]byte(model.MapToJson(result)))
+ }
+}
diff --git a/api/team_test.go b/api/team_test.go
index c942e2e1f..bbbc8385d 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -108,49 +108,36 @@ func TestCreateTeam(t *testing.T) {
}
}
-func TestFindTeamByEmail(t *testing.T) {
+func TestGetAllTeams(t *testing.T) {
Setup()
- team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN, AllowTeamListing: true}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
- if r1, err := Client.FindTeams(user.Email); err != nil {
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ enableIncomingHooks := *utils.Cfg.TeamSettings.EnableTeamListing
+ defer func() {
+ *utils.Cfg.TeamSettings.EnableTeamListing = enableIncomingHooks
+ }()
+ *utils.Cfg.TeamSettings.EnableTeamListing = true
+
+ if r1, err := Client.GetAllTeams(); err != nil {
t.Fatal(err)
} else {
teams := r1.Data.(map[string]*model.Team)
if teams[team.Id].Name != team.Name {
t.Fatal()
}
- if teams[team.Id].DisplayName != team.DisplayName {
- t.Fatal()
+ if teams[team.Id].Email != "" {
+ t.Fatal("Non admin users shoudn't get full listings")
}
}
- if _, err := Client.FindTeams("missing"); err != nil {
- t.Fatal(err)
- }
-}
-
-func TestGetAllTeams(t *testing.T) {
- Setup()
-
- team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
- team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
-
- user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
- user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
-
- Client.LoginByEmail(team.Name, user.Email, "pwd")
-
- if _, err := Client.GetAllTeams(); err == nil {
- t.Fatal("you shouldn't have permissions")
- }
-
c := &Context{}
c.RequestId = model.NewId()
c.IpAddress = "cmd_line"
@@ -165,6 +152,9 @@ func TestGetAllTeams(t *testing.T) {
if teams[team.Id].Name != team.Name {
t.Fatal()
}
+ if teams[team.Id].Email != team.Email {
+ t.Fatal()
+ }
}
}
@@ -207,75 +197,6 @@ func TestTeamPermDelete(t *testing.T) {
Client.ClearOAuthToken()
}
-/*
-
-XXXXXX investigate and fix failing test
-
-func TestFindTeamByDomain(t *testing.T) {
- Setup()
-
- team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
- team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
-
- user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
- user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
-
- if r1, err := Client.FindTeamByDomain(team.Name, false); err != nil {
- t.Fatal(err)
- } else {
- val := r1.Data.(bool)
- if !val {
- t.Fatal("should be a valid domain")
- }
- }
-
- if r1, err := Client.FindTeamByDomain(team.Name, true); err != nil {
- t.Fatal(err)
- } else {
- val := r1.Data.(bool)
- if !val {
- t.Fatal("should be a valid domain")
- }
- }
-
- if r1, err := Client.FindTeamByDomain("a"+model.NewId()+"a", false); err != nil {
- t.Fatal(err)
- } else {
- val := r1.Data.(bool)
- if val {
- t.Fatal("shouldn't be a valid domain")
- }
- }
-}
-
-*/
-
-func TestFindTeamByEmailSend(t *testing.T) {
- Setup()
-
- team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
- team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
-
- user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
- user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
- Client.LoginByEmail(team.Name, user.Email, "pwd")
-
- if _, err := Client.FindTeamsSendEmail(user.Email); err != nil {
- t.Fatal(err)
- } else {
- }
-
- if _, err := Client.FindTeamsSendEmail("missing"); err != nil {
-
- // It should actually succeed at sending the email since it doesn't exist
- if !strings.Contains(err.DetailedError, "Failed to add to email address") {
- t.Fatal(err)
- }
- }
-}
-
func TestInviteMembers(t *testing.T) {
Setup()
diff --git a/api/templates/email_change_subject.html b/api/templates/email_change_subject.html
deleted file mode 100644
index afabc2191..000000000
--- a/api/templates/email_change_subject.html
+++ /dev/null
@@ -1 +0,0 @@
-{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/templates/email_change_verify_subject.html b/api/templates/email_change_verify_subject.html
deleted file mode 100644
index 4fc4f4846..000000000
--- a/api/templates/email_change_verify_subject.html
+++ /dev/null
@@ -1 +0,0 @@
-{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/templates/error.html b/api/templates/error.html
deleted file mode 100644
index 2f588aead..000000000
--- a/api/templates/error.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<html>
-<head>
- <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
- <title><span class='fa fa-chevron-left'></span>Back - Error</title>
-
- <link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
- <link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css" rel="stylesheet">
-
- <script src="/static/js/react-with-addons-0.13.3.min.js"></script>
- <script src="/static/js/jquery-1.11.1.min.js"></script>
- <script src="/static/js/bootstrap-3.3.5.min.js"></script>
- <script src="/static/js/react-bootstrap-0.25.1.min.js"></script>
-
- <link id="favicon" rel="icon" href="/static/images/favicon/favicon-16x16.png" type="image/x-icon">
- <link rel="shortcut icon" href="/static/images/favicon/favicon-16x16.png" type="image/x-icon">
- <link href='/static/css/google-fonts.css' rel='stylesheet' type='text/css'>
- <link rel="stylesheet" href="/static/css/styles.css">
-
-
-</head>
-<body class="white error">
- <div class="container-fluid">
- <div class="error__container">
- <div class="error__icon"><i class="fa fa-exclamation-triangle"></i></div>
- <h2>{{.Props.Title}}</h2>
- <p>{{ .Props.Message }}</p>
- <a href="{{.Props.SiteURL}}">{{.Props.Link}}</a>
- </div>
- </div>
-</body>
-<script>
- var details = "{{ .Details }}";
- if (details.length > 0) {
- console.log("error details: " + details);
- }
-</script>
-</html>
diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html
deleted file mode 100644
index 1324091aa..000000000
--- a/api/templates/find_teams_body.html
+++ /dev/null
@@ -1,52 +0,0 @@
-{{define "find_teams_body"}}
-
-<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;">
- <tr>
- <td>
- <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;">
- <tr>
- <td style="border: 1px solid #ddd;">
- <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
- <tr>
- <td style="padding: 20px 20px 10px; text-align:left;">
- <img src="{{.ClientCfg.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
- </td>
- </tr>
- <tr>
- <td>
- <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
- <tr>
- <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
- <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
- <p>{{ if .Extra }}
- {{.Props.Found}}<br>
- {{range $index, $element := .Extra}}
- <a href="{{ $element }}" style="text-decoration: none; color:#2389D7;">{{ $index }}</a><br>
- {{ end }}
- {{ else }}
- {{.Props.NotFound}}
- {{ end }}
- </p>
- </td>
- </tr>
- <tr>
- {{template "email_info" . }}
- </tr>
- </table>
- </td>
- </tr>
- <tr>
- {{template "email_footer" . }}
- </tr>
- </table>
- </td>
- </tr>
- </table>
- </td>
- </tr>
-</table>
-
-{{end}}
-
-
-
diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html
deleted file mode 100644
index ebc339562..000000000
--- a/api/templates/find_teams_subject.html
+++ /dev/null
@@ -1 +0,0 @@
-{{define "find_teams_subject"}}{{.Props.Subject}}{{end}}
diff --git a/api/templates/post_subject.html b/api/templates/post_subject.html
deleted file mode 100644
index 60daaa432..000000000
--- a/api/templates/post_subject.html
+++ /dev/null
@@ -1 +0,0 @@
-{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/user.go b/api/user.go
index b7e6220d8..0841c38aa 100644
--- a/api/user.go
+++ b/api/user.go
@@ -53,10 +53,13 @@ func InitUser(r *mux.Router) {
sr.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST")
sr.Handle("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST")
sr.Handle("/switch_to_email", ApiUserRequired(switchToEmail)).Methods("POST")
+ sr.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST")
+ sr.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST")
sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET")
+ sr.Handle("/me_logged_in", ApiAppHandler(getMeLoggedIn)).Methods("GET")
sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST")
sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET")
sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET")
@@ -315,10 +318,10 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service
func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) {
go func() {
- subjectPage := NewServerTemplatePage("welcome_subject", c.Locale)
+ subjectPage := utils.NewHTMLTemplate("welcome_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.welcome_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName})
- bodyPage := NewServerTemplatePage("welcome_body", c.Locale)
+ bodyPage := utils.NewHTMLTemplate("welcome_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["Title"] = c.T("api.templates.welcome_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName})
bodyPage.Props["Info"] = c.T("api.templates.welcome_body.info")
@@ -328,7 +331,7 @@ func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayN
bodyPage.Props["TeamURL"] = teamURL
if !verified {
- link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email)
+ link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email)
bodyPage.Props["VerifyUrl"] = link
}
@@ -380,13 +383,13 @@ func addDirectChannelsAndForget(user *model.User) {
func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) {
go func() {
- link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail)
+ link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail)
- subjectPage := NewServerTemplatePage("verify_subject", c.Locale)
+ subjectPage := utils.NewHTMLTemplate("verify_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.verify_subject",
map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
- bodyPage := NewServerTemplatePage("verify_body", c.Locale)
+ bodyPage := utils.NewHTMLTemplate("verify_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["Title"] = c.T("api.templates.verify_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName})
bodyPage.Props["Info"] = c.T("api.templates.verify_body.info")
@@ -621,31 +624,17 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User,
w.Header().Set(model.HEADER_TOKEN, session.Token)
- tokens := GetMultiSessionCookieTokens(r)
- multiToken := ""
- seen := make(map[string]string)
- seen[session.TeamId] = session.TeamId
- for _, token := range tokens {
- s := GetSession(token)
- if s != nil && !s.IsExpired() && seen[s.TeamId] == "" {
- multiToken += " " + token
- seen[s.TeamId] = s.TeamId
- }
- }
-
- multiToken = strings.TrimSpace(multiToken + " " + session.Token)
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0)
-
- multiSessionCookie := &http.Cookie{
+ sessionCookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
- Value: multiToken,
+ Value: session.Token,
Path: "/",
MaxAge: maxAge,
Expires: expiresAt,
HttpOnly: true,
}
- http.SetCookie(w, multiSessionCookie)
+ http.SetCookie(w, sessionCookie)
c.Session = *session
c.LogAuditWithUserId(user.Id, "success")
@@ -902,6 +891,26 @@ func getMe(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
+func getMeLoggedIn(c *Context, w http.ResponseWriter, r *http.Request) {
+ data := make(map[string]string)
+ data["logged_in"] = "false"
+ data["team_name"] = ""
+
+ if len(c.Session.UserId) != 0 {
+ teamChan := Srv.Store.Team().Get(c.Session.TeamId)
+ var team *model.Team
+ if tr := <-teamChan; tr.Err != nil {
+ c.Err = tr.Err
+ return
+ } else {
+ team = tr.Data.(*model.Team)
+ }
+ data["logged_in"] = "true"
+ data["team_name"] = team.Name
+ }
+ w.Write([]byte(model.MapToJson(data)))
+}
+
func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
@@ -1622,12 +1631,12 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
data := model.MapToJson(newProps)
hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt))
- link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash))
+ link := fmt.Sprintf("%s/reset_password_complete?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash))
- subjectPage := NewServerTemplatePage("reset_subject", c.Locale)
+ subjectPage := utils.NewHTMLTemplate("reset_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.reset_subject")
- bodyPage := NewServerTemplatePage("reset_body", c.Locale)
+ bodyPage := utils.NewHTMLTemplate("reset_body", c.Locale)
bodyPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage.Props["Title"] = c.T("api.templates.reset_body.title")
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.reset_body.info"))
@@ -1743,11 +1752,11 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) {
go func() {
- subjectPage := NewServerTemplatePage("password_change_subject", c.Locale)
+ subjectPage := utils.NewHTMLTemplate("password_change_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.password_change_subject",
map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
- bodyPage := NewServerTemplatePage("password_change_body", c.Locale)
+ bodyPage := utils.NewHTMLTemplate("password_change_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["Title"] = c.T("api.templates.password_change_body.title")
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.password_change_body.info",
@@ -1763,11 +1772,12 @@ func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamUR
func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) {
go func() {
- subjectPage := NewServerTemplatePage("email_change_subject", c.Locale)
+ subjectPage := utils.NewHTMLTemplate("email_change_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.email_change_subject",
map[string]interface{}{"TeamDisplayName": teamDisplayName})
+ subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName
- bodyPage := NewServerTemplatePage("email_change_body", c.Locale)
+ bodyPage := utils.NewHTMLTemplate("email_change_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["Title"] = c.T("api.templates.email_change_body.title")
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.email_change_body.info",
@@ -1785,11 +1795,12 @@ func SendEmailChangeVerifyEmailAndForget(c *Context, userId, newUserEmail, teamN
link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail)
- subjectPage := NewServerTemplatePage("email_change_verify_subject", c.Locale)
+ subjectPage := utils.NewHTMLTemplate("email_change_verify_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.email_change_verify_subject",
map[string]interface{}{"TeamDisplayName": teamDisplayName})
+ subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName
- bodyPage := NewServerTemplatePage("email_change_verify_body", c.Locale)
+ bodyPage := utils.NewHTMLTemplate("email_change_verify_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["Title"] = c.T("api.templates.email_change_verify_body.title")
bodyPage.Props["Info"] = c.T("api.templates.email_change_verify_body.info",
@@ -1918,7 +1929,7 @@ func GetAuthorizationCode(c *Context, service, teamName string, props map[string
props["team"] = teamName
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props)))
- redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8)
+ redirectUri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete"
authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state)
@@ -2216,11 +2227,11 @@ func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) {
go func() {
- subjectPage := NewServerTemplatePage("signin_change_subject", c.Locale)
+ subjectPage := utils.NewHTMLTemplate("signin_change_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.singin_change_email.subject",
map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
- bodyPage := NewServerTemplatePage("signin_change_body", c.Locale)
+ bodyPage := utils.NewHTMLTemplate("signin_change_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["Title"] = c.T("api.templates.signin_change_email.body.title")
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.singin_change_email.body.info",
@@ -2232,3 +2243,68 @@ func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL,
}()
}
+
+func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+
+ userId := props["uid"]
+ if len(userId) != 26 {
+ c.SetInvalidParam("verifyEmail", "uid")
+ return
+ }
+
+ hashedId := props["hid"]
+ if len(hashedId) == 0 {
+ c.SetInvalidParam("verifyEmail", "hid")
+ return
+ }
+
+ if model.ComparePassword(hashedId, userId) {
+ if c.Err = (<-Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil {
+ return
+ } else {
+ c.LogAudit("Email Verified")
+ return
+ }
+ }
+
+ c.Err = model.NewLocAppError("verifyEmail", "api.user.verify_email.bad_link.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+}
+
+func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+
+ teamName := props["team_name"]
+ if len(teamName) == 0 {
+ c.SetInvalidParam("resendVerification", "team_name")
+ return
+ }
+
+ email := props["email"]
+ if len(email) == 0 {
+ c.SetInvalidParam("resendVerification", "email")
+ return
+ }
+
+ var team *model.Team
+ if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ user := result.Data.(*model.User)
+
+ if user.LastActivityAt > 0 {
+ SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
+ } else {
+ SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
+ }
+ }
+}
diff --git a/api/user_test.go b/api/user_test.go
index 1a1cf9634..27f00829f 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -1263,3 +1263,38 @@ func TestSwitchToEmail(t *testing.T) {
t.Fatal("should have failed - wrong user")
}
}
+
+func TestMeLoggedIn(t *testing.T) {
+ Setup()
+
+ team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ rteam, _ := Client.CreateTeam(&team)
+
+ user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
+ ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
+
+ Client.AuthToken = "invalid"
+
+ if result, err := Client.GetMeLoggedIn(); err != nil {
+ t.Fatal(err)
+ } else {
+ meLoggedIn := result.Data.(map[string]string)
+
+ if val, ok := meLoggedIn["logged_in"]; !ok || val != "false" {
+ t.Fatal("Got: " + val)
+ }
+ }
+
+ Client.LoginByEmail(team.Name, user.Email, user.Password)
+
+ if result, err := Client.GetMeLoggedIn(); err != nil {
+ t.Fatal(err)
+ } else {
+ meLoggedIn := result.Data.(map[string]string)
+
+ if val, ok := meLoggedIn["logged_in"]; !ok || val != "true" {
+ t.Fatal("Got: " + val)
+ }
+ }
+}
diff --git a/docker/1.4/Dockerrun.aws.zip b/docker/1.4/Dockerrun.aws.zip
deleted file mode 100644
index 55bc98eea..000000000
--- a/docker/1.4/Dockerrun.aws.zip
+++ /dev/null
Binary files differ
diff --git a/docker/2.0/Dockerfile b/docker/2.0/Dockerfile
index 0f7a13e45..38cb1e197 100644
--- a/docker/2.0/Dockerfile
+++ b/docker/2.0/Dockerfile
@@ -34,7 +34,7 @@ VOLUME /var/lib/mysql
WORKDIR /mattermost
# Copy over files
-ADD https://github.com/mattermost/platform/releases/download/v2.0.0-rc2/mattermost.tar.gz /
+ADD https://github.com/mattermost/platform/releases/download/v2.0.0/mattermost.tar.gz /
RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz
ADD config_docker.json /
ADD docker-entry.sh /
diff --git a/docker/1.4/Dockerfile b/docker/2.1/Dockerfile
index 8dcea3927..7ecf64116 100644
--- a/docker/1.4/Dockerfile
+++ b/docker/2.1/Dockerfile
@@ -34,8 +34,8 @@ VOLUME /var/lib/mysql
WORKDIR /mattermost
# Copy over files
-ADD https://github.com/mattermost/platform/releases/download/v1.4.0/mattermost.tar.gz /
-RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz
+ADD https://releases.mattermost.com/2.1.0-rc1/mattermost-team-2.1.0-rc1-linux-amd64.tar.gz /
+RUN tar -zxvf /mattermost-team-2.1.0-rc1-linux-amd64.tar.gz --strip-components=1 && rm /mattermost-team-2.1.0-rc1-linux-amd64.tar.gz
ADD config_docker.json /
ADD docker-entry.sh /
diff --git a/docker/2.1/Dockerrun.aws.zip b/docker/2.1/Dockerrun.aws.zip
new file mode 100644
index 000000000..19bbc7403
--- /dev/null
+++ b/docker/2.1/Dockerrun.aws.zip
Binary files differ
diff --git a/docker/1.4/Dockerrun.aws/.ebextensions/01_files.config b/docker/2.1/Dockerrun.aws/.ebextensions/01_files.config
index 7f40a8b34..7f40a8b34 100644
--- a/docker/1.4/Dockerrun.aws/.ebextensions/01_files.config
+++ b/docker/2.1/Dockerrun.aws/.ebextensions/01_files.config
diff --git a/docker/1.4/Dockerrun.aws/Dockerrun.aws.json b/docker/2.1/Dockerrun.aws/Dockerrun.aws.json
index 654961589..a52309aad 100755
--- a/docker/1.4/Dockerrun.aws/Dockerrun.aws.json
+++ b/docker/2.1/Dockerrun.aws/Dockerrun.aws.json
@@ -1,7 +1,7 @@
{
"AWSEBDockerrunVersion": "1",
"Image": {
- "Name": "mattermost/platform:1.4",
+ "Name": "mattermost/platform:2.1",
"Update": "true"
},
"Ports": [
diff --git a/docker/1.4/README.md b/docker/2.1/README.md
index f737a1554..f737a1554 100644
--- a/docker/1.4/README.md
+++ b/docker/2.1/README.md
diff --git a/docker/1.4/config_docker.json b/docker/2.1/config_docker.json
index 1aa2ee843..6a1290189 100644
--- a/docker/1.4/config_docker.json
+++ b/docker/2.1/config_docker.json
@@ -7,11 +7,14 @@
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": false,
"EnableOutgoingWebhooks": false,
+ "EnableCommands": false,
+ "EnableOnlyAdminIntegrations": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"EnableTesting": false,
"EnableDeveloper": false,
"EnableSecurityFixAlert": true,
+ "EnableInsecureOutgoingConnections": false,
"SessionLengthWebInDays" : 30,
"SessionLengthMobileInDays" : 30,
"SessionLengthSSOInDays" : 30,
@@ -66,6 +69,8 @@
},
"EmailSettings": {
"EnableSignUpWithEmail": true,
+ "EnableSignInWithEmail": true,
+ "EnableSignInWithUsername": false,
"SendEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
diff --git a/docker/1.4/docker-entry.sh b/docker/2.1/docker-entry.sh
index 6bd2a1263..6bd2a1263 100755
--- a/docker/1.4/docker-entry.sh
+++ b/docker/2.1/docker-entry.sh
diff --git a/i18n/en.json b/i18n/en.json
index 57d5179b6..e42ade162 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -456,8 +456,8 @@
"translation": "S3 is not supported for local storage export."
},
{
- "id": "api.export.write_file.app_error",
- "translation": "Unable to write to export file"
+ "id": "api.file.file_upload.exceeds",
+ "translation": "File exceeds max image size."
},
{
"id": "api.file.file_upload.exceeds",
@@ -508,6 +508,22 @@
"translation": "Initializing file api routes"
},
{
+ "id": "api.file.move_file.configured.app_error",
+ "translation": "File storage not configured properly. Please configure for either S3 or local server file storage."
+ },
+ {
+ "id": "api.file.move_file.delete_from_s3.app_error",
+ "translation": "Unable to delete file from S3."
+ },
+ {
+ "id": "api.file.move_file.get_from_s3.app_error",
+ "translation": "Unable to get file from S3."
+ },
+ {
+ "id": "api.file.move_file.rename.app_error",
+ "translation": "Unable to move file locally."
+ },
+ {
"id": "api.file.open_file_write_stream.configured.app_error",
"translation": "File storage not configured properly. Please configure for either S3 or local server file storage."
},
@@ -1608,6 +1624,10 @@
"translation": "Couldn't upload profile image"
},
{
+ "id": "api.user.verify_email.bad_link.app_error",
+ "translation": "Bad verify email link."
+ },
+ {
"id": "api.web_conn.new_web_conn.last_activity.error",
"translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v"
},
@@ -3405,22 +3425,6 @@
"translation": "Find Team"
},
{
- "id": "web.footer.about",
- "translation": "About"
- },
- {
- "id": "web.footer.help",
- "translation": "Help"
- },
- {
- "id": "web.footer.privacy",
- "translation": "Privacy"
- },
- {
- "id": "web.footer.terms",
- "translation": "Terms"
- },
- {
"id": "web.get_access_token.bad_client_id.app_error",
"translation": "invalid_request: Bad client_id"
},
@@ -3553,10 +3557,6 @@
"translation": "Home"
},
{
- "id": "web.root.singup_info",
- "translation": "All team communication in one place, searchable and accessible anywhere"
- },
- {
"id": "web.root.singup_title",
"translation": "Signup"
},
@@ -3611,5 +3611,9 @@
{
"id": "web.watcher_fail.error",
"translation": "Failed to add directory to watcher %v"
+ },
+ {
+ "id": "api.team.get_invite_info.not_open_team",
+ "translation": "Invite is invalid because this is not an open team."
}
]
diff --git a/i18n/es.json b/i18n/es.json
index 6443d18dc..93ffb2341 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -456,8 +456,8 @@
"translation": "S3 no está soportado para exportar al almacenamiento local."
},
{
- "id": "api.export.write_file.app_error",
- "translation": "No se puede escribir al archivo a exportar"
+ "id": "api.file.file_upload.exceeds",
+ "translation": "El archivo excede el tamaño máximo para una imagen."
},
{
"id": "api.file.file_upload.exceeds",
@@ -508,6 +508,22 @@
"translation": "Inicializando rutas del API para los archivos"
},
{
+ "id": "api.file.move_file.configured.app_error",
+ "translation": "No ha sido configurado apropiadamente el almacenamiento. Por favor configuralo para utilizar ya sea S3 o almacenamiento local."
+ },
+ {
+ "id": "api.file.move_file.delete_from_s3.app_error",
+ "translation": "No se pudo eliminar el archivo del S3."
+ },
+ {
+ "id": "api.file.move_file.get_from_s3.app_error",
+ "translation": "No se pudo obtener el archivo desde el S3."
+ },
+ {
+ "id": "api.file.move_file.rename.app_error",
+ "translation": "No se pudo mover el archivo localmente."
+ },
+ {
"id": "api.file.open_file_write_stream.configured.app_error",
"translation": "El almacenamiento de archivos no ha sido configurado apropiadamente. Por favor configuralo ya sea para S3 o para almacenamiento en el servidor local."
},
@@ -3384,22 +3400,6 @@
"translation": "Encontrar Equipo"
},
{
- "id": "web.footer.about",
- "translation": "Acerca"
- },
- {
- "id": "web.footer.help",
- "translation": "Ayuda"
- },
- {
- "id": "web.footer.privacy",
- "translation": "Privacidad"
- },
- {
- "id": "web.footer.terms",
- "translation": "Términos"
- },
- {
"id": "web.get_access_token.bad_client_id.app_error",
"translation": "invalid_request: client_id malo"
},
@@ -3532,10 +3532,6 @@
"translation": "Inicio"
},
{
- "id": "web.root.singup_info",
- "translation": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte"
- },
- {
"id": "web.root.singup_title",
"translation": "Registrar"
},
diff --git a/i18n/pt.json b/i18n/pt.json
index e01db6b22..29c8c58c7 100644
--- a/i18n/pt.json
+++ b/i18n/pt.json
@@ -456,8 +456,8 @@
"translation": "S3 não é suportado para o armazenamento local de exportação."
},
{
- "id": "api.export.write_file.app_error",
- "translation": "Não é possível gravar para exportar arquivo"
+ "id": "api.file.file_upload.exceeds",
+ "translation": "Arquivo excede o máximo de tamanho de imagem."
},
{
"id": "api.file.file_upload.exceeds",
@@ -508,6 +508,22 @@
"translation": "Inicializando file api routes"
},
{
+ "id": "api.file.move_file.configured.app_error",
+ "translation": "Armazenamento de arquivos não está configurado corretamente, Por favor configure S3 ou armazenamento de arquivos no servidor local."
+ },
+ {
+ "id": "api.file.move_file.delete_from_s3.app_error",
+ "translation": "Não é possível deletar o arquivo a partir do S3."
+ },
+ {
+ "id": "api.file.move_file.get_from_s3.app_error",
+ "translation": "Não é possível obter o arquivo a partir do S3."
+ },
+ {
+ "id": "api.file.move_file.rename.app_error",
+ "translation": "Não foi possível mover o arquivo localmente."
+ },
+ {
"id": "api.file.open_file_write_stream.configured.app_error",
"translation": "Armazenamento de arquivos não está configurado corretamente, Por favor configure S3 ou armazenamento de arquivos no servidor local."
},
@@ -1205,7 +1221,7 @@
},
{
"id": "api.templates.signin_change_email.body.title",
- "translation": "Você atualizou seu método de acesso"
+ "translation": "Você atualizou seu método de login"
},
{
"id": "api.templates.signup_team_body.button",
@@ -1225,7 +1241,7 @@
},
{
"id": "api.templates.singin_change_email.body.info",
- "translation": "Você atualizou seu método de inscrição para {{.TeamDisplayName}} no {{ .TeamURL }} para {{.Method}}.<br>Se esta mudança não foi iniciada por você, por favor entre em contato com o administrador do sistema."
+ "translation": "Você atualizou seu método de login para {{.TeamDisplayName}} no {{ .TeamURL }} para {{.Method}}.<br>Se esta mudança não foi iniciada por você, por favor entre em contato com o administrador do sistema."
},
{
"id": "api.templates.singin_change_email.subject",
@@ -1257,7 +1273,7 @@
},
{
"id": "api.templates.welcome_body.info2",
- "translation": "Você pode acessar sua nova equipe pelo endereço web:"
+ "translation": "Você pode fazer login sua nova equipe pelo endereço web:"
},
{
"id": "api.templates.welcome_body.info3",
@@ -1333,7 +1349,7 @@
},
{
"id": "api.user.create_oauth_user.already_used.app_error",
- "translation": "Está conta {{.Service}} já foi utilizada para logar na equipe {{.DisplayName}}"
+ "translation": "Está conta {{.Service}} já foi utilizada para se inscrever na equipe {{.DisplayName}}"
},
{
"id": "api.user.create_oauth_user.create.app_error",
@@ -1357,7 +1373,7 @@
},
{
"id": "api.user.create_user.accepted_domain.app_error",
- "translation": "O email que você forneceu não pertence a um domínio aceito. Por favor contacte o seu administrador ou entre com um email diferente."
+ "translation": "O email que você forneceu não pertence a um domínio aceito. Por favor contacte o seu administrador ou se inscreve com um email diferente."
},
{
"id": "api.user.create_user.joining.error",
@@ -1373,11 +1389,11 @@
},
{
"id": "api.user.create_user.signup_link_expired.app_error",
- "translation": "O link de acesso expirou"
+ "translation": "O link de inscrição expirou"
},
{
"id": "api.user.create_user.signup_link_invalid.app_error",
- "translation": "O link de acesso não parece ser válido"
+ "translation": "O link de inscrição não parece ser válido"
},
{
"id": "api.user.create_user.team_name.app_error",
@@ -2408,6 +2424,10 @@
"translation": "Pingando banco de dados sql %v"
},
{
+ "id": "store.sql.read_replicas_not_licensed.critical",
+ "translation": "A funcionalidade de mais de uma replica está desabilitada pela licença atual. Entre em contato com o administrador do sistema sobre como atualizar sua licença de empresa."
+ },
+ {
"id": "store.sql.remove_index.critical",
"translation": "Falha ao remover o índice %v"
},
@@ -2648,6 +2668,10 @@
"translation": "Encontramos um erro ao atualizar o membro do canal"
},
{
+ "id": "store.sql_command.analytics_command_count.app_error",
+ "translation": "Não foi possível contar os comandos"
+ },
+ {
"id": "store.sql_command.save.delete.app_error",
"translation": "Não foi possível deletar o comando"
},
@@ -2676,10 +2700,6 @@
"translation": "Não foi possível atualizar o comando"
},
{
- "id": "store.sql_command.analytics_command_count.app_error",
- "translation": "Não foi possível contar os comandos"
- },
- {
"id": "store.sql_license.get.app_error",
"translation": "Encontramos um erro ao obter a licença"
},
@@ -2900,6 +2920,10 @@
"translation": "Não foi possível atualizar a preferência"
},
{
+ "id": "store.sql_session.analytics_session_count.app_error",
+ "translation": "Não foi possível contar a sessão"
+ },
+ {
"id": "store.sql_session.cleanup_expired_sessions.app_error",
"translation": "Encontramos um erro enquanto deletava a sessão expirada do usuário"
},
@@ -2952,10 +2976,6 @@
"translation": "Não foi possível atualizar as funções"
},
{
- "id": "store.sql_session.analytics_session_count.app_error",
- "translation": "Não foi possível contar a sessão"
- },
- {
"id": "store.sql_system.get.app_error",
"translation": "Encontramos um erro ao procurar as propriedades de sistema"
},
@@ -2968,6 +2988,10 @@
"translation": "Encontramos um erro ao atualizar as propriedades de sistema"
},
{
+ "id": "store.sql_team.analytics_team_count.app_error",
+ "translation": "Não foi possível contar as equipes"
+ },
+ {
"id": "store.sql_team.get.find.app_error",
"translation": "Não foi possível encontrar a equipe existente"
},
@@ -3036,10 +3060,6 @@
"translation": "Não foi possível atualizar o nome da equipe"
},
{
- "id": "store.sql_team.analytics_team_count.app_error",
- "translation": "Não foi possível contar as equipes"
- },
- {
"id": "store.sql_user.analytics_unique_user_count.app_error",
"translation": "Não foi possível obter o número de usuários únicos"
},
@@ -3587,4 +3607,4 @@
"id": "web.watcher_fail.error",
"translation": "Falha ao adicionar diretório observador %v"
}
-]
+] \ No newline at end of file
diff --git a/model/client.go b/model/client.go
index 560e47b76..3adcb980d 100644
--- a/model/client.go
+++ b/model/client.go
@@ -16,19 +16,17 @@ import (
)
const (
- HEADER_REQUEST_ID = "X-Request-ID"
- HEADER_VERSION_ID = "X-Version-ID"
- HEADER_ETAG_SERVER = "ETag"
- HEADER_ETAG_CLIENT = "If-None-Match"
- HEADER_FORWARDED = "X-Forwarded-For"
- HEADER_REAL_IP = "X-Real-IP"
- HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
- HEADER_TOKEN = "token"
- HEADER_BEARER = "BEARER"
- HEADER_AUTH = "Authorization"
- HEADER_MM_SESSION_TOKEN_INDEX = "X-MM-TokenIndex"
- SESSION_TOKEN_INDEX = "session_token_index"
- API_URL_SUFFIX = "/api/v1"
+ HEADER_REQUEST_ID = "X-Request-ID"
+ HEADER_VERSION_ID = "X-Version-ID"
+ HEADER_ETAG_SERVER = "ETag"
+ HEADER_ETAG_CLIENT = "If-None-Match"
+ HEADER_FORWARDED = "X-Forwarded-For"
+ HEADER_REAL_IP = "X-Real-IP"
+ HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
+ HEADER_TOKEN = "token"
+ HEADER_BEARER = "BEARER"
+ HEADER_AUTH = "Authorization"
+ API_URL_SUFFIX = "/api/v1"
)
type Result struct {
@@ -179,29 +177,6 @@ func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppErro
}
}
-func (c *Client) FindTeams(email string) (*Result, *AppError) {
- m := make(map[string]string)
- m["email"] = email
- if r, err := c.DoApiPost("/teams/find_teams", MapToJson(m)); err != nil {
- return nil, err
- } else {
-
- return &Result{r.Header.Get(HEADER_REQUEST_ID),
- r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil
- }
-}
-
-func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) {
- m := make(map[string]string)
- m["email"] = email
- if r, err := c.DoApiPost("/teams/email_teams", MapToJson(m)); err != nil {
- return nil, err
- } else {
- return &Result{r.Header.Get(HEADER_REQUEST_ID),
- r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil
- }
-}
-
func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) {
if r, err := c.DoApiPost("/teams/invite_members", invites.ToJson()); err != nil {
return nil, err
@@ -938,7 +913,7 @@ func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (*
}
func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) {
- if r, err := c.DoPost("/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil {
+ if r, err := c.DoApiPost("/oauth/access_token", data.Encode()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -1057,3 +1032,21 @@ func (c *Client) MockSession(sessionToken string) {
c.AuthToken = sessionToken
c.AuthType = HEADER_BEARER
}
+
+func (c *Client) GetClientLicenceConfig() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/license/client_config", "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetMeLoggedIn() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/users/me_logged_in", "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
diff --git a/model/session.go b/model/session.go
index 5d9424d64..bf0d9531e 100644
--- a/model/session.go
+++ b/model/session.go
@@ -9,7 +9,7 @@ import (
)
const (
- SESSION_COOKIE_TOKEN = "MMTOKEN"
+ SESSION_COOKIE_TOKEN = "MMAUTHTOKEN"
SESSION_CACHE_SIZE = 10000
SESSION_PROP_PLATFORM = "platform"
SESSION_PROP_OS = "os"
diff --git a/model/team.go b/model/team.go
index 9e9eaa25f..bed7bbd8d 100644
--- a/model/team.go
+++ b/model/team.go
@@ -232,3 +232,10 @@ func (o *Team) Sanitize() {
o.Email = ""
o.AllowedDomains = ""
}
+
+func (o *Team) SanitizeForNotLoggedIn() {
+ o.Email = ""
+ o.AllowedDomains = ""
+ o.CompanyName = ""
+ o.InviteId = ""
+}
diff --git a/model/version.go b/model/version.go
index 8fbd65d03..6011a9245 100644
--- a/model/version.go
+++ b/model/version.go
@@ -13,6 +13,7 @@ import (
// It should be maitained in chronological order with most current
// release at the front of the list.
var versions = []string{
+ "2.1.0",
"2.0.0",
"1.4.0",
"1.3.0",
diff --git a/web/templates/authorize.html b/templates/authorize.html
index 0fa36b0ab..0fa36b0ab 100644
--- a/web/templates/authorize.html
+++ b/templates/authorize.html
diff --git a/api/templates/email_change_body.html b/templates/email_change_body.html
index 41b1bcd7d..41b1bcd7d 100644
--- a/api/templates/email_change_body.html
+++ b/templates/email_change_body.html
diff --git a/templates/email_change_subject.html b/templates/email_change_subject.html
new file mode 100644
index 000000000..540bc6eab
--- /dev/null
+++ b/templates/email_change_subject.html
@@ -0,0 +1 @@
+{{define "email_change_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/templates/email_change_verify_body.html b/templates/email_change_verify_body.html
index 0d0c0aaba..0d0c0aaba 100644
--- a/api/templates/email_change_verify_body.html
+++ b/templates/email_change_verify_body.html
diff --git a/templates/email_change_verify_subject.html b/templates/email_change_verify_subject.html
new file mode 100644
index 000000000..04da7593c
--- /dev/null
+++ b/templates/email_change_verify_subject.html
@@ -0,0 +1 @@
+{{define "email_change_verify_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/templates/email_footer.html b/templates/email_footer.html
index 6dc7fa483..6dc7fa483 100644
--- a/api/templates/email_footer.html
+++ b/templates/email_footer.html
diff --git a/api/templates/email_info.html b/templates/email_info.html
index 0a34f18a0..0a34f18a0 100644
--- a/api/templates/email_info.html
+++ b/templates/email_info.html
diff --git a/templates/error.html b/templates/error.html
new file mode 100644
index 000000000..b86039ca3
--- /dev/null
+++ b/templates/error.html
@@ -0,0 +1,24 @@
+{{define "error"}}
+<!DOCTYPE html>
+<html>
+{{template "head" . }}
+<body class="white error">
+ <div class="container-fluid">
+ <div class="error__container">
+ <div class="error__icon">
+ <i class="fa fa-exclamation-triangle"/>
+ </div>
+ <h2>{{.Props.Title}}</h2>
+ <p>{{ .Props.Message }}</p>
+ <a href="{{.Props.SiteURL}}">{{.Props.Link}}</a>
+ </div>
+ </div>
+</body>
+<script>
+var details = {{ .Props.Details }};
+if (details.length > 0) {
+ console.log("error details: " + details);
+}
+</script>
+</html>
+{{end}}
diff --git a/templates/head.html b/templates/head.html
new file mode 100644
index 000000000..a7eacc85f
--- /dev/null
+++ b/templates/head.html
@@ -0,0 +1,92 @@
+{{define "head"}}
+<head>
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+ <meta name="robots" content="noindex, nofollow">
+ <meta name="referrer" content="no-referrer">
+
+ <title>{{ .Props.Title }}</title>
+
+ <!-- iOS add to homescreen -->
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-title" content="{{ .Props.Title }}">
+ <meta name="application-name" content="{{ .Props.Title }}">
+ <meta name="format-detection" content="telephone=no">
+ <!-- iOS add to homescreen -->
+
+ <!-- Android add to homescreen -->
+ <link rel="apple-touch-icon" sizes="57x57" href="/static/images/favicon/apple-touch-icon-57x57.png">
+ <link rel="apple-touch-icon" sizes="60x60" href="/static/images/favicon/apple-touch-icon-60x60.png">
+ <link rel="apple-touch-icon" sizes="72x72" href="/static/images/favicon/apple-touch-icon-72x72.png">
+ <link rel="apple-touch-icon" sizes="76x76" href="/static/images/favicon/apple-touch-icon-76x76.png">
+ <link rel="apple-touch-icon" sizes="114x114" href="/static/images/favicon/apple-touch-icon-114x114.png">
+ <link rel="apple-touch-icon" sizes="120x120" href="/static/images/favicon/apple-touch-icon-120x120.png">
+ <link rel="apple-touch-icon" sizes="144x144" href="/static/images/favicon/apple-touch-icon-144x144.png">
+ <link rel="apple-touch-icon" sizes="152x152" href="/static/images/favicon/apple-touch-icon-152x152.png">
+ <link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicon/apple-touch-icon-180x180.png">
+ <link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon/favicon-32x32.png">
+ <link rel="icon" type="image/png" sizes="192x192" href="/static/images/favicon/android-chrome-192x192.png">
+ <link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon/favicon-96x96.png">
+ <link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon/favicon-16x16.png">
+ <link rel="manifest" href="/static/config/manifest.json">
+ <!-- Android add to homescreen -->
+
+ <!-- CSS Should always go first -->
+ <link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
+ <link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css">
+ <link rel="stylesheet" href="/static/css/bootstrap-colorpicker.min.css">
+ <link rel="stylesheet" href="/static/css/styles.css">
+ <link rel="stylesheet" href="/static/css/google-fonts.css">
+ <link rel="stylesheet" href="/static/css/katex.min.css">
+ <link rel="stylesheet" class="code_theme" href="">
+
+ <script src="/static/js/intl-1.0.0/Intl.js"></script>
+ <script src="/static/js/intl-1.0.0/locale-data/jsonp/en.js"></script>
+ <script src="/static/js/intl-1.0.0/locale-data/jsonp/es.js"></script>
+ <script src="/static/js/intl-1.0.0/locale-data/jsonp/pt.js"></script>
+
+ <script src="/static/js/react-0.14.3.js"></script>
+ <script src="/static/js/react-dom-0.14.3.js"></script>
+ <script src="/static/js/react-intl-2.0.0-beta-2/react-intl.js"></script>
+ <script src="/static/js/react-intl-2.0.0-beta-2/locale-data/en.js"></script>
+ <script src="/static/js/react-intl-2.0.0-beta-2/locale-data/es.js"></script>
+ <script src="/static/js/react-intl-2.0.0-beta-2/locale-data/pt.js"></script>
+ <script src="/static/js/jquery-2.1.4.js"></script>
+ <script src="/static/js/bootstrap-3.3.5.js"></script>
+ <script src="/static/js/bootstrap-colorpicker.min.js"></script>
+ <script src="/static/js/react-bootstrap-0.28.1.js"></script>
+ <script src="/static/js/velocity.min.js"></script>
+ <script src="/static/js/perfect-scrollbar-0.6.7.jquery.min.js"></script>
+ <script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
+ <script src="/static/js/babel-polyfill-6.1.18.min.js"></script>
+ <script src="/static/js/katex.min.js"></script>
+ <script src="/static/js/Chart.min.js"></script>
+
+ <style id="antiClickjack">body{display:none !important;}</style>
+
+ <script>
+ if ('ReactIntl' in window && 'ReactIntlLocaleData' in window) {
+ Object.keys(ReactIntlLocaleData).forEach(function(lang) {
+ ReactIntl.addLocaleData(ReactIntlLocaleData[lang]);
+ });
+ }
+
+ $(window).on('beforeunload', function(){
+ if (window.SocketStore) {
+ SocketStore.close();
+ }
+ });
+ </script>
+
+ <script src="/static/js/libs.min.js"></script>
+ <script src="/static/js/bundle.js"></script>
+
+ <script type="text/javascript">
+ if (self === top) {
+ var blocker = document.getElementById("antiClickjack");
+ blocker.parentNode.removeChild(blocker);
+ }
+ </script>
+</head>
+{{end}}
diff --git a/api/templates/invite_body.html b/templates/invite_body.html
index 2b6bde6d3..2b6bde6d3 100644
--- a/api/templates/invite_body.html
+++ b/templates/invite_body.html
diff --git a/api/templates/invite_subject.html b/templates/invite_subject.html
index 504915d50..504915d50 100644
--- a/api/templates/invite_subject.html
+++ b/templates/invite_subject.html
diff --git a/api/templates/password_change_body.html b/templates/password_change_body.html
index 2c4ba10ca..2c4ba10ca 100644
--- a/api/templates/password_change_body.html
+++ b/templates/password_change_body.html
diff --git a/api/templates/password_change_subject.html b/templates/password_change_subject.html
index 897f1210d..897f1210d 100644
--- a/api/templates/password_change_subject.html
+++ b/templates/password_change_subject.html
diff --git a/api/templates/post_body.html b/templates/post_body.html
index 54f34d1dd..54f34d1dd 100644
--- a/api/templates/post_body.html
+++ b/templates/post_body.html
diff --git a/templates/post_subject.html b/templates/post_subject.html
new file mode 100644
index 000000000..9789d4142
--- /dev/null
+++ b/templates/post_subject.html
@@ -0,0 +1 @@
+{{define "post_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
diff --git a/api/templates/reset_body.html b/templates/reset_body.html
index 69cd44957..69cd44957 100644
--- a/api/templates/reset_body.html
+++ b/templates/reset_body.html
diff --git a/api/templates/reset_subject.html b/templates/reset_subject.html
index a2852d332..a2852d332 100644
--- a/api/templates/reset_subject.html
+++ b/templates/reset_subject.html
diff --git a/templates/root.html b/templates/root.html
new file mode 100644
index 000000000..560c7a4b0
--- /dev/null
+++ b/templates/root.html
@@ -0,0 +1,12 @@
+{{define "root"}}
+<!DOCTYPE html>
+<html>
+{{template "head" . }}
+<body>
+ <div id='root'/>
+ <script>
+ window.setup_root();
+ </script>
+</body>
+</html>
+{{end}}
diff --git a/api/templates/signin_change_body.html b/templates/signin_change_body.html
index af8577f0f..af8577f0f 100644
--- a/api/templates/signin_change_body.html
+++ b/templates/signin_change_body.html
diff --git a/api/templates/signin_change_subject.html b/templates/signin_change_subject.html
index 606dc4df3..606dc4df3 100644
--- a/api/templates/signin_change_subject.html
+++ b/templates/signin_change_subject.html
diff --git a/api/templates/signup_team_body.html b/templates/signup_team_body.html
index 683a9891e..683a9891e 100644
--- a/api/templates/signup_team_body.html
+++ b/templates/signup_team_body.html
diff --git a/api/templates/signup_team_subject.html b/templates/signup_team_subject.html
index 413a5c8c1..413a5c8c1 100644
--- a/api/templates/signup_team_subject.html
+++ b/templates/signup_team_subject.html
diff --git a/api/templates/verify_body.html b/templates/verify_body.html
index 2b0d25f94..2b0d25f94 100644
--- a/api/templates/verify_body.html
+++ b/templates/verify_body.html
diff --git a/api/templates/verify_subject.html b/templates/verify_subject.html
index ad7fc2aaa..ad7fc2aaa 100644
--- a/api/templates/verify_subject.html
+++ b/templates/verify_subject.html
diff --git a/api/templates/welcome_body.html b/templates/welcome_body.html
index b5ca9beb3..b5ca9beb3 100644
--- a/api/templates/welcome_body.html
+++ b/templates/welcome_body.html
diff --git a/api/templates/welcome_subject.html b/templates/welcome_subject.html
index 95189b900..95189b900 100644
--- a/api/templates/welcome_subject.html
+++ b/templates/welcome_subject.html
diff --git a/tests/test-search.md b/tests/test-search.md
index 0f0ba1153..828828c25 100644
--- a/tests/test-search.md
+++ b/tests/test-search.md
@@ -40,4 +40,5 @@ Click on the linked hashtags below, and confirm that the search results match th
#### Markdown surrounding a hashtag:
-*#markdown-hashtag*
+*#markdown* **#markdown** ~~#markdown~~
+##### #markdown
diff --git a/utils/html.go b/utils/html.go
new file mode 100644
index 000000000..4203160d5
--- /dev/null
+++ b/utils/html.go
@@ -0,0 +1,97 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package utils
+
+import (
+ "bytes"
+ "html/template"
+ "net/http"
+
+ l4g "github.com/alecthomas/log4go"
+ "gopkg.in/fsnotify.v1"
+)
+
+// Global storage for templates
+var htmlTemplates *template.Template
+
+type HTMLTemplate struct {
+ TemplateName string
+ Props map[string]string
+ Html map[string]template.HTML
+ Locale string
+}
+
+func InitHTML() {
+ templatesDir := FindDir("templates")
+ l4g.Debug(T("api.api.init.parsing_templates.debug"), templatesDir)
+ var err error
+ if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil {
+ l4g.Error(T("api.api.init.parsing_templates.error"), err)
+ }
+
+ // Watch the templates folder for changes.
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ l4g.Error(T("web.create_dir.error"), err)
+ }
+
+ go func() {
+ for {
+ select {
+ case event := <-watcher.Events:
+ if event.Op&fsnotify.Write == fsnotify.Write {
+ l4g.Info(T("web.reparse_templates.info"), event.Name)
+ if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil {
+ l4g.Error(T("web.parsing_templates.error"), err)
+ }
+ }
+ case err := <-watcher.Errors:
+ l4g.Error(T("web.dir_fail.error"), err)
+ }
+ }
+ }()
+
+ err = watcher.Add(templatesDir)
+ if err != nil {
+ l4g.Error(T("web.watcher_fail.error"), err)
+ }
+}
+
+func NewHTMLTemplate(templateName string, locale string) *HTMLTemplate {
+ return &HTMLTemplate{
+ TemplateName: templateName,
+ Props: make(map[string]string),
+ Html: make(map[string]template.HTML),
+ Locale: locale,
+ }
+}
+
+func (t *HTMLTemplate) addDefaultProps() {
+ T := GetUserTranslations(t.Locale)
+ t.Props["Footer"] = T("api.templates.email_footer")
+ t.Html["EmailInfo"] = template.HTML(T("api.templates.email_info",
+ map[string]interface{}{"SupportEmail": Cfg.SupportSettings.SupportEmail, "SiteName": Cfg.TeamSettings.SiteName}))
+}
+
+func (t *HTMLTemplate) Render() string {
+ t.addDefaultProps()
+
+ var text bytes.Buffer
+
+ if err := htmlTemplates.ExecuteTemplate(&text, t.TemplateName, t); err != nil {
+ l4g.Error(T("api.api.render.error"), t.TemplateName, err)
+ }
+
+ return text.String()
+}
+
+func (t *HTMLTemplate) RenderToWriter(w http.ResponseWriter) error {
+ t.addDefaultProps()
+
+ if err := htmlTemplates.ExecuteTemplate(w, t.TemplateName, t); err != nil {
+ l4g.Error(T("api.api.render.error"), t.TemplateName, err)
+ return err
+ }
+ return nil
+}
diff --git a/utils/license.go b/utils/license.go
index b773a163e..b1f15ad92 100644
--- a/utils/license.go
+++ b/utils/license.go
@@ -20,17 +20,16 @@ import (
var IsLicensed bool = false
var License *model.License = &model.License{}
-var ClientLicense map[string]string = make(map[string]string)
+var ClientLicense map[string]string = map[string]string{"IsLicensed": "false"}
-// test public key
var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY-----
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3/k3Al9q1Xe+xngQ/yGn
-0suaJopea3Cpf6NjIHdO8sYTwLlxqt0Mdb+qBR9LbCjZfcNmqc5mZONvsyCEoN/5
-VoLdlv1m9ao2BSAWphUxE2CPdUWdLOsDbQWliSc5//UhiYeR+67Xxon0Hg0LKXF6
-PumRIWQenRHJWqlUQZ147e7/1v9ySVRZksKpvlmMDzgq+kCH/uyM1uVP3z7YXhlN
-K7vSSQYbt4cghvWQxDZFwpLlsChoY+mmzClgq+Yv6FLhj4/lk94twdOZau/AeZFJ
-NxpC+5KFhU+xSeeklNqwCgnlOyZ7qSTxmdJHb+60SwuYnnGIYzLJhY4LYDr4J+KR
-1wIDAQAB
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r
+tSyzyxrXkJjsFUf0Ke7bm/TLtIggRdqOcUF3XEWqQk5RGD5vuq7Rlg1zZqMEBk8N
+EZeRhkxyaZW8pLjxwuBUOnXfJew31+gsTNdKZzRjrvPumKr3EtkleuoxNdoatu4E
+HrKmR/4Yi71EqAvkhk7ZjQFuF0osSWJMEEGGCSUYQnTEqUzcZSh1BhVpkIkeu8Kk
+1wCtptODixvEujgqVe+SrE3UlZjBmPjC/CL+3cYmufpSNgcEJm2mwsdaXp2OPpfn
+a0v85XL6i9ote2P+fLZ3wX9EoioHzgdgB7arOxY50QRJO7OyCqpKFKv6lRWTXuSt
+hwIDAQAB
-----END PUBLIC KEY-----`)
func LoadLicense(licenseBytes []byte) {
diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/action_creators/global_actions.jsx
index 367347d4b..4375d6c87 100644
--- a/web/react/dispatcher/event_helpers.jsx
+++ b/web/react/action_creators/global_actions.jsx
@@ -220,3 +220,33 @@ export function sendEphemeralPost(message, channelId) {
emitPostRecievedEvent(post);
}
+
+export function loadTeamRequiredPage() {
+ AsyncClient.getAllTeams();
+}
+
+export function newLocalizationSelected(locale) {
+ Client.getTranslations(
+ locale,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_LOCALE,
+ locale,
+ translations: data
+ });
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getTranslations');
+ }
+ );
+}
+
+export function viewLoggedIn() {
+ AsyncClient.getChannels();
+ AsyncClient.getChannelExtraInfo();
+ AsyncClient.getMyTeam();
+ AsyncClient.getMe();
+
+ // Clear pending posts (shouldn't have pending posts if we are loading)
+ PostStore.clearPendingPosts();
+}
diff --git a/web/react/components/about_build_modal.jsx b/web/react/components/about_build_modal.jsx
index fe48bb48e..34b1fdccf 100644
--- a/web/react/components/about_build_modal.jsx
+++ b/web/react/components/about_build_modal.jsx
@@ -21,29 +21,38 @@ export default class AboutBuildModal extends React.Component {
let title = (
<FormattedMessage
- id='about.teamEdtion'
- defaultMessage='Team Edition'
+ id='about.teamEditiont0'
+ defaultMessage='Team Edition T0'
/>
);
+
let licensee;
- if (config.BuildEnterpriseReady === 'true' && license.IsLicensed === 'true') {
+ if (config.BuildEnterpriseReady === 'true') {
title = (
<FormattedMessage
- id='about.enterpriseEdition'
- defaultMessage='Enterprise Edition'
+ id='about.teamEditiont1'
+ defaultMessage='Team Edition T1'
/>
);
- licensee = (
- <div className='row form-group'>
- <div className='col-sm-3 info__label'>
- <FormattedMessage
- id='about.licensed'
- defaultMessage='Licensed by:'
- />
+ if (license.IsLicensed === 'true') {
+ title = (
+ <FormattedMessage
+ id='about.enterpriseEditione1'
+ defaultMessage='Enterprise Edition E1'
+ />
+ );
+ licensee = (
+ <div className='row form-group'>
+ <div className='col-sm-3 info__label'>
+ <FormattedMessage
+ id='about.licensed'
+ defaultMessage='Licensed by:'
+ />
+ </div>
+ <div className='col-sm-9'>{license.Company}</div>
</div>
- <div className='col-sm-9'>{license.Company}</div>
- </div>
- );
+ );
+ }
}
return (
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx
index 95b4caa12..db366f8ed 100644
--- a/web/react/components/activity_log_modal.jsx
+++ b/web/react/components/activity_log_modal.jsx
@@ -8,7 +8,7 @@ const Modal = ReactBootstrap.Modal;
import LoadingScreen from './loading_screen.jsx';
import * as Utils from '../utils/utils.jsx';
-import {FormattedMessage} from 'mm-intl';
+import {FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl';
export default class ActivityLogModal extends React.Component {
constructor(props) {
@@ -144,8 +144,21 @@ export default class ActivityLogModal extends React.Component {
id='activity_log.firstTime'
defaultMessage='First time active: {date}, {time}'
values={{
- date: firstAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}),
- time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'})
+ date: (
+ <FormattedDate
+ value={firstAccessTime}
+ day='2-digit'
+ month='long'
+ year='numeric'
+ />
+ ),
+ time: (
+ <FormattedTime
+ value={firstAccessTime}
+ hour='2-digit'
+ minute='2-digit'
+ />
+ )
}}
/>
</div>
@@ -206,8 +219,21 @@ export default class ActivityLogModal extends React.Component {
id='activity_log.lastActivity'
defaultMessage='Last activity: {date}, {time}'
values={{
- date: lastAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}),
- time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'})
+ date: (
+ <FormattedDate
+ value={lastAccessTime}
+ day='2-digit'
+ month='long'
+ year='numeric'
+ />
+ ),
+ time: (
+ <FormattedTime
+ value={lastAccessTime}
+ hour='2-digit'
+ minute='2-digit'
+ />
+ )
}}
/>
</div>
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index 32ed70a99..4c4f21f08 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -6,7 +6,6 @@ import AdminStore from '../../stores/admin_store.jsx';
import TeamStore from '../../stores/team_store.jsx';
import * as AsyncClient from '../../utils/async_client.jsx';
import LoadingScreen from '../loading_screen.jsx';
-import * as Utils from '../../utils/utils.jsx';
import EmailSettingsTab from './email_settings.jsx';
import LogSettingsTab from './log_settings.jsx';
@@ -50,11 +49,6 @@ export default class AdminController extends React.Component {
selected: props.tab || 'system_analytics',
selectedTeam: props.teamId || null
};
-
- if (!props.tab) {
- var tokenIndex = Utils.getUrlParameter('session_token_index');
- history.replaceState(null, null, `/admin_console/${this.state.selected}?session_token_index=${tokenIndex}`);
- }
}
componentDidMount() {
@@ -63,6 +57,9 @@ export default class AdminController extends React.Component {
AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
AsyncClient.getAllTeams();
+
+ $('[data-toggle="tooltip"]').tooltip();
+ $('[data-toggle="popover"]').popover();
}
componentWillUnmount() {
@@ -175,7 +172,7 @@ export default class AdminController extends React.Component {
}
return (
- <div>
+ <div id='admin_controller'>
<div
className='sidebar--menu'
id='sidebar-menu'
diff --git a/web/react/components/admin_console/admin_navbar_dropdown.jsx b/web/react/components/admin_console/admin_navbar_dropdown.jsx
index dc0b3c4cb..ae95f5a3a 100644
--- a/web/react/components/admin_console/admin_navbar_dropdown.jsx
+++ b/web/react/components/admin_console/admin_navbar_dropdown.jsx
@@ -2,13 +2,14 @@
// See License.txt for license information.
import * as Utils from '../../utils/utils.jsx';
-import * as Client from '../../utils/client.jsx';
import TeamStore from '../../stores/team_store.jsx';
import Constants from '../../utils/constants.jsx';
import {FormattedMessage} from 'mm-intl';
+import {Link} from 'react-router';
+
function getStateFromStores() {
return {currentTeam: TeamStore.getCurrent()};
}
@@ -18,16 +19,9 @@ export default class AdminNavbarDropdown extends React.Component {
super(props);
this.blockToggle = false;
- this.handleLogoutClick = this.handleLogoutClick.bind(this);
-
this.state = getStateFromStores();
}
- handleLogoutClick(e) {
- e.preventDefault();
- Client.logout();
- }
-
componentDidMount() {
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
this.blockToggle = true;
@@ -78,15 +72,12 @@ export default class AdminNavbarDropdown extends React.Component {
</a>
</li>
<li>
- <a
- href='#'
- onClick={this.handleLogoutClick}
- >
+ <Link to={Utils.getTeamURLFromAddressBar() + '/logout'}>
<FormattedMessage
id='admin.nav.logout'
defaultMessage='Logout'
/>
- </a>
+ </Link>
</li>
<li className='divider'></li>
<li>
@@ -116,4 +107,4 @@ export default class AdminNavbarDropdown extends React.Component {
</ul>
);
}
-} \ No newline at end of file
+}
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index 6621e5743..c2f31f569 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -3,7 +3,6 @@
import AdminSidebarHeader from './admin_sidebar_header.jsx';
import SelectTeamModal from './select_team_modal.jsx';
-import * as Utils from '../../utils/utils.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -30,8 +29,6 @@ export default class AdminSidebar extends React.Component {
handleClick(name, teamId, e) {
e.preventDefault();
this.props.selectTab(name, teamId);
- var tokenIndex = Utils.getUrlParameter('session_token_index');
- history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}?session_token_index=${tokenIndex}`);
}
isSelected(name, teamId) {
@@ -73,7 +70,6 @@ export default class AdminSidebar extends React.Component {
}
teamSelectedModal(teamId) {
- this.props.selectedTeams[teamId] = 'true';
this.setState({showSelectModal: false});
this.props.addSelectedTeam(teamId);
this.forceUpdate();
diff --git a/web/react/components/admin_console/admin_sidebar_header.jsx b/web/react/components/admin_console/admin_sidebar_header.jsx
index 8c9f74934..f1281c6ee 100644
--- a/web/react/components/admin_console/admin_sidebar_header.jsx
+++ b/web/react/components/admin_console/admin_sidebar_header.jsx
@@ -3,7 +3,6 @@
import AdminNavbarDropdown from './admin_navbar_dropdown.jsx';
import UserStore from '../../stores/user_store.jsx';
-import * as Utils from '../../utils/utils.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -39,7 +38,7 @@ export default class SidebarHeader extends React.Component {
profilePicture = (
<img
className='user__picture'
- src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()}
+ src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
/>
);
}
@@ -65,4 +64,4 @@ export default class SidebarHeader extends React.Component {
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/web/react/components/admin_console/ldap_settings.jsx b/web/react/components/admin_console/ldap_settings.jsx
index 535c264dd..4cd19c886 100644
--- a/web/react/components/admin_console/ldap_settings.jsx
+++ b/web/react/components/admin_console/ldap_settings.jsx
@@ -20,7 +20,7 @@ var holders = defineMessages({
},
baseEx: {
id: 'admin.ldap.baseEx',
- defaultMessage: 'Ex "dc=mydomain,dc=com"'
+ defaultMessage: 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"'
},
firstnameAttrEx: {
id: 'admin.ldap.firstnameAttrEx',
@@ -32,7 +32,7 @@ var holders = defineMessages({
},
emailAttrEx: {
id: 'admin.ldap.emailAttrEx',
- defaultMessage: 'Ex "mail"'
+ defaultMessage: 'Ex "mail" or "userPrincipalName"'
},
usernameAttrEx: {
id: 'admin.ldap.usernameAttrEx',
@@ -581,4 +581,4 @@ LdapSettings.propTypes = {
config: React.PropTypes.object
};
-export default injectIntl(LdapSettings); \ No newline at end of file
+export default injectIntl(LdapSettings);
diff --git a/web/react/components/admin_console/license_settings.jsx b/web/react/components/admin_console/license_settings.jsx
index d4dfa13f2..9d2ec8030 100644
--- a/web/react/components/admin_console/license_settings.jsx
+++ b/web/react/components/admin_console/license_settings.jsx
@@ -27,6 +27,7 @@ class LicenseSettings extends React.Component {
this.state = {
fileSelected: false,
+ fileName: null,
serverError: null
};
}
@@ -34,7 +35,7 @@ class LicenseSettings extends React.Component {
handleChange() {
const element = $(ReactDOM.findDOMNode(this.refs.fileInput));
if (element.prop('files').length > 0) {
- this.setState({fileSelected: true});
+ this.setState({fileSelected: true, fileName: element.prop('files')[0].name});
}
}
@@ -56,13 +57,13 @@ class LicenseSettings extends React.Component {
() => {
Utils.clearFileInput(element[0]);
$('#upload-button').button('reset');
- this.setState({serverError: null});
+ this.setState({fileSelected: false, fileName: null, serverError: null});
window.location.reload(true);
},
(error) => {
Utils.clearFileInput(element[0]);
$('#upload-button').button('reset');
- this.setState({serverError: error.message});
+ this.setState({fileSelected: false, fileName: null, serverError: error.message});
}
);
}
@@ -75,12 +76,12 @@ class LicenseSettings extends React.Component {
Client.removeLicenseFile(
() => {
$('#remove-button').button('reset');
- this.setState({serverError: null});
+ this.setState({fileSelected: false, fileName: null, serverError: null});
window.location.reload(true);
},
(error) => {
$('#remove-button').button('reset');
- this.setState({serverError: error.message});
+ this.setState({fileSelected: false, fileName: null, serverError: error.message});
}
);
}
@@ -172,17 +173,36 @@ class LicenseSettings extends React.Component {
/>
);
+ let fileName;
+ if (this.state.fileName) {
+ fileName = this.state.fileName;
+ } else {
+ fileName = (
+ <FormattedMessage
+ id='admin.license.noFile'
+ defaultMessage='No file uploaded'
+ />
+ );
+ }
+
licenseKey = (
<div className='col-sm-8'>
- <input
- className='pull-left'
- ref='fileInput'
- type='file'
- accept='.mattermost-license'
- onChange={this.handleChange}
- />
+ <div className='file__upload'>
+ <button className='btn btn-default'>
+ <FormattedMessage
+ id='admin.license.choose'
+ defaultMessage='Choose File'
+ />
+ </button>
+ <input
+ ref='fileInput'
+ type='file'
+ accept='.mattermost-license'
+ onChange={this.handleChange}
+ />
+ </div>
<button
- className={btnClass + ' pull-left'}
+ className={btnClass}
disabled={!this.state.fileSelected}
onClick={this.handleSubmit}
id='upload-button'
@@ -193,11 +213,12 @@ class LicenseSettings extends React.Component {
defaultMessage='Upload'
/>
</button>
- <br/>
- <br/>
+ <div className='help-text no-margin'>
+ {fileName}
+ </div>
<br/>
{serverError}
- <p className='help-text'>
+ <p className='help-text no-margin'>
<FormattedHTMLMessage
id='admin.license.uploadDesc'
defaultMessage='Upload a license key for Mattermost Enterprise Edition to upgrade this server. <a href="http://mattermost.com" target="_blank">Visit us online</a> to learn more about the benefits of Enterprise Edition or to purchase a key.'
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
index 4af350bcd..7d6cfb5c3 100644
--- a/web/react/components/admin_console/user_item.jsx
+++ b/web/react/components/admin_console/user_item.jsx
@@ -366,7 +366,7 @@ export default class UserItem extends React.Component {
<td className='row member-div padding--equal'>
<img
className='post-profile-img pull-left'
- src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
height='36'
width='36'
/>
diff --git a/web/react/components/audit_table.jsx b/web/react/components/audit_table.jsx
index 47eee6d3f..917093840 100644
--- a/web/react/components/audit_table.jsx
+++ b/web/react/components/audit_table.jsx
@@ -5,7 +5,7 @@ import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import * as Utils from '../utils/utils.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
+import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate, FormattedTime} from 'mm-intl';
const holders = defineMessages({
sessionRevoked: {
@@ -598,8 +598,23 @@ export function formatAuditInfo(audit, formatMessage) {
}
const date = new Date(audit.create_at);
- let auditInfo = {};
- auditInfo.timestamp = date.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + date.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'});
+ const auditInfo = {};
+ auditInfo.timestamp = (
+ <div>
+ <FormattedDate
+ value={date}
+ day='2-digit'
+ month='short'
+ year='numeric'
+ />
+ {' - '}
+ <FormattedTime
+ value={date}
+ hour='2-digit'
+ minute='2-digit'
+ />
+ </div>
+ );
auditInfo.userId = audit.user_id;
auditInfo.desc = auditDesc;
auditInfo.ip = audit.ip_address;
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
index 2422588cf..2ea840c1e 100644
--- a/web/react/components/center_panel.jsx
+++ b/web/react/components/center_panel.jsx
@@ -25,40 +25,43 @@ export default class CenterPanel extends React.Component {
constructor(props) {
super(props);
- this.onPreferenceChange = this.onPreferenceChange.bind(this);
- this.onChannelChange = this.onChannelChange.bind(this);
- this.onUserChange = this.onUserChange.bind(this);
+ this.getStateFromStores = this.getStateFromStores.bind(this);
+ this.validState = this.validState.bind(this);
+ this.onStoresChange = this.onStoresChange.bind(this);
+ this.state = this.getStateFromStores();
+ }
+ getStateFromStores() {
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
- this.state = {
- showTutorialScreens: tutorialStep === TutorialSteps.INTRO_SCREENS,
+ return {
+ showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS,
showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS,
user: UserStore.getCurrentUser(),
+ channel: ChannelStore.getCurrent(),
profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))
};
}
- componentDidMount() {
- PreferenceStore.addChangeListener(this.onPreferenceChange);
- ChannelStore.addChangeListener(this.onChannelChange);
- UserStore.addChangeListener(this.onUserChange);
- }
- componentWillUnmount() {
- PreferenceStore.removeChangeListener(this.onPreferenceChange);
- ChannelStore.removeChangeListener(this.onChannelChange);
- UserStore.removeChangeListener(this.onUserChange);
+ validState() {
+ return this.state.user && this.state.channel && this.state.profiles;
}
- onPreferenceChange() {
- const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
- this.setState({showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS});
+ onStoresChange() {
+ this.setState(this.getStateFromStores());
}
- onChannelChange() {
- this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS});
+ componentDidMount() {
+ PreferenceStore.addChangeListener(this.onStoresChange);
+ ChannelStore.addChangeListener(this.onStoresChange);
+ UserStore.addChangeListener(this.onStoresChange);
}
- onUserChange() {
- this.setState({user: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))});
+ componentWillUnmount() {
+ PreferenceStore.removeChangeListener(this.onStoresChange);
+ ChannelStore.removeChangeListener(this.onStoresChange);
+ UserStore.removeChangeListener(this.onStoresChange);
}
render() {
- const channel = ChannelStore.getCurrent();
+ if (!this.validState()) {
+ return null;
+ }
+ const channel = this.state.channel;
var handleClick = null;
let postsContainer;
let createPost;
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 51be13dcf..882c575f0 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -57,20 +57,33 @@ export default class ChannelHeader extends React.Component {
memberChannel: ChannelStore.getCurrentMember(),
users: extraInfo.members,
userCount: extraInfo.member_count,
- searchVisible: SearchStore.getSearchResults() !== null
+ searchVisible: SearchStore.getSearchResults() !== null,
+ currentUser: UserStore.getCurrentUser()
};
}
+ validState() {
+ if (!this.state.channel ||
+ !this.state.memberChannel ||
+ !this.state.users ||
+ !this.state.userCount ||
+ !this.state.currentUser) {
+ return false;
+ }
+ return true;
+ }
componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange);
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
SearchStore.addSearchChangeListener(this.onListenerChange);
PreferenceStore.addChangeListener(this.onListenerChange);
+ UserStore.addChangeListener(this.onListenerChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
SearchStore.removeSearchChangeListener(this.onListenerChange);
PreferenceStore.removeChangeListener(this.onListenerChange);
+ UserStore.removeChangeListener(this.onListenerChange);
}
onListenerChange() {
const newState = this.getStateFromStores();
@@ -98,7 +111,7 @@ export default class ChannelHeader extends React.Component {
searchMentions(e) {
e.preventDefault();
- const user = this.props.user;
+ const user = this.state.currentUser;
let terms = '';
if (user.notify_props && user.notify_props.mention_keys) {
@@ -134,7 +147,7 @@ export default class ChannelHeader extends React.Component {
});
}
render() {
- if (this.state.channel === null) {
+ if (!this.validState()) {
return null;
}
@@ -163,8 +176,8 @@ export default class ChannelHeader extends React.Component {
</Popover>
);
let channelTitle = channel.display_name;
- const currentId = this.props.user.id;
- const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.props.user.roles);
+ const currentId = this.state.currentUser.id;
+ const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.state.currentUser.roles);
const isDirect = (this.state.channel.type === 'D');
if (isDirect) {
@@ -252,7 +265,7 @@ export default class ChannelHeader extends React.Component {
<ToggleModalButton
role='menuitem'
dialogType={ChannelInviteModal}
- dialogProps={{channel}}
+ dialogProps={{channel, currentUser: this.state.currentUser}}
>
<FormattedMessage
id='chanel_header.addMembers'
@@ -331,7 +344,11 @@ export default class ChannelHeader extends React.Component {
<ToggleModalButton
role='menuitem'
dialogType={ChannelNotificationsModal}
- dialogProps={{channel}}
+ dialogProps={{
+ channel,
+ channelMember: this.state.memberChannel,
+ currentUser: this.state.currentUser
+ }}
>
<FormattedMessage
id='channel_header.notificationPreferences'
@@ -497,5 +514,4 @@ export default class ChannelHeader extends React.Component {
}
ChannelHeader.propTypes = {
- user: React.PropTypes.object.isRequired
};
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 6c8d51abb..4157812a9 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -4,8 +4,8 @@
import FilteredUserList from './filtered_user_list.jsx';
import LoadingScreen from './loading_screen.jsx';
-import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
+import UserStore from '../stores/user_store.jsx';
import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
@@ -16,18 +16,15 @@ import {FormattedMessage} from 'mm-intl';
const Modal = ReactBootstrap.Modal;
export default class ChannelInviteModal extends React.Component {
- constructor() {
- super();
+ constructor(props) {
+ super(props);
this.onListenerChange = this.onListenerChange.bind(this);
this.handleInvite = this.handleInvite.bind(this);
-
+ this.getStateFromStores = this.getStateFromStores.bind(this);
this.createInviteButton = this.createInviteButton.bind(this);
- // the state gets populated when the modal is shown
- this.state = {
- loading: true
- };
+ this.state = this.getStateFromStores();
}
shouldComponentUpdate(nextProps, nextState) {
if (!this.props.show && !nextProps.show) {
@@ -63,6 +60,20 @@ export default class ChannelInviteModal extends React.Component {
};
}
+ const currentUser = UserStore.getCurrentUser();
+ if (!currentUser) {
+ return {
+ loading: true
+ };
+ }
+
+ const currentMember = ChannelStore.getCurrentMember();
+ if (!currentMember) {
+ return {
+ loading: true
+ };
+ }
+
const memberIds = extraInfo.members.map((user) => user.id);
var nonmembers = [];
@@ -78,7 +89,9 @@ export default class ChannelInviteModal extends React.Component {
return {
nonmembers,
- loading: false
+ loading: false,
+ currentUser,
+ currentMember
};
}
componentWillReceiveProps(nextProps) {
@@ -93,6 +106,11 @@ export default class ChannelInviteModal extends React.Component {
UserStore.removeChangeListener(this.onListenerChange);
}
}
+ componentWillUnmount() {
+ ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
+ ChannelStore.removeChangeListener(this.onListenerChange);
+ UserStore.removeChangeListener(this.onListenerChange);
+ }
onListenerChange() {
var newState = this.getStateFromStores();
if (!Utils.areObjectsEqual(this.state, newState)) {
@@ -144,7 +162,6 @@ export default class ChannelInviteModal extends React.Component {
if (Utils.windowHeight() <= 1200) {
maxHeight = Utils.windowHeight() - 300;
}
-
content = (
<FilteredUserList
style={{maxHeight}}
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
deleted file mode 100644
index e47f2aa50..000000000
--- a/web/react/components/channel_loader.jsx
+++ /dev/null
@@ -1,204 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-/* This is a special React control with the sole purpose of making all the AsyncClient calls
- to the server on page load. This is to prevent other React controls from spamming
- AsyncClient with requests. */
-
-import * as AsyncClient from '../utils/async_client.jsx';
-import * as Client from '../utils/client.jsx';
-import SocketStore from '../stores/socket_store.jsx';
-import ChannelStore from '../stores/channel_store.jsx';
-import PostStore from '../stores/post_store.jsx';
-import UserStore from '../stores/user_store.jsx';
-import PreferenceStore from '../stores/preference_store.jsx';
-
-import * as Utils from '../utils/utils.jsx';
-import Constants from '../utils/constants.jsx';
-
-import {intlShape, injectIntl, defineMessages} from 'mm-intl';
-
-const holders = defineMessages({
- socketError: {
- id: 'channel_loader.socketError',
- defaultMessage: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.'
- },
- someone: {
- id: 'channel_loader.someone',
- defaultMessage: 'Someone'
- },
- posted: {
- id: 'channel_loader.posted',
- defaultMessage: 'Posted'
- },
- uploadedImage: {
- id: 'channel_loader.uploadedImage',
- defaultMessage: ' uploaded an image'
- },
- uploadedFile: {
- id: 'channel_loader.uploadedFile',
- defaultMessage: ' uploaded a file'
- },
- something: {
- id: 'channel_loader.something',
- defaultMessage: ' did something new'
- },
- wrote: {
- id: 'channel_loader.wrote',
- defaultMessage: ' wrote: '
- },
- connectionError: {
- id: 'channel_loader.connection_error',
- defaultMessage: 'There appears to be a problem with your internet connection.'
- },
- unknownError: {
- id: 'channel_loader.unknown_error',
- defaultMessage: 'We received an unexpected status code from the server.'
- }
-});
-
-class ChannelLoader extends React.Component {
- constructor(props) {
- super(props);
-
- this.intervalId = null;
-
- this.onSocketChange = this.onSocketChange.bind(this);
-
- const {formatMessage} = this.props.intl;
- SocketStore.setTranslations({
- socketError: formatMessage(holders.socketError),
- someone: formatMessage(holders.someone),
- posted: formatMessage(holders.posted),
- uploadedImage: formatMessage(holders.uploadedImage),
- uploadedFile: formatMessage(holders.uploadedFile),
- something: formatMessage(holders.something),
- wrote: formatMessage(holders.wrote)
- });
-
- Client.setTranslations({
- connectionError: formatMessage(holders.connectionError),
- unknownError: formatMessage(holders.unknownError)
- });
-
- this.state = {};
- }
- componentDidMount() {
- /* Initial aysnc loads */
- AsyncClient.getPosts(ChannelStore.getCurrentId());
- AsyncClient.getChannels();
- AsyncClient.getChannelExtraInfo();
- AsyncClient.findTeams();
- AsyncClient.getMyTeam();
- setTimeout(() => AsyncClient.getStatuses(), 3000); // temporary until statuses are reworked a bit
-
- /* Perform pending post clean-up */
- PostStore.clearPendingPosts();
-
- /* Set up interval functions */
- this.intervalId = setInterval(() => AsyncClient.getStatuses(), 30000);
-
- /* Device tracking setup */
- var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
- if (iOS) {
- $('body').addClass('ios');
- }
-
- /* Set up tracking for whether the window is active */
- window.isActive = true;
-
- $(window).on('focus', function windowFocus() {
- AsyncClient.updateLastViewedAt();
- ChannelStore.resetCounts(ChannelStore.getCurrentId());
- ChannelStore.emitChange();
- window.isActive = true;
- });
-
- $(window).on('blur', function windowBlur() {
- window.isActive = false;
- });
-
- /* Start global change listeners setup */
- SocketStore.addChangeListener(this.onSocketChange);
-
- /* Update CSS classes to match user theme */
- var user = UserStore.getCurrentUser();
-
- if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
- Utils.applyTheme(user.theme_props);
- } else {
- Utils.applyTheme(Constants.THEMES.default);
- }
-
- // if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx
- const selectedFont = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT);
- Utils.applyFont(selectedFont);
-
- $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
- if (ev.type === 'mouseenter') {
- $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
- $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
- } else {
- $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
- $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
- }
- });
-
- $('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) {
- if (ev.type === 'mouseenter') {
- $(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after');
- $(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before');
- } else {
- $(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after');
- $(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before');
- }
- });
-
- $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) {
- if (ev.type === 'mouseenter') {
- $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment');
- $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment');
- } else {
- $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment');
- $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
- }
- });
-
- /* Prevent backspace from navigating back a page */
- $(window).on('keydown.preventBackspace', (e) => {
- if (e.which === 8 && !$(e.target).is('input, textarea')) {
- e.preventDefault();
- }
- });
- }
- componentWillUnmount() {
- clearInterval(this.intervalId);
-
- $(window).off('focus');
- $(window).off('blur');
-
- SocketStore.removeChangeListener(this.onSocketChange);
-
- $('body').off('click.userpopover');
- $('body').off('mouseenter mouseleave', '.post');
- $('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
-
- $('.modal').off('show.bs.modal');
-
- $(window).off('keydown.preventBackspace');
- }
- onSocketChange(msg) {
- if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
- UserStore.setStatus(msg.user_id, 'online');
- }
- }
- render() {
- return <div/>;
- }
-}
-
-ChannelLoader.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(ChannelLoader);
diff --git a/web/react/components/channel_notifications_modal.jsx b/web/react/components/channel_notifications_modal.jsx
index 7048434f8..acefaf024 100644
--- a/web/react/components/channel_notifications_modal.jsx
+++ b/web/react/components/channel_notifications_modal.jsx
@@ -6,7 +6,6 @@ import SettingItemMin from './setting_item_min.jsx';
import SettingItemMax from './setting_item_max.jsx';
import * as Client from '../utils/client.jsx';
-import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -15,7 +14,6 @@ export default class ChannelNotificationsModal extends React.Component {
constructor(props) {
super(props);
- this.onListenerChange = this.onListenerChange.bind(this);
this.updateSection = this.updateSection.bind(this);
this.handleSubmitNotifyLevel = this.handleSubmitNotifyLevel.bind(this);
@@ -26,58 +24,41 @@ export default class ChannelNotificationsModal extends React.Component {
this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this);
this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this);
- const member = ChannelStore.getMember(props.channel.id);
this.state = {
- notifyLevel: member.notify_props.desktop,
- markUnreadLevel: member.notify_props.mark_unread,
- channelId: ChannelStore.getCurrentId(),
- activeSection: ''
+ activeSection: '',
+ notifyLevel: '',
+ unreadLevel: ''
};
}
+ updateSection(section) {
+ this.setState({activeSection: section});
+ }
componentWillReceiveProps(nextProps) {
if (!this.props.show && nextProps.show) {
- this.onListenerChange();
- ChannelStore.addChangeListener(this.onListenerChange);
- } else {
- ChannelStore.removeChangeListener(this.onListenerChange);
+ this.setState({
+ notifyLevel: nextProps.channelMember.notify_props.desktop,
+ unreadLevel: nextProps.channelMember.notify_props.mark_unread
+ });
}
}
- onListenerChange() {
- const curChannelId = ChannelStore.getCurrentId();
-
- if (!curChannelId) {
- return;
- }
-
- const newState = {channelId: curChannelId};
- const member = ChannelStore.getMember(curChannelId);
-
- if (member.notify_props.desktop !== this.state.notifyLevel || member.notify_props.mark_unread !== this.state.mark_unread) {
- newState.notifyLevel = member.notify_props.desktop;
- newState.markUnreadLevel = member.notify_props.mark_unread;
- }
-
- this.setState(newState);
- }
- updateSection(section) {
- this.setState({activeSection: section});
- }
handleSubmitNotifyLevel() {
- var channelId = this.state.channelId;
+ var channelId = this.props.channel.id;
var notifyLevel = this.state.notifyLevel;
- if (ChannelStore.getMember(channelId).notify_props.desktop === notifyLevel) {
+ if (this.props.channelMember.notify_props.desktop === notifyLevel) {
this.updateSection('');
return;
}
var data = {};
data.channel_id = channelId;
- data.user_id = UserStore.getCurrentId();
+ data.user_id = this.props.currentUser.id;
data.desktop = notifyLevel;
+ //TODO: This should be moved to event_helpers
Client.updateNotifyProps(data,
() => {
+ // YUCK
var member = ChannelStore.getMember(channelId);
member.notify_props.desktop = notifyLevel;
ChannelStore.setChannelMember(member);
@@ -92,11 +73,8 @@ export default class ChannelNotificationsModal extends React.Component {
this.setState({notifyLevel});
}
createNotifyLevelSection(serverError) {
- var handleUpdateSection;
-
- const user = UserStore.getCurrentUser();
- const globalNotifyLevel = user.notify_props.desktop;
-
+ // Get glabal user setting for notifications
+ const globalNotifyLevel = this.props.currentUser.notify_props.desktop;
let globalNotifyLevelName;
if (globalNotifyLevel === 'all') {
globalNotifyLevelName = (
@@ -128,13 +106,15 @@ export default class ChannelNotificationsModal extends React.Component {
/>
);
+ const notificationLevel = this.state.notifyLevel;
+
if (this.state.activeSection === 'desktop') {
- var notifyActive = [false, false, false, false];
- if (this.state.notifyLevel === 'default') {
+ const notifyActive = [false, false, false, false];
+ if (notificationLevel === 'default') {
notifyActive[0] = true;
- } else if (this.state.notifyLevel === 'all') {
+ } else if (notificationLevel === 'all') {
notifyActive[1] = true;
- } else if (this.state.notifyLevel === 'mention') {
+ } else if (notificationLevel === 'mention') {
notifyActive[2] = true;
} else {
notifyActive[3] = true;
@@ -196,7 +176,7 @@ export default class ChannelNotificationsModal extends React.Component {
</div>
);
- handleUpdateSection = function updateSection(e) {
+ const handleUpdateSection = function updateSection(e) {
this.updateSection('');
this.onListenerChange();
e.preventDefault();
@@ -224,7 +204,7 @@ export default class ChannelNotificationsModal extends React.Component {
}
var describe;
- if (this.state.notifyLevel === 'default') {
+ if (notificationLevel === 'default') {
describe = (
<FormattedMessage
id='channel_notifications.globalDefault'
@@ -233,45 +213,44 @@ export default class ChannelNotificationsModal extends React.Component {
}}
/>
);
- } else if (this.state.notifyLevel === 'mention') {
+ } else if (notificationLevel === 'mention') {
describe = (<FormattedMessage id='channel_notifications.onlyMentions'/>);
- } else if (this.state.notifyLevel === 'all') {
+ } else if (notificationLevel === 'all') {
describe = (<FormattedMessage id='channel_notifications.allActivity'/>);
} else {
describe = (<FormattedMessage id='channel_notifications.never'/>);
}
- handleUpdateSection = function updateSection(e) {
- this.updateSection('desktop');
- e.preventDefault();
- }.bind(this);
-
return (
<SettingItemMin
title={sendDesktop}
describe={describe}
- updateSection={handleUpdateSection}
+ updateSection={() => {
+ this.updateSection('desktop');
+ }}
/>
);
}
handleSubmitMarkUnreadLevel() {
- const channelId = this.state.channelId;
- const markUnreadLevel = this.state.markUnreadLevel;
+ const channelId = this.props.channel.id;
+ const markUnreadLevel = this.state.unreadLevel;
- if (ChannelStore.getMember(channelId).notify_props.mark_unread === markUnreadLevel) {
+ if (this.props.channelMember.notify_props.mark_unread === markUnreadLevel) {
this.updateSection('');
return;
}
const data = {
channel_id: channelId,
- user_id: UserStore.getCurrentId(),
+ user_id: this.props.currentUser.id,
mark_unread: markUnreadLevel
};
+ //TODO: This should be fixed, moved to event_helpers
Client.updateNotifyProps(data,
() => {
+ // Yuck...
var member = ChannelStore.getMember(channelId);
member.notify_props.mark_unread = markUnreadLevel;
ChannelStore.setChannelMember(member);
@@ -283,8 +262,8 @@ export default class ChannelNotificationsModal extends React.Component {
);
}
- handleUpdateMarkUnreadLevel(markUnreadLevel) {
- this.setState({markUnreadLevel});
+ handleUpdateMarkUnreadLevel(unreadLevel) {
+ this.setState({unreadLevel});
}
createMarkUnreadLevelSection(serverError) {
@@ -303,7 +282,7 @@ export default class ChannelNotificationsModal extends React.Component {
<label>
<input
type='radio'
- checked={this.state.markUnreadLevel === 'all'}
+ checked={this.state.unreadLevel === 'all'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
/>
<FormattedMessage
@@ -317,7 +296,7 @@ export default class ChannelNotificationsModal extends React.Component {
<label>
<input
type='radio'
- checked={this.state.markUnreadLevel === 'mention'}
+ checked={this.state.unreadLevel === 'mention'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
/>
<FormattedMessage id='channel_notifications.onlyMentions'/>
@@ -355,7 +334,7 @@ export default class ChannelNotificationsModal extends React.Component {
} else {
let describe;
- if (!this.state.markUnreadLevel || this.state.markUnreadLevel === 'all') {
+ if (!this.state.unreadLevel || this.state.unreadLevel === 'all') {
describe = (
<FormattedMessage
id='channel_notifications.allUnread'
@@ -430,5 +409,7 @@ export default class ChannelNotificationsModal extends React.Component {
ChannelNotificationsModal.propTypes = {
show: React.PropTypes.bool.isRequired,
onHide: React.PropTypes.func.isRequired,
- channel: React.PropTypes.object.isRequired
+ channel: React.PropTypes.object.isRequired,
+ channelMember: React.PropTypes.object.isRequired,
+ currentUser: React.PropTypes.object.isRequired
};
diff --git a/web/react/components/channel_view.jsx b/web/react/components/channel_view.jsx
index 9c4131292..76744d6d7 100644
--- a/web/react/components/channel_view.jsx
+++ b/web/react/components/channel_view.jsx
@@ -2,34 +2,11 @@
// See License.txt for license information.
import CenterPanel from '../components/center_panel.jsx';
-import Sidebar from '../components/sidebar.jsx';
-import SidebarRight from '../components/sidebar_right.jsx';
-import SidebarRightMenu from '../components/sidebar_right_menu.jsx';
export default class ChannelView extends React.Component {
render() {
return (
- <div className='container-fluid'>
- <div
- className='sidebar--right'
- id='sidebar-right'
- >
- <SidebarRight/>
- </div>
- <div
- className='sidebar--menu'
- id='sidebar-menu'
- >
- <SidebarRightMenu/>
- </div>
- <div
- className='sidebar--left'
- id='sidebar-left'
- >
- <Sidebar/>
- </div>
- <CenterPanel/>
- </div>
+ <CenterPanel/>
);
}
}
@@ -37,4 +14,5 @@ ChannelView.defaultProps = {
};
ChannelView.propTypes = {
+ params: React.PropTypes.object
};
diff --git a/web/react/components/claim/claim_account.jsx b/web/react/components/claim/claim_account.jsx
index 5b3b584ee..42fd8dafa 100644
--- a/web/react/components/claim/claim_account.jsx
+++ b/web/react/components/claim/claim_account.jsx
@@ -3,6 +3,7 @@
import EmailToSSO from './email_to_sso.jsx';
import SSOToEmail from './sso_to_email.jsx';
+import TeamStore from '../../stores/team_store.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -10,11 +11,46 @@ export default class ClaimAccount extends React.Component {
constructor(props) {
super(props);
+ this.onTeamChange = this.onTeamChange.bind(this);
+ this.updateStateFromStores = this.updateStateFromStores.bind(this);
+
this.state = {};
}
+ componentWillMount() {
+ this.setState({
+ email: this.props.location.query.email,
+ newType: this.props.location.query.new_type,
+ oldType: this.props.location.query.old_type,
+ teamName: this.props.params.team,
+ teamDisplayName: ''
+ });
+ this.updateStateFromStores();
+ }
+ componentDidMount() {
+ TeamStore.addChangeListener(this.onTeamChange);
+ }
+ componentWillUnmount() {
+ TeamStore.removeChangeListener(this.onTeamChange);
+ }
+ updateStateFromStores() {
+ const team = TeamStore.getByName(this.state.teamName);
+ let displayName = '';
+ if (team) {
+ displayName = team.displayName;
+ }
+ this.setState({
+ teamDisplayName: displayName
+ });
+ }
+ onTeamChange() {
+ this.updateStateFromStores();
+ }
render() {
+ if (this.state.teamDisplayName === '') {
+ return (<div/>);
+ }
let content;
- if (this.props.email === '') {
+ if (this.state.email === '') {
content = (
<p>
<FormattedMessage
@@ -23,36 +59,55 @@ export default class ClaimAccount extends React.Component {
/>
</p>
);
- } else if (this.props.currentType === '' && this.props.newType !== '') {
+ } else if (this.state.oldType === '' && this.state.newType !== '') {
content = (
<EmailToSSO
- email={this.props.email}
- type={this.props.newType}
- teamName={this.props.teamName}
- teamDisplayName={this.props.teamDisplayName}
+ email={this.state.email}
+ type={this.state.newType}
+ teamName={this.state.teamName}
+ teamDisplayName={this.state.teamDisplayName}
/>
);
} else {
content = (
<SSOToEmail
- email={this.props.email}
- currentType={this.props.currentType}
- teamName={this.props.teamName}
- teamDisplayName={this.props.teamDisplayName}
+ email={this.state.email}
+ currentType={this.state.oldType}
+ teamName={this.state.teamName}
+ teamDisplayName={this.state.teamDisplayName}
/>
);
}
- return content;
+ return (
+ <div>
+ <div className='signup-header'>
+ <a href='/'>
+ <span className='fa fa-chevron-left'/>
+ <FormattedMessage
+ id='web.header.back'
+ />
+ </a>
+ </div>
+ <div className='col-sm-12'>
+ <div className='signup-team__container'>
+ <img
+ className='signup-team-logo'
+ src='/static/images/logo.png'
+ />
+ <div id='claim'>
+ {content}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
}
}
ClaimAccount.defaultProps = {
};
ClaimAccount.propTypes = {
- currentType: React.PropTypes.string.isRequired,
- newType: React.PropTypes.string.isRequired,
- email: React.PropTypes.string.isRequired,
- teamName: React.PropTypes.string.isRequired,
- teamDisplayName: React.PropTypes.string.isRequired
+ params: React.PropTypes.object.isRequired,
+ location: React.PropTypes.object.isRequired
};
diff --git a/web/react/components/claim/sso_to_email.jsx b/web/react/components/claim/sso_to_email.jsx
index 74137082a..a16efb57b 100644
--- a/web/react/components/claim/sso_to_email.jsx
+++ b/web/react/components/claim/sso_to_email.jsx
@@ -159,7 +159,7 @@ SSOToEmail.propTypes = {
currentType: React.PropTypes.string.isRequired,
email: React.PropTypes.string.isRequired,
teamName: React.PropTypes.string.isRequired,
- teamDisplayName: React.PropTypes.string.isRequired
+ teamDisplayName: React.PropTypes.string
};
export default injectIntl(SSOToEmail);
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 62319b1a7..69cc74842 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -9,7 +9,7 @@ import PostDeletedModal from './post_deleted_modal.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import * as Utils from '../utils/utils.jsx';
@@ -165,7 +165,7 @@ class CreatePost extends React.Component {
const channel = ChannelStore.get(this.state.channelId);
- EventHelpers.emitUserPostedEvent(post);
+ GlobalActions.emitUserPostedEvent(post);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post, channel,
@@ -177,7 +177,7 @@ class CreatePost extends React.Component {
member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member);
- EventHelpers.emitPostRecievedEvent(data);
+ GlobalActions.emitPostRecievedEvent(data);
},
(err) => {
if (err.id === 'api.post.create_post.root_id.app_error') {
diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx
index d9113bc9f..70e7a67a8 100644
--- a/web/react/components/delete_channel_modal.jsx
+++ b/web/react/components/delete_channel_modal.jsx
@@ -9,6 +9,8 @@ import Constants from '../utils/constants.jsx';
import {FormattedMessage} from 'mm-intl';
+import {browserHistory} from 'react-router';
+
export default class DeleteChannelModal extends React.Component {
constructor(props) {
super(props);
@@ -21,11 +23,11 @@ export default class DeleteChannelModal extends React.Component {
return;
}
+ browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square');
Client.deleteChannel(
this.props.channel.id,
() => {
AsyncClient.getChannels(true);
- window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
},
(err) => {
AsyncClient.dispatchError(err, 'handleDelete');
diff --git a/web/react/components/do_verify_email.jsx b/web/react/components/do_verify_email.jsx
new file mode 100644
index 000000000..df98bf463
--- /dev/null
+++ b/web/react/components/do_verify_email.jsx
@@ -0,0 +1,82 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {FormattedMessage} from 'mm-intl';
+import * as Client from '../utils/client.jsx';
+import LoadingScreen from './loading_screen.jsx';
+
+import {browserHistory} from 'react-router';
+
+export default class DoVerifyEmail extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ verifyStatus: 'pending',
+ serverError: ''
+ };
+ }
+ componentWillMount() {
+ const uid = this.props.location.query.uid;
+ const hid = this.props.location.query.hid;
+ const teamName = this.props.location.query.teamname;
+ const email = this.props.location.query.email;
+
+ Client.verifyEmail(
+ () => {
+ browserHistory.push('/' + teamName + '/login?extra=verified&email=' + email);
+ },
+ (err) => {
+ this.setState({verifyStatus: 'failure', serverError: err.message});
+ },
+ uid,
+ hid
+ );
+ }
+ render() {
+ if (this.state.verifyStatus !== 'failure') {
+ return (<LoadingScreen/>);
+ }
+
+ return (
+ <div>
+ <div className='signup-header'>
+ <a href='/'>
+ <span className='fa fa-chevron-left'/>
+ <FormattedMessage
+ id='web.header.back'
+ />
+ </a>
+ </div>
+ <div className='col-sm-12'>
+ <div className='signup-team__container'>
+ <h3>
+ <FormattedMessage
+ id='email_verify.almost'
+ defaultMessage='{siteName}: You are almost done'
+ values={{
+ siteName: global.window.mm_config.SiteName
+ }}
+ />
+ </h3>
+ <div>
+ <p>
+ <FormattedMessage id='email_verify.verifyFailed'/>
+ </p>
+ <p className='alert alert-danger'>
+ <i className='fa fa-times'/>
+ {this.state.serverError}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+DoVerifyEmail.defaultProps = {
+};
+DoVerifyEmail.propTypes = {
+ location: React.PropTypes.object.isRequired
+};
diff --git a/web/react/components/docs.jsx b/web/react/components/docs.jsx
deleted file mode 100644
index 6d3a109c2..000000000
--- a/web/react/components/docs.jsx
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import * as TextFormatting from '../utils/text_formatting.jsx';
-import UserStore from '../stores/user_store.jsx';
-
-export default class Docs extends React.Component {
- constructor(props) {
- super(props);
- UserStore.setCurrentUser(global.window.mm_user || {});
-
- this.state = {text: ''};
- const errorState = {text: '## 404'};
-
- if (props.site) {
- $.get(`/static/help/${props.site}_${global.window.mm_locale}.md`).then((response) => {
- this.setState({text: response});
- }, () => {
- this.setState(errorState);
- });
- } else {
- this.setState(errorState);
- }
- }
-
- render() {
- return (
- <div
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.text)}}
- >
- </div>
- );
- }
-}
-
-Docs.defaultProps = {
- site: ''
-};
-Docs.propTypes = {
- site: React.PropTypes.string
-};
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index 380ca7bde..f02239fcf 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -3,7 +3,7 @@
import * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import Textbox from './textbox.jsx';
import BrowserStore from '../stores/browser_store.jsx';
import PostStore from '../stores/post_store.jsx';
@@ -45,7 +45,7 @@ class EditPostModal extends React.Component {
delete tempState.editText;
BrowserStore.setItem('edit_state_transfer', tempState);
$('#edit_post').modal('hide');
- EventHelpers.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
+ GlobalActions.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
return;
}
diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx
deleted file mode 100644
index 702a20eba..000000000
--- a/web/react/components/email_verify.jsx
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
-
-export default class EmailVerify extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleResend = this.handleResend.bind(this);
-
- this.state = {};
- }
- handleResend() {
- const newAddress = window.location.href.replace('&resend_success=true', '');
- window.location.href = newAddress + '&resend=true';
- }
- render() {
- var title = '';
- var body = '';
- var resend = '';
- var resendConfirm = '';
- if (this.props.isVerified === 'true') {
- title = (
- <FormattedMessage
- id='email_verify.verified'
- defaultMessage='{siteName} Email Verified'
- values={{
- siteName: global.window.mm_config.SiteName
- }}
- />
- );
- body = (
- <FormattedHTMLMessage
- id='email_verify.verifiedBody'
- defaultMessage='<p>Your email has been verified! Click <a href={url}>here</a> to log in.</p>'
- values={{
- url: this.props.teamURL + '?email=' + this.props.userEmail
- }}
- />
- );
- } else {
- title = (
- <FormattedMessage
- id='email_verify.almost'
- defaultMessage='{siteName}: You are almost done'
- values={{
- siteName: global.window.mm_config.SiteName
- }}
- />
- );
- body = (
- <p>
- <FormattedMessage
- id='email_verify.notVerifiedBody'
- defaultMessage='Please verify your email address. Check your inbox for an email.'
- />
- </p>
- );
- resend = (
- <button
- onClick={this.handleResend}
- className='btn btn-primary'
- >
- <FormattedMessage
- id='email_verify.resend'
- defaultMessage='Resend Email'
- />
- </button>
- );
- if (this.props.resendSuccess) {
- resendConfirm = (
- <div><br/><p className='alert alert-success'><i className='fa fa-check'></i>
- <FormattedMessage
- id='email_verify.sent'
- defaultMessage=' Verification email sent.'
- />
- </p></div>);
- }
- }
-
- return (
- <div className='col-sm-12'>
- <div className='signup-team__container'>
- <h3>{title}</h3>
- <div>
- {body}
- {resend}
- {resendConfirm}
- </div>
- </div>
- </div>
- );
- }
-}
-
-EmailVerify.defaultProps = {
- isVerified: 'false',
- teamURL: '',
- userEmail: '',
- resendSuccess: 'false'
-};
-EmailVerify.propTypes = {
- isVerified: React.PropTypes.string,
- teamURL: React.PropTypes.string,
- userEmail: React.PropTypes.string,
- resendSuccess: React.PropTypes.string
-};
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index c719c6c7d..8abcac8c3 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -43,7 +43,7 @@ class FileAttachment extends React.Component {
if (type === 'image') {
var self = this; // Need this reference since we use the given "this"
- $('<img/>').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) {
+ $('<img/>').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) {
return function loader() {
$(this).remove();
if (name in self.refs) {
@@ -114,7 +114,7 @@ class FileAttachment extends React.Component {
var re3 = new RegExp('\\)', 'g');
var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
- $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')');
+ $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)');
}
}
removeBackgroundImage(name) {
@@ -185,6 +185,7 @@ class FileAttachment extends React.Component {
data-toggle='tooltip'
title={this.props.intl.formatMessage(holders.download) + ' \"' + filenameString + '\"'}
className='post-image__name'
+ target='_blank'
>
{trimmedFilename}
</a>
@@ -193,6 +194,7 @@ class FileAttachment extends React.Component {
href={fileUrl}
download={filenameString}
className='post-image__download'
+ target='_blank'
>
<span
className='fa fa-download'
diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx
deleted file mode 100644
index 3ff9787ad..000000000
--- a/web/react/components/find_team.jsx
+++ /dev/null
@@ -1,135 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import * as utils from '../utils/utils.jsx';
-import * as client from '../utils/client.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
-
-var holders = defineMessages({
- submitError: {
- id: 'find_team.submitError',
- defaultMessage: 'Please enter a valid email address'
- },
- placeholder: {
- id: 'find_team.placeholder',
- defaultMessage: 'you@domain.com'
- }
-});
-
-class FindTeam extends React.Component {
- constructor(props) {
- super(props);
- this.state = {};
-
- this.handleSubmit = this.handleSubmit.bind(this);
- }
-
- handleSubmit(e) {
- e.preventDefault();
-
- var state = { };
-
- var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase();
- if (!email || !utils.isEmail(email)) {
- state.email_error = this.props.intl.formatMessage(holders.submitError);
- this.setState(state);
- return;
- }
-
- state.email_error = '';
-
- client.findTeamsSendEmail(email,
- function success() {
- state.sent = true;
- this.setState(state);
- }.bind(this),
- function fail(err) {
- state.email_error = err.message;
- this.setState(state);
- }.bind(this)
- );
- }
-
- render() {
- var emailError = null;
- var emailErrorClass = 'form-group';
-
- if (this.state.email_error) {
- emailError = <label className='control-label'>{this.state.email_error}</label>;
- emailErrorClass = 'form-group has-error';
- }
-
- if (this.state.sent) {
- return (
- <div>
- <h4>
- <FormattedMessage
- id='find_team.findTitle'
- defaultMessage='Find Your Team'
- />
- </h4>
- <p>
- <FormattedMessage
- id='find_team.findDescription'
- defaultMessage='An email was sent with links to any teams to which you are a member.'
- />
- </p>
- </div>
- );
- }
-
- return (
- <div>
- <h4>
- <FormattedMessage
- id='find_team.findTitle'
- defaultMessage='Find Your Team'
- />
- </h4>
- <form onSubmit={this.handleSubmit}>
- <p>
- <FormattedMessage
- id='find_team.getLinks'
- defaultMessage='Get an email with links to any teams to which you are a member.'
- />
- </p>
- <div className='form-group'>
- <label className='control-label'>
- <FormattedMessage
- id='find_team.email'
- defaultMessage='Email'
- />
- </label>
- <div className={emailErrorClass}>
- <input
- type='text'
- ref='email'
- className='form-control'
- placeholder={this.props.intl.formatMessage(holders.placeholder)}
- maxLength='128'
- spellCheck='false'
- />
- {emailError}
- </div>
- </div>
- <button
- className='btn btn-md btn-primary'
- type='submit'
- >
- <FormattedMessage
- id='find_team.send'
- defaultMessage='Send'
- />
- </button>
- </form>
- </div>
- );
- }
-}
-
-FindTeam.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(FindTeam); \ No newline at end of file
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 184ba1357..71cd5b8b6 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -5,7 +5,7 @@ import * as utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
import * as Client from '../utils/client.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import ModalStore from '../stores/modal_store.jsx';
import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
@@ -223,7 +223,7 @@ class InviteMemberModal extends React.Component {
showGetTeamInviteLinkModal() {
this.handleHide(false);
- EventHelpers.showGetTeamInviteLinkModal();
+ GlobalActions.showGetTeamInviteLinkModal();
}
render() {
diff --git a/web/react/components/logged_in.jsx b/web/react/components/logged_in.jsx
new file mode 100644
index 000000000..1ed3694e9
--- /dev/null
+++ b/web/react/components/logged_in.jsx
@@ -0,0 +1,224 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
+import UserStore from '../stores/user_store.jsx';
+import SocketStore from '../stores/socket_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
+import * as Utils from '../utils/utils.jsx';
+import Constants from '../utils/constants.jsx';
+import ErrorBar from '../components/error_bar.jsx';
+
+import {browserHistory} from 'react-router';
+
+import SidebarRight from '../components/sidebar_right.jsx';
+import SidebarRightMenu from '../components/sidebar_right_menu.jsx';
+
+// Modals
+import GetPostLinkModal from '../components/get_post_link_modal.jsx';
+import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx';
+import EditPostModal from '../components/edit_post_modal.jsx';
+import DeletePostModal from '../components/delete_post_modal.jsx';
+import MoreChannelsModal from '../components/more_channels.jsx';
+import TeamSettingsModal from '../components/team_settings_modal.jsx';
+import RemovedFromChannelModal from '../components/removed_from_channel_modal.jsx';
+import RegisterAppModal from '../components/register_app_modal.jsx';
+import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx';
+import InviteMemberModal from '../components/invite_member_modal.jsx';
+import SelectTeamModal from '../components/admin_console/select_team_modal.jsx';
+
+const CLIENT_STATUS_INTERVAL = 30000;
+const BACKSPACE_CHAR = 8;
+
+export default class LoggedIn extends React.Component {
+ constructor(params) {
+ super(params);
+
+ this.onUserChanged = this.onUserChanged.bind(this);
+ }
+ onUserChanged() {
+ // Grab the current user
+ const user = UserStore.getCurrentUser();
+
+ // Update segment indentify
+ if (global.window.mm_config.SegmentDeveloperKey != null && global.window.mm_config.SegmentDeveloperKey !== '') {
+ global.window.analytics.identify(user.id, {
+ name: user.nickname,
+ email: user.email,
+ createdAt: user.create_at,
+ username: user.username,
+ team_id: user.team_id,
+ id: user.id
+ });
+ }
+
+ // Update CSS classes to match user theme
+ if (user) {
+ if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
+ Utils.applyTheme(user.theme_props);
+ } else {
+ Utils.applyTheme(Constants.THEMES.default);
+ }
+ }
+ }
+ onSocketChange(msg) {
+ if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
+ UserStore.setStatus(msg.user_id, 'online');
+ }
+ }
+ componentWillMount() {
+ // Emit view action
+ GlobalActions.viewLoggedIn();
+
+ // Listen for user
+ UserStore.addChangeListener(this.onUserChanged);
+
+ // Add listner for socker store
+ SocketStore.addChangeListener(this.onSocketChange);
+
+ // Get all statuses regularally. (Soon to be switched to websocket)
+ this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL);
+
+ // Force logout of all tabs if one tab is logged out
+ $(window).bind('storage', (e) => {
+ // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
+ if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
+ // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
+ if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
+ return;
+ }
+
+ console.log('detected logout from a different tab'); //eslint-disable-line no-console
+ browserHistory.push('/' + this.props.params.team);
+ }
+
+ if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
+ // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
+ if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
+ return;
+ }
+
+ console.log('detected login from a different tab'); //eslint-disable-line no-console
+ location.reload();
+ }
+ });
+
+ // Because current CSS requires the root tag to have specific stuff
+ $('#root').attr('class', 'channel-view');
+
+ // ???
+ $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
+ if (ev.type === 'mouseenter') {
+ $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
+ $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
+ } else {
+ $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
+ $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
+ }
+ });
+
+ $('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) {
+ if (ev.type === 'mouseenter') {
+ $(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after');
+ $(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before');
+ } else {
+ $(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after');
+ $(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before');
+ }
+ });
+
+ $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) {
+ if (ev.type === 'mouseenter') {
+ $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment');
+ $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment');
+ } else {
+ $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment');
+ $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
+ }
+ });
+
+ // Device tracking setup
+ var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
+ if (iOS) {
+ $('body').addClass('ios');
+ }
+
+ // Set up tracking for whether the window is active
+ window.isActive = true;
+ $(window).on('focus', () => {
+ AsyncClient.updateLastViewedAt();
+ ChannelStore.resetCounts(ChannelStore.getCurrentId());
+ ChannelStore.emitChange();
+ window.isActive = true;
+ });
+ $(window).on('blur', () => {
+ window.isActive = false;
+ });
+
+ // if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx
+ const selectedFont = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT);
+ Utils.applyFont(selectedFont);
+
+ // Pervent backspace from navigating back a page
+ $(window).on('keydown.preventBackspace', (e) => {
+ if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) {
+ e.preventDefault();
+ }
+ });
+ }
+ componentWillUnmount() {
+ $('#root').attr('class', '');
+ clearInterval(this.intervalId);
+
+ $(window).off('focus');
+ $(window).off('blur');
+
+ SocketStore.removeChangeListener(this.onSocketChange);
+ UserStore.removeChangeListener(this.onUserChanged);
+
+ $('body').off('click.userpopover');
+ $('body').off('mouseenter mouseleave', '.post');
+ $('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
+
+ $('.modal').off('show.bs.modal');
+
+ $(window).off('keydown.preventBackspace');
+ }
+ render() {
+ return (
+ <div className='channel-view'>
+ <ErrorBar/>
+ <div className='container-fluid'>
+ <SidebarRight/>
+ <SidebarRightMenu/>
+ {this.props.sidebar}
+ {this.props.center}
+
+ <GetPostLinkModal/>
+ <GetTeamInviteLinkModal/>
+ <InviteMemberModal/>
+ <ImportThemeModal/>
+ <TeamSettingsModal/>
+ <MoreChannelsModal/>
+ <EditPostModal/>
+ <DeletePostModal/>
+ <RemovedFromChannelModal/>
+ <RegisterAppModal/>
+ <SelectTeamModal/>
+ </div>
+ </div>
+ );
+ }
+}
+
+LoggedIn.defaultProps = {
+};
+
+LoggedIn.propTypes = {
+ children: React.PropTypes.object,
+ sidebar: React.PropTypes.object,
+ center: React.PropTypes.object,
+ params: React.PropTypes.object
+};
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index 581b8e0b5..d3ee35082 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -6,82 +6,120 @@ import LoginUsername from './login_username.jsx';
import LoginLdap from './login_ldap.jsx';
import * as Utils from '../utils/utils.jsx';
+import * as Client from '../utils/client.jsx';
import Constants from '../utils/constants.jsx';
+import TeamStore from '../stores/team_store.jsx';
import {FormattedMessage} from 'mm-intl';
+import {browserHistory} from 'react-router';
export default class Login extends React.Component {
constructor(props) {
super(props);
- this.state = {};
+ this.getStateFromStores = this.getStateFromStores.bind(this);
+ this.onTeamChange = this.onTeamChange.bind(this);
+
+ this.state = this.getStateFromStores();
+ }
+ componentDidMount() {
+ TeamStore.addChangeListener(this.onTeamChange);
+ Client.getMeLoggedIn((data) => {
+ if (data && data.logged_in !== 'false') {
+ browserHistory.push('/' + this.props.params.team + '/channels/town-square');
+ }
+ });
+ }
+ componentWillUnmount() {
+ TeamStore.removeChangeListener(this.onTeamChange);
+ }
+ getStateFromStores() {
+ return {
+ currentTeam: TeamStore.getByName(this.props.params.team)
+ };
+ }
+ onTeamChange() {
+ this.setState(this.getStateFromStores());
}
render() {
- const teamDisplayName = this.props.teamDisplayName;
- const teamName = this.props.teamName;
+ const currentTeam = this.state.currentTeam;
+ if (currentTeam == null) {
+ return <div/>;
+ }
+
+ const teamDisplayName = currentTeam.display_name;
+ const teamName = currentTeam.name;
+ const ldapEnabled = global.window.mm_config.EnableLdap === 'true';
+ const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true';
let loginMessage = [];
if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
loginMessage.push(
- <a
- className='btn btn-custom-login gitlab'
- key='gitlab'
- href={'/' + teamName + '/login/gitlab'}
- >
- <span className='icon'/>
- <span>
- <FormattedMessage
- id='login.gitlab'
- defaultMessage='with GitLab'
- />
- </span>
- </a>
+ <a
+ className='btn btn-custom-login gitlab'
+ key='gitlab'
+ href={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)}
+ >
+ <span className='icon'/>
+ <span>
+ <FormattedMessage
+ id='login.gitlab'
+ defaultMessage='with GitLab'
+ />
+ </span>
+ </a>
);
}
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
loginMessage.push(
- <a
- className='btn btn-custom-login google'
- key='google'
- href={'/' + teamName + '/login/google'}
- >
- <span className='icon'/>
- <span>
- <FormattedMessage
- id='login.google'
- defaultMessage='with Google Apps'
- />
- </span>
- </a>
- );
+ <a
+ className='btn btn-custom-login google'
+ key='google'
+ href={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)}
+ >
+ <span className='icon'/>
+ <span>
+ <FormattedMessage
+ id='login.google'
+ defaultMessage='with Google Apps'
+ />
+ </span>
+ </a>
+ );
}
const extraParam = Utils.getUrlParameter('extra');
let extraBox = '';
if (extraParam) {
- let msg;
if (extraParam === Constants.SIGNIN_CHANGE) {
- msg = (
- <FormattedMessage
- id='login.changed'
- defaultMessage=' Sign-in method changed successfully'
- />
+ extraBox = (
+ <div className='alert alert-success'>
+ <i className='fa fa-check'/>
+ <FormattedMessage
+ id='login.changed'
+ defaultMessage=' Sign-in method changed successfully'
+ />
+ </div>
);
} else if (extraParam === Constants.SIGNIN_VERIFIED) {
- msg = (
- <FormattedMessage
- id='login.verified'
- defaultMessage=' Email Verified'
- />
- );
- }
-
- if (msg != null) {
extraBox = (
<div className='alert alert-success'>
<i className='fa fa-check'/>
- {msg}
+ <FormattedMessage
+ id='login.verified'
+ defaultMessage=' Email Verified'
+ />
+ </div>
+ );
+ } else if (extraParam === Constants.SESSION_EXPIRED) {
+ extraBox = (
+ <div className='alert alert-warning'>
+ <i className='fa fa-exclamation-triangle'/>
+ <FormattedMessage
+ id='login.session_expired'
+ defaultMessage=' Your session has expired. Please login again.'
+ />
</div>
);
}
@@ -91,7 +129,7 @@ export default class Login extends React.Component {
if (global.window.mm_config.EnableSignInWithEmail === 'true') {
emailSignup = (
<LoginEmail
- teamName={this.props.teamName}
+ teamName={teamName}
/>
);
}
@@ -125,7 +163,7 @@ export default class Login extends React.Component {
}
let userSignUp = null;
- if (this.props.inviteId) {
+ if (currentTeam.allow_open_invite) {
userSignUp = (
<div>
<span>
@@ -134,7 +172,7 @@ export default class Login extends React.Component {
defaultMessage="Don't have an account? "
/>
<a
- href={'/signup_user_complete/?id=' + this.props.inviteId}
+ href={'/signup_user_complete/?id=' + currentTeam.invite_id}
className='signup-team-login'
>
<FormattedMessage
@@ -168,22 +206,23 @@ export default class Login extends React.Component {
if (global.window.mm_config.EnableLdap === 'true') {
ldapLogin = (
<LoginLdap
- teamName={this.props.teamName}
+ teamName={teamName}
/>
);
}
- let findTeams = null;
- if (!Utils.isMobileApp()) {
- findTeams = (
- <div className='form-group margin--extra form-group--small'>
- <span>
- <a href='/find_team'>
- <FormattedMessage
- id='login.find'
- defaultMessage='Find your other teams'
- />
- </a></span>
+ if (ldapEnabled && (loginMessage.length > 0 || emailSignup || usernameSigninEnabled)) {
+ ldapLogin = (
+ <div>
+ <div className='or__container'>
+ <FormattedMessage
+ id='login.or'
+ defaultMessage='or'
+ />
+ </div>
+ <LoginLdap
+ teamName={teamName}
+ />
</div>
);
}
@@ -192,49 +231,72 @@ export default class Login extends React.Component {
if (global.window.mm_config.EnableSignInWithUsername === 'true') {
usernameLogin = (
<LoginUsername
- teamName={this.props.teamName}
+ teamName={teamName}
/>
);
}
- return (
- <div className='signup-team__container'>
- <h5 className='margin--less'>
- <FormattedMessage
- id='login.signTo'
- defaultMessage='Sign in to:'
- />
- </h5>
- <h2 className='signup-team__name'>{teamDisplayName}</h2>
- <h2 className='signup-team__subdomain'>
- <FormattedMessage
- id='login.on'
- defaultMessage='on {siteName}'
- values={{
- siteName: global.window.mm_config.SiteName
- }}
+ if (usernameSigninEnabled && (loginMessage.length > 0 || emailSignup || ldapEnabled)) {
+ usernameLogin = (
+ <div>
+ <div className='or__container'>
+ <FormattedMessage
+ id='login.or'
+ defaultMessage='or'
+ />
+ </div>
+ <LoginUsername
+ teamName={teamName}
/>
- </h2>
- {extraBox}
- {loginMessage}
- {emailSignup}
- {usernameLogin}
- {ldapLogin}
- {userSignUp}
- {findTeams}
- {forgotPassword}
- {teamSignUp}
+ </div>
+ );
+ }
+
+ return (
+ <div>
+ <div className='signup-header'>
+ <a href='/'>
+ <span className='fa fa-chevron-left'/>
+ <FormattedMessage
+ id='web.header.back'
+ />
+ </a>
+ </div>
+ <div className='col-sm-12'>
+ <div className='signup-team__container'>
+ <h5 className='margin--less'>
+ <FormattedMessage
+ id='login.signTo'
+ defaultMessage='Sign in to:'
+ />
+ </h5>
+ <h2 className='signup-team__name'>{teamDisplayName}</h2>
+ <h2 className='signup-team__subdomain'>
+ <FormattedMessage
+ id='login.on'
+ defaultMessage='on {siteName}'
+ values={{
+ siteName: global.window.mm_config.SiteName
+ }}
+ />
+ </h2>
+ {extraBox}
+ {loginMessage}
+ {emailSignup}
+ {usernameLogin}
+ {ldapLogin}
+ {userSignUp}
+ {forgotPassword}
+ {teamSignUp}
+ </div>
+ </div>
</div>
);
}
}
Login.defaultProps = {
- teamName: '',
- teamDisplayName: ''
};
Login.propTypes = {
- teamName: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string,
- inviteId: React.PropTypes.string
+ params: React.PropTypes.object.isRequired
};
diff --git a/web/react/components/login_email.jsx b/web/react/components/login_email.jsx
index cf1e1bc40..3e0d8919d 100644
--- a/web/react/components/login_email.jsx
+++ b/web/react/components/login_email.jsx
@@ -4,6 +4,7 @@
import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
import UserStore from '../stores/user_store.jsx';
+import {browserHistory} from 'react-router';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
@@ -72,13 +73,7 @@ class LoginEmail extends React.Component {
Client.loginByEmail(name, email, password,
() => {
UserStore.setLastEmail(email);
-
- const redirect = Utils.getUrlParameter('redirect');
- if (redirect) {
- window.location.href = decodeURIComponent(redirect);
- } else {
- window.location.href = '/' + name + '/channels/town-square';
- }
+ browserHistory.push('/' + name + '/channels/town-square');
},
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
@@ -167,4 +162,4 @@ LoginEmail.propTypes = {
teamName: React.PropTypes.string.isRequired
};
-export default injectIntl(LoginEmail); \ No newline at end of file
+export default injectIntl(LoginEmail);
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index 93fe6c05a..974f026d0 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -56,9 +56,13 @@ export default class Navbar extends React.Component {
return {
channel: ChannelStore.getCurrent(),
member: ChannelStore.getCurrentMember(),
- users: ChannelStore.getCurrentExtraInfo().members
+ users: ChannelStore.getCurrentExtraInfo().members,
+ currentUser: UserStore.getCurrentUser()
};
}
+ stateValid() {
+ return this.state.channel && this.state.member && this.state.users && this.state.currentUser;
+ }
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
ChannelStore.addExtraInfoChangeListener(this.onChange);
@@ -201,7 +205,7 @@ export default class Navbar extends React.Component {
<ToggleModalButton
role='menuitem'
dialogType={ChannelInviteModal}
- dialogProps={{channel}}
+ dialogProps={{channel, currentUser: this.state.currentUser}}
>
<FormattedMessage
id='navbar.addMembers'
@@ -286,7 +290,11 @@ export default class Navbar extends React.Component {
<ToggleModalButton
role='menuitem'
dialogType={ChannelNotificationsModal}
- dialogProps={{channel}}
+ dialogProps={{
+ channel,
+ channelMember: this.state.member,
+ currentUser: this.state.currentUser
+ }}
>
<FormattedMessage
id='navbar.preferences'
@@ -412,7 +420,11 @@ export default class Navbar extends React.Component {
return buttons;
}
render() {
- var currentId = UserStore.getCurrentId();
+ if (!this.stateValid()) {
+ return null;
+ }
+
+ var currentId = this.state.currentUser.id;
var channel = this.state.channel;
var channelTitle = this.props.teamDisplayName;
var popoverContent;
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 0ddd6ff4f..12227fd13 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -2,10 +2,7 @@
// See License.txt for license information.
import * as Utils from '../utils/utils.jsx';
-import * as client from '../utils/client.jsx';
-import UserStore from '../stores/user_store.jsx';
-import TeamStore from '../stores/team_store.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import AboutBuildModal from './about_build_modal.jsx';
import TeamMembersModal from './team_members_modal.jsx';
@@ -15,38 +12,20 @@ import UserSettingsModal from './user_settings/user_settings_modal.jsx';
import Constants from '../utils/constants.jsx';
import {FormattedMessage} from 'mm-intl';
-
-function getStateFromStores() {
- const teams = [];
- const teamsObject = UserStore.getTeams();
- for (const teamId in teamsObject) {
- if (teamsObject.hasOwnProperty(teamId)) {
- teams.push(teamsObject[teamId]);
- }
- }
-
- teams.sort(Utils.sortByDisplayName);
- return {teams};
-}
+import {Link} from 'react-router';
export default class NavbarDropdown extends React.Component {
constructor(props) {
super(props);
this.blockToggle = false;
- this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.handleAboutModal = this.handleAboutModal.bind(this);
- this.onListenerChange = this.onListenerChange.bind(this);
this.aboutModalDismissed = this.aboutModalDismissed.bind(this);
- const state = getStateFromStores();
- state.showUserSettingsModal = false;
- state.showAboutModal = false;
- this.state = state;
- }
- handleLogoutClick(e) {
- e.preventDefault();
- client.logout();
+ this.state = {
+ showUserSettingsModal: false,
+ showAboutModal: false
+ };
}
handleAboutModal() {
this.setState({showAboutModal: true});
@@ -55,9 +34,6 @@ export default class NavbarDropdown extends React.Component {
this.setState({showAboutModal: false});
}
componentDidMount() {
- UserStore.addTeamsChangeListener(this.onListenerChange);
- TeamStore.addChangeListener(this.onListenerChange);
-
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
$('.sidebar--left .dropdown-menu').scrollTop(0);
this.blockToggle = true;
@@ -67,24 +43,15 @@ export default class NavbarDropdown extends React.Component {
});
}
componentWillUnmount() {
- UserStore.removeTeamsChangeListener(this.onListenerChange);
- TeamStore.removeChangeListener(this.onListenerChange);
-
$(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
}
- onListenerChange() {
- var newState = getStateFromStores();
- if (!Utils.areObjectsEqual(newState, this.state)) {
- this.setState(newState);
- }
- }
render() {
var teamLink = '';
var inviteLink = '';
var manageLink = '';
var sysAdminLink = '';
var adminDivider = '';
- var currentUser = UserStore.getCurrentUser();
+ var currentUser = this.props.currentUser;
var isAdmin = false;
var isSystemAdmin = false;
var teamSettings = null;
@@ -97,7 +64,7 @@ export default class NavbarDropdown extends React.Component {
<li>
<a
href='#'
- onClick={EventHelpers.showInviteMemberModal}
+ onClick={GlobalActions.showInviteMemberModal}
>
<FormattedMessage
id='navbar_dropdown.inviteMember'
@@ -112,7 +79,7 @@ export default class NavbarDropdown extends React.Component {
<li>
<a
href='#'
- onClick={EventHelpers.showGetTeamInviteLinkModal}
+ onClick={GlobalActions.showGetTeamInviteLinkModal}
>
<FormattedMessage
id='navbar_dropdown.teamLink'
@@ -158,7 +125,7 @@ export default class NavbarDropdown extends React.Component {
sysAdminLink = (
<li>
<a
- href={'/admin_console?' + Utils.getSessionIndex()}
+ href={'/admin_console'}
>
<FormattedMessage
id='navbar_dropdown.console'
@@ -171,31 +138,6 @@ export default class NavbarDropdown extends React.Component {
var teams = [];
- if (this.state.teams.length > 1) {
- teams.push(
- <li
- className='divider'
- key='div'
- >
- </li>
- );
-
- this.state.teams.forEach((team) => {
- if (team.name !== this.props.teamName) {
- teams.push(
- <li key={team.name}><a href={Utils.getWindowLocationOrigin() + '/' + team.name}>
- <FormattedMessage
- id='navbar_dropdown.switchTeam'
- defaultMessage='Switch to {team}'
- values={{
- team: team.display_name
- }}
- />
- </a></li>);
- }
- });
- }
-
if (global.window.mm_config.EnableTeamCreation === 'true') {
teams.push(
<li key='newTeam_li'>
@@ -283,15 +225,12 @@ export default class NavbarDropdown extends React.Component {
{inviteLink}
{teamLink}
<li>
- <a
- href='#'
- onClick={this.handleLogoutClick}
- >
+ <Link to={'/' + this.props.teamName + '/logout'}>
<FormattedMessage
id='navbar_dropdown.logout'
defaultMessage='Logout'
/>
- </a>
+ </Link>
</li>
{adminDivider}
{teamSettings}
@@ -333,5 +272,6 @@ NavbarDropdown.defaultProps = {
NavbarDropdown.propTypes = {
teamType: React.PropTypes.string,
teamDisplayName: React.PropTypes.string,
- teamName: React.PropTypes.string
+ teamName: React.PropTypes.string,
+ currentUser: React.PropTypes.object
};
diff --git a/web/react/components/needs_team.jsx b/web/react/components/needs_team.jsx
new file mode 100644
index 000000000..33b9cd37e
--- /dev/null
+++ b/web/react/components/needs_team.jsx
@@ -0,0 +1,20 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as GlobalActions from '../action_creators/global_actions.jsx';
+
+export default class NeedsTeam extends React.Component {
+ componentWillMount() {
+ GlobalActions.loadTeamRequiredPage();
+ }
+ render() {
+ return this.props.children;
+ }
+}
+
+NeedsTeam.defaultProps = {
+};
+
+NeedsTeam.propTypes = {
+ children: React.PropTypes.object
+};
diff --git a/web/react/components/not_logged_in.jsx b/web/react/components/not_logged_in.jsx
new file mode 100644
index 000000000..7af293e77
--- /dev/null
+++ b/web/react/components/not_logged_in.jsx
@@ -0,0 +1,70 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {FormattedMessage} from 'mm-intl';
+
+export default class NotLoggedIn extends React.Component {
+ componentDidMount() {
+ $('body').attr('class', 'white');
+ $('#root').attr('class', 'container-fluid');
+ }
+ componentWillUnmount() {
+ $('body').attr('class', '');
+ $('#root').attr('class', '');
+ }
+ render() {
+ return (
+ <div className='inner__wrap'>
+ <div className='row content'>
+ {this.props.children}
+ <div className='footer-push'></div>
+ </div>
+ <div className='row footer'>
+ <div className='footer-pane col-xs-12'>
+ <div className='col-xs-12'>
+ <span className='pull-right footer-site-name'>{global.window.mm_config.SiteName}</span>
+ </div>
+ <div className='col-xs-12'>
+ <span className='pull-right footer-link copyright'>{'© 2015 Mattermost, Inc.'}</span>
+ <a
+ id='help_link'
+ className='pull-right footer-link'
+ href={global.window.mm_config.HelpLink}
+ >
+ <FormattedMessage id='web.footer.help'/>
+ </a>
+ <a
+ id='terms_link'
+ className='pull-right footer-link'
+ href={global.window.mm_config.TermsOfServiceLink}
+ >
+ <FormattedMessage id='web.footer.terms'/>
+ </a>
+ <a
+ id='privacy_link'
+ className='pull-right footer-link'
+ href={global.window.mm_config.PrivacyPolicyLink}
+ >
+ <FormattedMessage id='web.footer.privacy'/>
+ </a>
+ <a
+ id='about_link'
+ className='pull-right footer-link'
+ href={global.window.mm_config.AboutLink}
+ >
+ <FormattedMessage id='web.footer.about'/>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+NotLoggedIn.defaultProps = {
+};
+
+NotLoggedIn.propTypes = {
+ children: React.PropTypes.object
+};
diff --git a/web/react/components/password_reset.jsx b/web/react/components/password_reset.jsx
deleted file mode 100644
index 4c9bb6310..000000000
--- a/web/react/components/password_reset.jsx
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import PasswordResetSendLink from './password_reset_send_link.jsx';
-import PasswordResetForm from './password_reset_form.jsx';
-
-export default class PasswordReset extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {};
- }
- render() {
- if (this.props.isReset === 'false') {
- return (
- <PasswordResetSendLink
- teamDisplayName={this.props.teamDisplayName}
- teamName={this.props.teamName}
- />
- );
- }
-
- return (
- <PasswordResetForm
- teamDisplayName={this.props.teamDisplayName}
- teamName={this.props.teamName}
- hash={this.props.hash}
- data={this.props.data}
- />
- );
- }
-}
-
-PasswordReset.defaultProps = {
- isReset: '',
- teamName: '',
- teamDisplayName: '',
- hash: '',
- data: ''
-};
-PasswordReset.propTypes = {
- isReset: React.PropTypes.string,
- teamName: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string,
- hash: React.PropTypes.string,
- data: React.PropTypes.string
-};
diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx
index 380dbe973..cfd39e440 100644
--- a/web/react/components/password_reset_form.jsx
+++ b/web/react/components/password_reset_form.jsx
@@ -2,24 +2,11 @@
// See License.txt for license information.
import * as Client from '../utils/client.jsx';
+import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
-import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
-
-const holders = defineMessages({
- error: {
- id: 'password_form.error',
- defaultMessage: 'Please enter at least {chars} characters.'
- },
- update: {
- id: 'password_form.update',
- defaultMessage: 'Your password has been updated successfully.'
- },
- pwd: {
- id: 'password_form.pwd',
- defaultMessage: 'Password'
- }
-});
+import {FormattedMessage} from 'mm-intl';
+import {browserHistory} from 'react-router';
class PasswordResetForm extends React.Component {
constructor(props) {
@@ -32,51 +19,50 @@ class PasswordResetForm extends React.Component {
handlePasswordReset(e) {
e.preventDefault();
- const {formatMessage} = this.props.intl;
- var state = {};
-
- var password = ReactDOM.findDOMNode(this.refs.password).value.trim();
+ const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) {
- state.error = formatMessage(holders.error, {chars: Constants.MIN_PASSWORD_LENGTH});
- this.setState(state);
+ this.setState({
+ error: (
+ <FormattedMessage
+ id='password_form.error'
+ defaultMessage='Please enter at least {chars} characters.'
+ chars={Constants.MIN_PASSWORD_LENGTH}
+ />
+ )
+ });
return;
}
- state.error = null;
- this.setState(state);
+ this.setState({
+ error: null
+ });
- var data = {};
+ const data = {};
data.new_password = password;
- data.hash = this.props.hash;
- data.data = this.props.data;
- data.name = this.props.teamName;
+ data.hash = this.props.location.query.h;
+ data.data = this.props.location.query.d;
+ data.name = this.props.params.team;
Client.resetPassword(data,
- function resetSuccess() {
- this.setState({error: null, updateText: formatMessage(holders.update)});
- }.bind(this),
- function resetFailure(err) {
- this.setState({error: err.message, updateText: null});
- }.bind(this)
+ () => {
+ this.setState({error: null});
+ browserHistory.push('/' + this.props.params.team + '/login');
+ },
+ (err) => {
+ this.setState({error: err.message});
+ }
);
}
render() {
- var updateText = null;
- if (this.state.updateText) {
- updateText = (<div className='form-group'><br/><label className='control-label reset-form'>{this.state.updateText}
- <FormattedHTMLMessage
- id='password_form.click'
- defaultMessage='Click <a href={url}>here</a> to log in.'
- values={{
- url: '/' + this.props.teamName + '/login'
- }}
- />
- </label></div>);
- }
-
var error = null;
if (this.state.error) {
- error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
+ error = (
+ <div className='form-group has-error'>
+ <label className='control-label'>
+ {this.state.error}
+ </label>
+ </div>
+ );
}
var formClass = 'form-group';
@@ -84,7 +70,6 @@ class PasswordResetForm extends React.Component {
formClass += ' has-error';
}
- const {formatMessage} = this.props.intl;
return (
<div className='col-sm-12'>
<div className='signup-team__container'>
@@ -98,9 +83,8 @@ class PasswordResetForm extends React.Component {
<p>
<FormattedMessage
id='password_form.enter'
- defaultMessage='Enter a new password for your {teamDisplayName} {siteName} account.'
+ defaultMessage='Enter a new password for your {siteName} account.'
values={{
- teamDisplayName: this.props.teamDisplayName,
siteName: global.window.mm_config.SiteName
}}
/>
@@ -111,7 +95,10 @@ class PasswordResetForm extends React.Component {
className='form-control'
name='password'
ref='password'
- placeholder={formatMessage(holders.pwd)}
+ placeholder={Utils.localizeMessage(
+ 'password_form.pwd',
+ 'Password'
+ )}
spellCheck='false'
/>
</div>
@@ -125,7 +112,6 @@ class PasswordResetForm extends React.Component {
defaultMessage='Change my password'
/>
</button>
- {updateText}
</form>
</div>
</div>
@@ -134,17 +120,10 @@ class PasswordResetForm extends React.Component {
}
PasswordResetForm.defaultProps = {
- teamName: '',
- teamDisplayName: '',
- hash: '',
- data: ''
};
PasswordResetForm.propTypes = {
- intl: intlShape.isRequired,
- teamName: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string,
- hash: React.PropTypes.string,
- data: React.PropTypes.string
+ params: React.PropTypes.object.isRequired,
+ location: React.PropTypes.object.isRequired
};
-export default injectIntl(PasswordResetForm); \ No newline at end of file
+export default PasswordResetForm;
diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx
index 8cc8a050d..ce6253e16 100644
--- a/web/react/components/password_reset_send_link.jsx
+++ b/web/react/components/password_reset_send_link.jsx
@@ -4,26 +4,7 @@
import * as Utils from '../utils/utils.jsx';
import * as client from '../utils/client.jsx';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
-
-const holders = defineMessages({
- error: {
- id: 'password_send.error',
- defaultMessage: 'Please enter a valid email address.'
- },
- link: {
- id: 'password_send.link',
- defaultMessage: '<p>A password reset link has been sent to <b>{email}</b> for your <b>{teamDisplayName}</b> team on {hostname}.</p>'
- },
- checkInbox: {
- id: 'password_send.checkInbox',
- defaultMessage: 'Please check your inbox.'
- },
- email: {
- id: 'password_send.email',
- defaultMessage: 'Email'
- }
-});
+import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
class PasswordResetSendLink extends React.Component {
constructor(props) {
@@ -31,48 +12,64 @@ class PasswordResetSendLink extends React.Component {
this.handleSendLink = this.handleSendLink.bind(this);
- this.state = {};
+ this.state = {
+ error: '',
+ updateText: ''
+ };
}
handleSendLink(e) {
e.preventDefault();
- var state = {};
- const {formatMessage} = this.props.intl;
var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase();
if (!email || !Utils.isEmail(email)) {
- state.error = formatMessage(holders.error);
- this.setState(state);
+ this.setState({
+ error: (
+ <FormattedMessage
+ id={'password_send.error'}
+ defaultMessage={'Please enter a valid email address.'}
+ />
+ )
+ });
return;
}
- state.error = null;
- this.setState(state);
+ // End of error checking clear error
+ this.setState({
+ error: ''
+ });
var data = {};
data.email = email;
- data.name = this.props.teamName;
-
+ data.name = this.props.params.team;
client.sendPasswordReset(data,
- function passwordResetSent() {
- this.setState({error: null, updateText: formatMessage(holders.link, {email: email, teamDisplayName: this.props.teamDisplayName, hostname: window.location.hostname}), moreUpdateText: formatMessage(holders.checkInbox)});
- $(ReactDOM.findDOMNode(this.refs.reset_form)).hide();
- }.bind(this),
- function passwordResetFailedToSend(err) {
- this.setState({error: err.message, update_text: null, moreUpdateText: null});
- }.bind(this)
- );
+ () => {
+ this.setState({
+ error: null,
+ updateText: (
+ <div className='reset-form alert alert-success'>
+ <FormattedHTMLMessage
+ id='password_send.link'
+ defaultMessage='<p>A password reset link has been sent to <b>{email}</b></p>'
+ email={email}
+ />
+ <FormattedMessage
+ id={'password_send.checkInbox'}
+ defaultMessage={'Please check your inbox.'}
+ />
+ </div>
+ )
+ });
+ $(ReactDOM.findDOMNode(this.refs.reset_form)).hide();
+ },
+ (err) => {
+ this.setState({
+ error: err.message,
+ update_text: null
+ });
+ }
+ );
}
render() {
- var updateText = null;
- if (this.state.updateText) {
- updateText = (
- <div className='reset-form alert alert-success'
- dangerouslySetInnerHTML={{__html: this.state.updateText + this.state.moreUpdateText}}
- >
- </div>
- );
- }
-
var error = null;
if (this.state.error) {
error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
@@ -83,51 +80,60 @@ class PasswordResetSendLink extends React.Component {
formClass += ' has-error';
}
- const {formatMessage} = this.props.intl;
return (
- <div className='col-sm-12'>
- <div className='signup-team__container'>
- <h3>
+ <div>
+ <div className='signup-header'>
+ <a href='/'>
+ <span className='fa fa-chevron-left'/>
<FormattedMessage
- id='password_send.title'
- defaultMessage='Password Reset'
+ id='web.header.back'
/>
- </h3>
- {updateText}
- <form
- onSubmit={this.handleSendLink}
- ref='reset_form'
- >
- <p>
+ </a>
+ </div>
+ <div className='col-sm-12'>
+ <div className='signup-team__container'>
+ <h3>
<FormattedMessage
- id='password_send.description'
- defaultMessage='To reset your password, enter the email address you used to sign up for {teamName}.'
- values={{
- teamName: this.props.teamDisplayName
- }}
+ id='password_send.title'
+ defaultMessage='Password Reset'
/>
- </p>
- <div className={formClass}>
- <input
- type='email'
- className='form-control'
- name='email'
- ref='email'
- placeholder={formatMessage(holders.email)}
- spellCheck='false'
- />
- </div>
- {error}
- <button
- type='submit'
- className='btn btn-primary'
+ </h3>
+ {this.state.updateText}
+ <form
+ onSubmit={this.handleSendLink}
+ ref='reset_form'
>
- <FormattedMessage
- id='password_send.reset'
- defaultMessage='Reset my password'
- />
- </button>
- </form>
+ <p>
+ <FormattedMessage
+ id='password_send.description'
+ defaultMessage='To reset your password, enter the email address you used to sign up'
+ />
+ </p>
+ <div className={formClass}>
+ <input
+ type='email'
+ className='form-control'
+ name='email'
+ ref='email'
+ placeholder={Utils.localizeMessage(
+ 'password_send.email',
+ 'Email'
+ )}
+ spellCheck='false'
+ />
+ </div>
+ {error}
+ <button
+ type='submit'
+ className='btn btn-primary'
+ >
+ <FormattedMessage
+ id='password_send.reset'
+ defaultMessage='Reset my password'
+ />
+ </button>
+ </form>
+ </div>
</div>
</div>
);
@@ -135,13 +141,9 @@ class PasswordResetSendLink extends React.Component {
}
PasswordResetSendLink.defaultProps = {
- teamName: '',
- teamDisplayName: ''
};
PasswordResetSendLink.propTypes = {
- intl: intlShape.isRequired,
- teamName: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string
+ params: React.PropTypes.object.isRequired
};
-export default injectIntl(PasswordResetSendLink); \ No newline at end of file
+export default PasswordResetSendLink;
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index afff78bae..1943fb409 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -118,7 +118,7 @@ export default class PopoverListMembers extends React.Component {
className='profile-img rounded pull-left'
width='26px'
height='26px'
- src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`}
+ src={`/api/v1/users/${m.id}/image?time=${m.update_at}`}
/>
<div className='pull-left'>
<div
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 889d4311e..3a855edf2 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -4,7 +4,6 @@
import PostHeader from './post_header.jsx';
import PostBody from './post_body.jsx';
-import UserStore from '../stores/user_store.jsx';
import PostStore from '../stores/post_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
@@ -98,7 +97,7 @@ export default class Post extends React.Component {
return true;
}
- if (nextProps.hasProfiles !== this.props.hasProfiles) {
+ if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) {
return true;
}
@@ -128,7 +127,6 @@ export default class Post extends React.Component {
const post = this.props.post;
const parentPost = this.props.parentPost;
const posts = this.props.posts;
- const user = this.props.user || {};
if (!post.props) {
post.props = {};
@@ -156,13 +154,15 @@ export default class Post extends React.Component {
}
let currentUserCss = '';
- if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook && !Utils.isSystemMessage(post)) {
+ if (this.props.currentUser.id === post.user_id && !post.props.from_webhook && !Utils.isSystemMessage(post)) {
currentUserCss = 'current--user';
}
- let timestamp = user.update_at;
- if (timestamp == null) {
- timestamp = UserStore.getCurrentUser().update_at;
+ let timestamp = 0;
+ if (!this.props.user || this.props.user.update_at == null) {
+ timestamp = this.props.currentUser.update_at;
+ } else {
+ timestamp = this.props.user.update_at;
}
let sameUserClass = '';
@@ -182,7 +182,7 @@ export default class Post extends React.Component {
let profilePic = null;
if (!this.props.hideProfilePic) {
- let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex();
+ let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp;
if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
if (post.props.override_icon_url) {
src = post.props.override_icon_url;
@@ -218,6 +218,7 @@ export default class Post extends React.Component {
isLastComment={this.props.isLastComment}
sameUser={this.props.sameUser}
user={this.props.user}
+ currentUser={this.props.currentUser}
/>
<PostBody
post={post}
@@ -226,7 +227,6 @@ export default class Post extends React.Component {
posts={posts}
handleCommentClick={this.handleCommentClick}
retryPost={this.retryPost}
- hasProfiles={this.props.hasProfiles}
/>
</div>
</div>
@@ -247,5 +247,6 @@ Post.propTypes = {
isLastComment: React.PropTypes.bool,
shouldHighlight: React.PropTypes.bool,
displayNameType: React.PropTypes.string,
- hasProfiles: React.PropTypes.bool
+ hasProfiles: React.PropTypes.bool,
+ currentUser: React.PropTypes.object.isRequired
};
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 70cf86748..2fa4cebfe 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -80,12 +80,10 @@ class PostBody extends React.Component {
username = parentPost.props.override_username;
}
- if (global.window.mm_locale === 'en') {
- if (username.slice(-1) === 's') {
- apostrophe = '\'';
- } else {
- apostrophe = '\'s';
- }
+ if (username.slice(-1) === 's') {
+ apostrophe = '\'';
+ } else {
+ apostrophe = '\'s';
}
name = (
<a
@@ -215,8 +213,7 @@ PostBody.propTypes = {
post: React.PropTypes.object.isRequired,
parentPost: React.PropTypes.object,
retryPost: React.PropTypes.func.isRequired,
- handleCommentClick: React.PropTypes.func.isRequired,
- hasProfiles: React.PropTypes.bool
+ handleCommentClick: React.PropTypes.func.isRequired
};
export default injectIntl(PostBody);
diff --git a/web/react/components/post_body_additional_content.jsx b/web/react/components/post_body_additional_content.jsx
index c2a928f3b..70b3c8dbf 100644
--- a/web/react/components/post_body_additional_content.jsx
+++ b/web/react/components/post_body_additional_content.jsx
@@ -112,24 +112,32 @@ export default class PostBodyAdditionalContent extends React.Component {
}
render() {
- var generateEmbed = this.generateEmbed();
+ const generateEmbed = this.generateEmbed();
+
if (generateEmbed) {
- return (
- <div>
+ let toggle;
+ if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_TOGGLE)) {
+ toggle = (
<a className='post__embed-visibility'
data-expanded={this.state.embedVisible}
aria-label='Toggle Embed Visibility'
onClick={this.toggleEmbedVisibility}
- >
- </a>
+ />
+ );
+ }
+
+ return (
+ <div>
+ {toggle}
<div className='post__embed-container'
hidden={!this.state.embedVisible}
>
{generateEmbed}
</div>
</div>
- );
+ );
}
+
return null;
}
}
diff --git a/web/react/components/post_focus_view.jsx b/web/react/components/post_focus_view.jsx
index 44a0bae09..fd654f502 100644
--- a/web/react/components/post_focus_view.jsx
+++ b/web/react/components/post_focus_view.jsx
@@ -5,7 +5,8 @@ import PostsView from './posts_view.jsx';
import PostStore from '../stores/post_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import UserStore from '../stores/user_store.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -15,6 +16,7 @@ export default class PostFocusView extends React.Component {
this.onChannelChange = this.onChannelChange.bind(this);
this.onPostsChange = this.onPostsChange.bind(this);
+ this.onUserChange = this.onUserChange.bind(this);
this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
@@ -26,18 +28,21 @@ export default class PostFocusView extends React.Component {
scrollPostId: focusedPostId,
postList: PostStore.getVisiblePosts(focusedPostId),
atTop: PostStore.getVisibilityAtTop(focusedPostId),
- atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
+ atBottom: PostStore.getVisibilityAtBottom(focusedPostId),
+ currentUser: UserStore.getCurrentUser()
};
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChannelChange);
PostStore.addChangeListener(this.onPostsChange);
+ UserStore.addChangeListener(this.onUserChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChannelChange);
PostStore.removeChangeListener(this.onPostsChange);
+ UserStore.removeChangeListener(this.onUserChange);
}
onChannelChange() {
@@ -46,6 +51,10 @@ export default class PostFocusView extends React.Component {
});
}
+ onUserChange() {
+ this.setState({currentUser: UserStore.getCurrentUser()});
+ }
+
onPostsChange() {
const focusedPostId = PostStore.getFocusedPostId();
if (focusedPostId == null) {
@@ -65,11 +74,11 @@ export default class PostFocusView extends React.Component {
}
loadMorePostsTop() {
- EventHelpers.emitLoadMorePostsFocusedTopEvent();
+ GlobalActions.emitLoadMorePostsFocusedTopEvent();
}
loadMorePostsBottom() {
- EventHelpers.emitLoadMorePostsFocusedBottomEvent();
+ GlobalActions.emitLoadMorePostsFocusedBottomEvent();
}
getIntroMessage() {
@@ -89,6 +98,10 @@ export default class PostFocusView extends React.Component {
const postsToHighlight = {};
postsToHighlight[this.state.scrollPostId] = true;
+ if (!this.state.currentUser || !this.state.postList) {
+ return null;
+ }
+
return (
<div id='post-list'>
<PostsView
@@ -106,6 +119,7 @@ export default class PostFocusView extends React.Component {
messageSeparatorTime={0}
postsToHighlight={postsToHighlight}
profiles={this.props.profiles}
+ currentUser={this.state.currentUser}
/>
</div>
);
diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx
index 2803fe387..966775dad 100644
--- a/web/react/components/post_header.jsx
+++ b/web/react/components/post_header.jsx
@@ -14,16 +14,15 @@ export default class PostHeader extends React.Component {
}
render() {
const post = this.props.post;
- const user = this.props.user;
- let userProfile = <UserProfile user={user}/>;
+ let userProfile = <UserProfile user={this.props.user}/>;
let botIndicator;
if (post.props && post.props.from_webhook) {
if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') {
userProfile = (
<UserProfile
- user={user}
+ user={this.props.user}
overwriteName={post.props.override_username}
disablePopover={true}
/>
@@ -54,6 +53,7 @@ export default class PostHeader extends React.Component {
allowReply='true'
isLastComment={this.props.isLastComment}
sameUser={this.props.sameUser}
+ currentUser={this.props.currentUser}
/>
</li>
</ul>
@@ -68,10 +68,11 @@ PostHeader.defaultProps = {
sameUser: false
};
PostHeader.propTypes = {
- post: React.PropTypes.object,
+ post: React.PropTypes.object.isRequired,
user: React.PropTypes.object,
- commentCount: React.PropTypes.number,
- isLastComment: React.PropTypes.bool,
- handleCommentClick: React.PropTypes.func,
- sameUser: React.PropTypes.bool
+ currentUser: React.PropTypes.object.isRequired,
+ commentCount: React.PropTypes.number.isRequired,
+ isLastComment: React.PropTypes.bool.isRequired,
+ handleCommentClick: React.PropTypes.func.isRequired,
+ sameUser: React.PropTypes.bool.isRequired
};
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index ffac6eaef..d0a4c828e 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -1,10 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import UserStore from '../stores/user_store.jsx';
import * as Utils from '../utils/utils.jsx';
import TimeSince from './time_since.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import Constants from '../utils/constants.jsx';
@@ -27,8 +26,8 @@ export default class PostInfo extends React.Component {
}
createDropdown() {
var post = this.props.post;
- var isOwner = UserStore.getCurrentId() === post.user_id;
- var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles);
+ var isOwner = this.props.currentUser.id === post.user_id;
+ var isAdmin = Utils.isAdmin(this.props.currentUser.roles);
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) {
return '';
@@ -47,21 +46,21 @@ export default class PostInfo extends React.Component {
if (this.props.allowReply === 'true') {
dropdownContents.push(
- <li
- key='replyLink'
- role='presentation'
- >
- <a
- className='link__reply theme'
- href='#'
- onClick={this.props.handleCommentClick}
- >
- <FormattedMessage
- id='post_info.reply'
- defaultMessage='Reply'
- />
- </a>
- </li>
+ <li
+ key='replyLink'
+ role='presentation'
+ >
+ <a
+ className='link__reply theme'
+ href='#'
+ onClick={this.props.handleCommentClick}
+ >
+ <FormattedMessage
+ id='post_info.reply'
+ defaultMessage='Reply'
+ />
+ </a>
+ </li>
);
}
@@ -93,7 +92,7 @@ export default class PostInfo extends React.Component {
<a
href='#'
role='menuitem'
- onClick={() => EventHelpers.showDeletePostModal(post, dataComments)}
+ onClick={() => GlobalActions.showDeletePostModal(post, dataComments)}
>
<FormattedMessage
id='post_info.del'
@@ -157,11 +156,11 @@ export default class PostInfo extends React.Component {
handlePermalink(e) {
e.preventDefault();
- EventHelpers.showGetPostLinkModal(this.props.post);
+ GlobalActions.showGetPostLinkModal(this.props.post);
}
removePost() {
- EventHelpers.emitRemovePost(this.props.post);
+ GlobalActions.emitRemovePost(this.props.post);
}
createRemovePostButton(post) {
if (!Utils.isPostEphemeral(post)) {
@@ -240,10 +239,11 @@ PostInfo.defaultProps = {
sameUser: false
};
PostInfo.propTypes = {
- post: React.PropTypes.object,
- commentCount: React.PropTypes.number,
- isLastComment: React.PropTypes.bool,
- allowReply: React.PropTypes.string,
- handleCommentClick: React.PropTypes.func,
- sameUser: React.PropTypes.bool
+ post: React.PropTypes.object.isRequired,
+ commentCount: React.PropTypes.number.isRequired,
+ isLastComment: React.PropTypes.bool.isRequired,
+ allowReply: React.PropTypes.string.isRequired,
+ handleCommentClick: React.PropTypes.func.isRequired,
+ sameUser: React.PropTypes.bool.isRequired,
+ currentUser: React.PropTypes.object.isRequired
};
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index 9a1673483..0a9232850 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -1,9 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import UserStore from '../stores/user_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import * as Utils from '../utils/utils.jsx';
import Post from './post.jsx';
import Constants from '../utils/constants.jsx';
@@ -144,7 +143,7 @@ export default class PostsView extends React.Component {
createPosts(posts, order) {
const postCtls = [];
let previousPostDay = new Date(0);
- const userId = UserStore.getCurrentId();
+ const userId = this.props.currentUser.id;
const profiles = this.props.profiles || {};
let renderedLastViewed = false;
@@ -230,8 +229,8 @@ export default class PostsView extends React.Component {
const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id);
let profile;
- if (UserStore.getCurrentId() === post.user_id) {
- profile = UserStore.getCurrentUser();
+ if (this.props.currentUser.id === post.user_id) {
+ profile = this.props.currentUser;
} else {
profile = profiles[post.user_id];
}
@@ -248,10 +247,10 @@ export default class PostsView extends React.Component {
hideProfilePic={hideProfilePic}
isLastComment={isLastComment}
shouldHighlight={shouldHighlight}
- onClick={() => EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func
+ onClick={() => GlobalActions.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func
displayNameType={this.state.displayNameType}
- hasProfiles={profiles && Object.keys(profiles).length > 1}
user={profile}
+ currentUser={this.props.currentUser}
/>
);
@@ -526,7 +525,7 @@ PostsView.defaultProps = {
PostsView.propTypes = {
isActive: React.PropTypes.bool,
postList: React.PropTypes.object,
- profiles: React.PropTypes.object,
+ profiles: React.PropTypes.object.isRequired,
scrollPostId: React.PropTypes.string,
scrollType: React.PropTypes.number,
postViewScrolled: React.PropTypes.func.isRequired,
@@ -536,7 +535,8 @@ PostsView.propTypes = {
showMoreMessagesBottom: React.PropTypes.bool,
introText: React.PropTypes.element,
messageSeparatorTime: React.PropTypes.number,
- postsToHighlight: React.PropTypes.object
+ postsToHighlight: React.PropTypes.object,
+ currentUser: React.PropTypes.object.isRequired
};
function FloatingTimestamp({isScrolling, post}) {
diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx
index 92d658b55..b361779d2 100644
--- a/web/react/components/posts_view_container.jsx
+++ b/web/react/components/posts_view_container.jsx
@@ -6,9 +6,10 @@ import LoadingScreen from './loading_screen.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import PostStore from '../stores/post_store.jsx';
+import UserStore from '../stores/user_store.jsx';
import * as Utils from '../utils/utils.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import Constants from '../utils/constants.jsx';
@@ -21,6 +22,7 @@ export default class PostsViewContainer extends React.Component {
this.onChannelChange = this.onChannelChange.bind(this);
this.onChannelLeave = this.onChannelLeave.bind(this);
this.onPostsChange = this.onPostsChange.bind(this);
+ this.onUserChange = this.onUserChange.bind(this);
this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this);
@@ -28,7 +30,8 @@ export default class PostsViewContainer extends React.Component {
const currentChannelId = ChannelStore.getCurrentId();
const state = {
scrollType: PostsView.SCROLL_TYPE_BOTTOM,
- scrollPost: null
+ scrollPost: null,
+ currentUser: UserStore.getCurrentUser()
};
if (currentChannelId) {
Object.assign(state, {
@@ -54,12 +57,17 @@ export default class PostsViewContainer extends React.Component {
ChannelStore.addLeaveListener(this.onChannelLeave);
PostStore.addChangeListener(this.onPostsChange);
PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest);
+ UserStore.addChangeListener(this.onUserChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChannelChange);
ChannelStore.removeLeaveListener(this.onChannelLeave);
PostStore.removeChangeListener(this.onPostsChange);
PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest);
+ UserStore.removeChangeListener(this.onUserChange);
+ }
+ onUserChange() {
+ this.setState({currentUser: UserStore.getCurrentUser()});
}
handlePostsViewJumpRequest(type, post) {
switch (type) {
@@ -139,7 +147,7 @@ export default class PostsViewContainer extends React.Component {
return PostStore.getVisiblePosts(id);
}
loadMorePostsTop() {
- EventHelpers.emitLoadMorePostsEvent();
+ GlobalActions.emitLoadMorePostsEvent();
}
handlePostsViewScroll(atBottom) {
if (atBottom) {
@@ -149,11 +157,15 @@ export default class PostsViewContainer extends React.Component {
}
}
shouldComponentUpdate(nextProps, nextState) {
- if (Utils.areObjectsEqual(this.state, nextState)) {
- return false;
+ if (!Utils.areObjectsEqual(this.state, nextState)) {
+ return true;
+ }
+
+ if (!Utils.areObjectsEqual(this.props, nextProps)) {
+ return true;
}
- return true;
+ return false;
}
render() {
const postLists = this.state.postLists;
@@ -161,6 +173,10 @@ export default class PostsViewContainer extends React.Component {
const currentChannelId = channels[this.state.currentChannelIndex];
const channel = ChannelStore.get(currentChannelId);
+ if (!this.state.currentUser || !channel) {
+ return null;
+ }
+
const postListCtls = [];
for (let i = 0; i < channels.length; i++) {
const isActive = (channels[i] === currentChannelId);
@@ -181,6 +197,7 @@ export default class PostsViewContainer extends React.Component {
introText={channel ? createChannelIntroMessage(channel) : null}
messageSeparatorTime={this.state.currentLastViewed}
profiles={this.props.profiles}
+ currentUser={this.state.currentUser}
/>
);
if (!postLists[i] && isActive) {
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index 9588809eb..9183b761f 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -14,7 +14,7 @@ import * as AsyncClient from '../utils/async_client.jsx';
var ActionTypes = Constants.ActionTypes;
import * as TextFormatting from '../utils/text_formatting.jsx';
import twemoji from 'twemoji';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl';
@@ -70,7 +70,7 @@ class RhsComment extends React.Component {
}
handlePermalink(e) {
e.preventDefault();
- EventHelpers.showGetPostLinkModal(this.props.post);
+ GlobalActions.showGetPostLinkModal(this.props.post);
}
componentDidMount() {
this.parseEmojis();
@@ -151,7 +151,7 @@ class RhsComment extends React.Component {
<a
href='#'
role='menuitem'
- onClick={() => EventHelpers.showDeletePostModal(post, 0)}
+ onClick={() => GlobalActions.showDeletePostModal(post, 0)}
>
<FormattedMessage
id='rhs_comment.del'
@@ -253,7 +253,7 @@ class RhsComment extends React.Component {
<div className='post__content'>
<div className='post__img'>
<img
- src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
+ src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp}
height='36'
width='36'
/>
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index 023f3dd2d..fc1cd0b41 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -10,7 +10,7 @@ import * as Emoji from '../utils/emoticons.jsx';
import FileAttachmentList from './file_attachment_list.jsx';
import twemoji from 'twemoji';
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import Constants from '../utils/constants.jsx';
@@ -34,7 +34,7 @@ export default class RhsRootPost extends React.Component {
}
handlePermalink(e) {
e.preventDefault();
- EventHelpers.showGetPostLinkModal(this.props.post);
+ GlobalActions.showGetPostLinkModal(this.props.post);
}
componentDidMount() {
this.parseEmojis();
@@ -142,7 +142,7 @@ export default class RhsRootPost extends React.Component {
<a
href='#'
role='menuitem'
- onClick={() => EventHelpers.showDeletePostModal(post, this.props.commentCount)}
+ onClick={() => GlobalActions.showDeletePostModal(post, this.props.commentCount)}
>
<FormattedMessage
id='rhs_root.del'
@@ -211,7 +211,7 @@ export default class RhsRootPost extends React.Component {
);
}
- let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex();
+ let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp;
if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
if (post.props.override_icon_url) {
src = post.props.override_icon_url;
diff --git a/web/react/components/root.jsx b/web/react/components/root.jsx
new file mode 100644
index 000000000..70038203b
--- /dev/null
+++ b/web/react/components/root.jsx
@@ -0,0 +1,90 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as GlobalActions from '../action_creators/global_actions.jsx';
+import BrowserStore from '../stores/browser_store.jsx';
+import LocalizationStore from '../stores/localization_store.jsx';
+
+var IntlProvider = ReactIntl.IntlProvider;
+
+export default class Root extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ locale: 'en',
+ translations: null
+ };
+
+ this.localizationChanged = this.localizationChanged.bind(this);
+ }
+ localizationChanged() {
+ this.setState({locale: LocalizationStore.getLocale(), translations: LocalizationStore.getTranslations()});
+ }
+ componentWillMount() {
+ // Setup localization listener
+ LocalizationStore.addChangeListener(this.localizationChanged);
+
+ // Browser store check version
+ BrowserStore.checkVersion();
+
+ window.onerror = (msg, url, line, column, stack) => {
+ var l = {};
+ l.level = 'ERROR';
+ l.message = 'msg: ' + msg + ' row: ' + line + ' col: ' + column + ' stack: ' + stack + ' url: ' + url;
+
+ $.ajax({
+ url: '/api/v1/admin/log_client',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(l)
+ });
+
+ if (window.mm_config.EnableDeveloper === 'true') {
+ window.ErrorStore.storeLastError({message: 'DEVELOPER MODE: A javascript error has occured. Please use the javascript console to capture and report the error (row: ' + line + ' col: ' + column + ').'});
+ window.ErrorStore.emitChange();
+ }
+ };
+
+ // Ya....
+ /*eslint-disable */
+ if (window.mm_config.SegmentDeveloperKey != null && window.mm_config.SegmentDeveloperKey !== "") {
+ !function(){var analytics=global.window.analytics=global.window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1";
+ analytics.load(window.mm_config.SegmentDeveloperKey);
+ analytics.page();
+ }}();
+ } else {
+ global.window.analytics = {};
+ global.window.analytics.page = function(){};
+ global.window.analytics.track = function(){};
+ }
+ /*eslint-enable */
+
+ // Get our localizaiton
+ GlobalActions.newLocalizationSelected('en');
+ }
+ componentWillUnmount() {
+ LocalizationStore.removeChangeListener(this.localizationChanged);
+ }
+ render() {
+ if (this.state.translations == null) {
+ return <div/>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.state.locale}
+ messages={this.state.translations}
+ key={this.state.locale}
+ >
+ {this.props.children}
+ </IntlProvider>
+ );
+ }
+}
+Root.defaultProps = {
+};
+
+Root.propTypes = {
+ children: React.PropTypes.object
+};
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index 05292b7b3..3a091bdd1 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -3,8 +3,7 @@
import UserStore from '../stores/user_store.jsx';
import UserProfile from './user_profile.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
-import * as utils from '../utils/utils.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import * as TextFormatting from '../utils/text_formatting.jsx';
import Constants from '../utils/constants.jsx';
@@ -22,7 +21,7 @@ export default class SearchResultsItem extends React.Component {
handleClick(e) {
e.preventDefault();
- EventHelpers.emitPostFocusEvent(this.props.post.id);
+ GlobalActions.emitPostFocusEvent(this.props.post.id);
if ($(window).width() < 768) {
$('.sidebar--right').removeClass('move--left');
@@ -32,7 +31,7 @@ export default class SearchResultsItem extends React.Component {
handleFocusRHSClick(e) {
e.preventDefault();
- EventHelpers.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch);
+ GlobalActions.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch);
}
render() {
@@ -78,7 +77,7 @@ export default class SearchResultsItem extends React.Component {
<div className='post__content'>
<div className='post__img'>
<img
- src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex()}
+ src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp}
height='36'
width='36'
/>
@@ -123,6 +122,7 @@ export default class SearchResultsItem extends React.Component {
</ul>
<div className='search-item-snippet'>
<span
+ onClick={TextFormatting.handleClick}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, formattingOptions)}}
/>
</div>
diff --git a/web/react/components/should_verify_email.jsx b/web/react/components/should_verify_email.jsx
new file mode 100644
index 000000000..c473fe366
--- /dev/null
+++ b/web/react/components/should_verify_email.jsx
@@ -0,0 +1,111 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {FormattedMessage} from 'mm-intl';
+import * as Client from '../utils/client.jsx';
+
+export default class ShouldVerifyEmail extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleResend = this.handleResend.bind(this);
+
+ this.state = {
+ resendStatus: 'none'
+ };
+ }
+ handleResend() {
+ const teamName = this.props.location.query.teamname;
+ const email = this.props.location.query.email;
+
+ this.setState({resendStatus: 'sending'});
+
+ Client.resendVerification(() => {
+ this.setState({resendStatus: 'success'});
+ },
+ () => {
+ this.setState({resendStatus: 'failure'});
+ },
+ teamName,
+ email);
+ }
+ render() {
+ let resendConfirm = '';
+ if (this.state.resendStatus === 'success') {
+ resendConfirm = (
+ <div>
+ <br/>
+ <p className='alert alert-success'>
+ <i className='fa fa-check'/>
+ <FormattedMessage
+ id='email_verify.sent'
+ defaultMessage=' Verification email sent.'
+ />
+ </p>
+ </div>
+ );
+ }
+
+ if (this.state.resendStatus === 'failure') {
+ resendConfirm = (
+ <div>
+ <br/>
+ <p className='alert alert-danger'>
+ <i className='fa fa-times'/>
+ <FormattedMessage id='email_verify.failed'/>
+ </p>
+ </div>
+ );
+ }
+
+ return (
+ <div>
+ <div className='signup-header'>
+ <a href='/'>
+ <span className='fa fa-chevron-left'/>
+ <FormattedMessage
+ id='web.header.back'
+ />
+ </a>
+ </div>
+ <div className='col-sm-12'>
+ <div className='signup-team__container'>
+ <h3>
+ <FormattedMessage
+ id='email_verify.almost'
+ defaultMessage='{siteName}: You are almost done'
+ values={{
+ siteName: global.window.mm_config.SiteName
+ }}
+ />
+ </h3>
+ <div>
+ <p>
+ <FormattedMessage
+ id='email_verify.notVerifiedBody'
+ defaultMessage='Please verify your email address. Check your inbox for an email.'
+ />
+ </p>
+ <button
+ onClick={this.handleResend}
+ className='btn btn-primary'
+ >
+ <FormattedMessage
+ id='email_verify.resend'
+ defaultMessage='Resend Email'
+ />
+ </button>
+ {resendConfirm}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+ShouldVerifyEmail.defaultProps = {
+};
+ShouldVerifyEmail.propTypes = {
+ location: React.PropTypes.object.isRequired
+};
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index c7dba306b..5c682d64b 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -129,7 +129,9 @@ export default class Sidebar extends React.Component {
directChannels,
hiddenDirectChannelCount,
unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
- showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER
+ showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER,
+ currentTeam: TeamStore.getCurrent(),
+ currentUser: UserStore.getCurrentUser()
};
}
@@ -179,7 +181,7 @@ export default class Sidebar extends React.Component {
}
updateTitle() {
const channel = ChannelStore.getCurrent();
- if (channel) {
+ if (channel && this.state.currentTeam) {
let currentSiteName = '';
if (global.window.mm_config.SiteName != null) {
currentSiteName = global.window.mm_config.SiteName;
@@ -196,7 +198,7 @@ export default class Sidebar extends React.Component {
const unread = this.getTotalUnreadCount();
const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : '';
const unreadTitle = unread.msgs > 0 ? '* ' : '';
- document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + TeamStore.getCurrent().display_name + ' ' + currentSiteName;
+ document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + this.state.currentTeam.display_name + ' ' + currentSiteName;
}
}
onScroll() {
@@ -401,7 +403,6 @@ export default class Sidebar extends React.Component {
// set up click handler to switch channels (or create a new channel for non-existant ones)
var handleClick = null;
var href = '#';
- var teamURL = TeamStore.getCurrentTeamUrl();
if (!channel.fake) {
handleClick = function clickHandler(e) {
@@ -413,7 +414,7 @@ export default class Sidebar extends React.Component {
e.preventDefault();
};
- } else if (channel.fake && teamURL) {
+ } else if (channel.fake) {
// It's a direct message channel that doesn't exist yet so let's create it now
var otherUserId = Utils.getUserIdFromChannelName(channel);
@@ -434,7 +435,7 @@ export default class Sidebar extends React.Component {
},
() => {
this.setState({loadingDMChannel: -1});
- window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name;
+ window.location.href = '/' + this.state.currentTeam.name;
}
);
}
@@ -497,6 +498,11 @@ export default class Sidebar extends React.Component {
);
}
render() {
+ // Check if we have all info needed to render
+ if (this.state.currentTeam == null || this.state.currentUser == null) {
+ return (<div/>);
+ }
+
this.badgesActive = false;
// keep track of the first and last unread channels so we can use them to set the unread indicators
@@ -586,7 +592,10 @@ export default class Sidebar extends React.Component {
);
return (
- <div>
+ <div
+ className='sidebar--left'
+ id='sidebar-left'
+ >
<NewChannelFlow
show={showChannelModal}
channelType={this.state.newChannelModalType}
@@ -598,9 +607,10 @@ export default class Sidebar extends React.Component {
/>
<SidebarHeader
- teamDisplayName={TeamStore.getCurrent().display_name}
- teamName={TeamStore.getCurrent().name}
- teamType={TeamStore.getCurrent().type}
+ teamDisplayName={this.state.currentTeam.display_name}
+ teamName={this.state.currentTeam.name}
+ teamType={this.state.currentTeam.type}
+ currentUser={this.state.currentUser}
/>
<UnreadChannelIndicator
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index 45b0a5fc4..00d30948a 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -4,10 +4,8 @@
import NavbarDropdown from './navbar_dropdown.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';
-import UserStore from '../stores/user_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
-import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
import {FormattedHTMLMessage} from 'mm-intl';
@@ -34,7 +32,7 @@ export default class SidebarHeader extends React.Component {
PreferenceStore.removeChangeListener(this.onPreferenceChange);
}
getStateFromStores() {
- const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
+ const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, this.props.currentUser.id, 999);
return {showTutorialTip: tutorialStep === TutorialSteps.MENU_POPOVER};
}
@@ -77,7 +75,7 @@ export default class SidebarHeader extends React.Component {
);
}
render() {
- var me = UserStore.getCurrentUser();
+ var me = this.props.currentUser;
var profilePicture = null;
if (!me) {
@@ -88,7 +86,7 @@ export default class SidebarHeader extends React.Component {
profilePicture = (
<img
className='user__picture'
- src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()}
+ src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
/>
);
}
@@ -124,6 +122,7 @@ export default class SidebarHeader extends React.Component {
teamType={this.props.teamType}
teamDisplayName={this.props.teamDisplayName}
teamName={this.props.teamName}
+ currentUser={this.props.currentUser}
/>
</div>
);
@@ -131,11 +130,12 @@ export default class SidebarHeader extends React.Component {
}
SidebarHeader.defaultProps = {
- teamDisplayName: global.window.mm_config.SiteName,
+ teamDisplayName: '',
teamType: ''
};
SidebarHeader.propTypes = {
teamDisplayName: React.PropTypes.string,
teamName: React.PropTypes.string,
- teamType: React.PropTypes.string
+ teamType: React.PropTypes.string,
+ currentUser: React.PropTypes.object
};
diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx
index b81c0d099..14853d3a3 100644
--- a/web/react/components/sidebar_right.jsx
+++ b/web/react/components/sidebar_right.jsx
@@ -127,8 +127,13 @@ export default class SidebarRight extends React.Component {
}
return (
- <div className='sidebar-right-container'>
- {content}
+ <div
+ className='sidebar--right'
+ id='sidebar-right'
+ >
+ <div className='sidebar-right-container'>
+ {content}
+ </div>
</div>
);
}
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index 4d714e9f1..c7c5bcfd6 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -5,11 +5,11 @@ import TeamMembersModal from './team_members_modal.jsx';
import ToggleModalButton from './toggle_modal_button.jsx';
import UserSettingsModal from './user_settings/user_settings_modal.jsx';
import UserStore from '../stores/user_store.jsx';
-import * as client from '../utils/client.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
-import * as utils from '../utils/utils.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
+import * as Utils from '../utils/utils.jsx';
import {FormattedMessage} from 'mm-intl';
+import {Link} from 'react-router';
export default class SidebarRightMenu extends React.Component {
componentDidMount() {
@@ -19,18 +19,11 @@ export default class SidebarRightMenu extends React.Component {
constructor(props) {
super(props);
- this.handleLogoutClick = this.handleLogoutClick.bind(this);
-
this.state = {
showUserSettingsModal: false
};
}
- handleLogoutClick(e) {
- e.preventDefault();
- client.logout();
- }
-
render() {
var teamLink = '';
var inviteLink = '';
@@ -42,14 +35,14 @@ export default class SidebarRightMenu extends React.Component {
var isSystemAdmin = false;
if (currentUser != null) {
- isAdmin = utils.isAdmin(currentUser.roles);
- isSystemAdmin = utils.isSystemAdmin(currentUser.roles);
+ isAdmin = Utils.isAdmin(currentUser.roles);
+ isSystemAdmin = Utils.isSystemAdmin(currentUser.roles);
inviteLink = (
<li>
<a
href='#'
- onClick={EventHelpers.showInviteMemberModal}
+ onClick={GlobalActions.showInviteMemberModal}
>
<i className='fa fa-user'></i>
<FormattedMessage
@@ -65,7 +58,7 @@ export default class SidebarRightMenu extends React.Component {
<li>
<a
href='#'
- onClick={EventHelpers.showGetTeamInviteLinkModal}
+ onClick={GlobalActions.showGetTeamInviteLinkModal}
>
<i className='glyphicon glyphicon-link'></i>
<FormattedMessage
@@ -107,13 +100,13 @@ export default class SidebarRightMenu extends React.Component {
);
}
- if (isSystemAdmin && !utils.isMobile()) {
+ if (isSystemAdmin && !Utils.isMobile()) {
consoleLink = (
<li>
<a
- href={'/admin_console?' + utils.getSessionIndex()}
+ href={'/admin_console'}
>
- <i className='fa fa-wrench'></i>
+ <i className='fa fa-wrench'></i>
<FormattedMessage
id='sidebar_right_menu.console'
defaultMessage='System Console'
@@ -168,7 +161,10 @@ export default class SidebarRightMenu extends React.Component {
);
}
return (
- <div>
+ <div
+ className='sidebar--menu'
+ id='sidebar-menu'
+ >
<div className='team__header theme'>
<a
className='team__name'
@@ -196,16 +192,13 @@ export default class SidebarRightMenu extends React.Component {
{manageLink}
{consoleLink}
<li>
- <a
- href='#'
- onClick={this.handleLogoutClick}
- >
+ <Link to={Utils.getTeamURLFromAddressBar() + '/logout'}>
<i className='fa fa-sign-out'></i>
<FormattedMessage
id='sidebar_right_menu.logout'
defaultMessage='Logout'
/>
- </a>
+ </Link>
</li>
<li className='divider'></li>
{helpLink}
diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx
index 26c46dad0..2adf8d111 100644
--- a/web/react/components/signup_team.jsx
+++ b/web/react/components/signup_team.jsx
@@ -6,6 +6,8 @@ import EmailSignUpPage from './team_signup_with_email.jsx';
import SSOSignupPage from './team_signup_with_sso.jsx';
import LdapSignUpPage from './team_signup_with_ldap.jsx';
import Constants from '../utils/constants.jsx';
+import TeamStore from '../stores/team_store.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -14,6 +16,7 @@ export default class TeamSignUp extends React.Component {
super(props);
this.updatePage = this.updatePage.bind(this);
+ this.onTeamUpdate = this.onTeamUpdate.bind(this);
var count = 0;
@@ -46,11 +49,34 @@ export default class TeamSignUp extends React.Component {
this.setState({page});
}
+ componentWillMount() {
+ if (global.window.mm_config.EnableTeamListing === 'true') {
+ AsyncClient.getAllTeams();
+ this.onTeamUpdate();
+ }
+ }
+
+ componentDidMount() {
+ TeamStore.addChangeListener(this.onTeamUpdate);
+ }
+
+ componentWillUnmount() {
+ TeamStore.removeChangeListener(this.onTeamUpdate);
+ }
+
+ onTeamUpdate() {
+ this.setState({
+ teams: TeamStore.getAll()
+ });
+ }
+
render() {
- var teamListing = null;
+ let teamListing = null;
if (global.window.mm_config.EnableTeamListing === 'true') {
- if (this.props.teams.length === 0) {
+ if (this.state.teams == null) {
+ teamListing = (<div/>);
+ } else if (this.state.teams.length === 0) {
if (global.window.mm_config.EnableTeamCreation !== 'true') {
teamListing = (
<div>
@@ -72,23 +98,26 @@ export default class TeamSignUp extends React.Component {
</h4>
<div className='signup-team-all'>
{
- this.props.teams.map((team) => {
- return (
- <div
- key={'team_' + team.name}
- className='signup-team-dir'
- >
- <a
- href={'/' + team.name}
+ Object.values(this.state.teams).map((team) => {
+ if (team.allow_team_listing) {
+ return (
+ <div
+ key={'team_' + team.name}
+ className='signup-team-dir'
>
- <span className='signup-team-dir__name'>{team.display_name}</span>
- <span
- className='glyphicon glyphicon-menu-right right signup-team-dir__arrow'
- aria-hidden='true'
- />
- </a>
- </div>
- );
+ <a
+ href={'/' + team.name}
+ >
+ <span className='signup-team-dir__name'>{team.display_name}</span>
+ <span
+ className='glyphicon glyphicon-menu-right right signup-team-dir__arrow'
+ aria-hidden='true'
+ />
+ </a>
+ </div>
+ );
+ }
+ return null;
})
}
</div>
@@ -103,42 +132,26 @@ export default class TeamSignUp extends React.Component {
}
}
+ let signupMethod = null;
+
if (global.window.mm_config.EnableTeamCreation !== 'true') {
if (teamListing == null) {
- return (
- <div>
- <FormattedMessage
- id='signup_team.disabled'
- defaultMessage='Team creation has been disabled. Please contact an administrator for access.'
- />
- </div>
+ signupMethod = (
+ <FormattedMessage
+ id='signup_team.disabled'
+ defaultMessage='Team creation has been disabled. Please contact an administrator for access.'
+ />
);
}
-
- return (
- <div>
- {teamListing}
- </div>
- );
- }
-
- if (this.state.page === 'choose') {
- return (
- <div>
- {teamListing}
- <ChoosePage
- updatePage={this.updatePage}
- />
- </div>
+ } else if (this.state.page === 'choose') {
+ signupMethod = (
+ <ChoosePage
+ updatePage={this.updatePage}
+ />
);
- }
-
- if (this.state.page === 'email') {
- return (
- <div>
- {teamListing}
- <EmailSignUpPage/>
- </div>
+ } else if (this.state.page === 'email') {
+ signupMethod = (
+ <EmailSignUpPage/>
);
} else if (this.state.page === 'ldap') {
return (
@@ -148,35 +161,45 @@ export default class TeamSignUp extends React.Component {
</div>
);
} else if (this.state.page === 'gitlab') {
- return (
- <div>
- {teamListing}
- <SSOSignupPage service={Constants.GITLAB_SERVICE}/>
- </div>
+ signupMethod = (
+ <SSOSignupPage service={Constants.GITLAB_SERVICE}/>
);
} else if (this.state.page === 'google') {
- return (
- <div>
- {teamListing}
- <SSOSignupPage service={Constants.GOOGLE_SERVICE}/>
- </div>
+ signupMethod = (
+ <SSOSignupPage service={Constants.GOOGLE_SERVICE}/>
);
} else if (this.state.page === 'none') {
- return (
- <div>
- <FormattedMessage
- id='signup_team.none'
- defaultMessage='No team creation method has been enabled. Please contact an administrator for access.'
- />
- </div>
+ signupMethod = (
+ <FormattedMessage
+ id='signup_team.none'
+ defaultMessage='No team creation method has been enabled. Please contact an administrator for access.'
+ />
);
}
- return null;
+ return (
+ <div className='col-sm-12'>
+ <div className='signup-team__container'>
+ <img
+ className='signup-team-logo'
+ src='/static/images/logo.png'
+ />
+ <h1>{global.window.mm_config.SiteName}</h1>
+ <h4 className='color--light'>
+ <FormattedMessage
+ id='web.root.singup_info'
+ />
+ </h4>
+ <div id='signup-team'>
+ {teamListing}
+ {signupMethod}
+ </div>
+ </div>
+ </div>
+ );
}
}
TeamSignUp.propTypes = {
- teams: React.PropTypes.array
};
diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx
deleted file mode 100644
index 16553daeb..000000000
--- a/web/react/components/signup_team_complete.jsx
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import WelcomePage from './team_signup_welcome_page.jsx';
-import TeamDisplayNamePage from './team_signup_display_name_page.jsx';
-import TeamURLPage from './team_signup_url_page.jsx';
-import SendInivtesPage from './team_signup_send_invites_page.jsx';
-import UsernamePage from './team_signup_username_page.jsx';
-import PasswordPage from './team_signup_password_page.jsx';
-import BrowserStore from '../stores/browser_store.jsx';
-
-import {FormattedMessage} from 'mm-intl';
-
-export default class SignupTeamComplete extends React.Component {
- constructor(props) {
- super(props);
-
- this.updateParent = this.updateParent.bind(this);
-
- var initialState = BrowserStore.getGlobalItem(props.hash);
-
- if (!initialState) {
- initialState = {};
- initialState.wizard = 'welcome';
- initialState.team = {};
- initialState.team.email = this.props.email;
- initialState.team.allowed_domains = '';
- initialState.invites = [];
- initialState.invites.push('');
- initialState.invites.push('');
- initialState.invites.push('');
- initialState.user = {};
- initialState.hash = this.props.hash;
- initialState.data = this.props.data;
- }
-
- this.state = initialState;
- }
- updateParent(state, skipSet) {
- BrowserStore.setGlobalItem(this.props.hash, state);
-
- if (!skipSet) {
- this.setState(state);
- }
- }
- render() {
- if (this.state.wizard === 'welcome') {
- return (
- <WelcomePage
- state={this.state}
- updateParent={this.updateParent}
- />
- );
- }
-
- if (this.state.wizard === 'team_display_name') {
- return (
- <TeamDisplayNamePage
- state={this.state}
- updateParent={this.updateParent}
- />
- );
- }
-
- if (this.state.wizard === 'team_url') {
- return (
- <TeamURLPage
- state={this.state}
- updateParent={this.updateParent}
- />
- );
- }
-
- if (this.state.wizard === 'send_invites') {
- return (
- <SendInivtesPage
- state={this.state}
- updateParent={this.updateParent}
- />
- );
- }
-
- if (this.state.wizard === 'username') {
- return (
- <UsernamePage
- state={this.state}
- updateParent={this.updateParent}
- />
- );
- }
-
- if (this.state.wizard === 'password') {
- return (
- <PasswordPage
- state={this.state}
- updateParent={this.updateParent}
- />
- );
- }
-
- return (
- <div>
- <FormattedMessage
- id='signup_team_complete.completed'
- defaultMessage="You've already completed the signup process for this invitation or this invitation has expired."
- />
- </div>
- );
- }
-}
-
-SignupTeamComplete.defaultProps = {
- hash: '',
- email: '',
- data: ''
-};
-SignupTeamComplete.propTypes = {
- hash: React.PropTypes.string,
- email: React.PropTypes.string,
- data: React.PropTypes.string
-};
diff --git a/web/react/components/signup_team_complete/components/signup_team_complete.jsx b/web/react/components/signup_team_complete/components/signup_team_complete.jsx
new file mode 100644
index 000000000..5ad21e941
--- /dev/null
+++ b/web/react/components/signup_team_complete/components/signup_team_complete.jsx
@@ -0,0 +1,79 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import BrowserStore from '../../../stores/browser_store.jsx';
+
+import {FormattedMessage} from 'mm-intl';
+
+import {browserHistory} from 'react-router';
+
+export default class SignupTeamComplete extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateParent = this.updateParent.bind(this);
+ }
+ componentWillMount() {
+ const data = JSON.parse(this.props.location.query.d);
+ this.hash = this.props.location.query.h;
+
+ var initialState = BrowserStore.getGlobalItem(this.hash);
+
+ if (!initialState) {
+ initialState = {};
+ initialState.wizard = 'welcome';
+ initialState.team = {};
+ initialState.team.email = data.email;
+ initialState.team.allowed_domains = '';
+ initialState.invites = [];
+ initialState.invites.push('');
+ initialState.invites.push('');
+ initialState.invites.push('');
+ initialState.user = {};
+ initialState.hash = this.hash;
+ initialState.data = this.props.location.query.d;
+ }
+
+ this.setState(initialState);
+ }
+ componentDidMount() {
+ browserHistory.push('/signup_team_complete/welcome');
+ }
+ updateParent(state, skipSet) {
+ BrowserStore.setGlobalItem(this.hash, state);
+
+ if (!skipSet) {
+ this.setState(state);
+ browserHistory.push('/signup_team_complete/' + state.wizard);
+ }
+ }
+ render() {
+ return (
+ <div>
+ <div className='signup-header'>
+ <a href='/'>
+ <span classNameName='fa fa-chevron-left'/>
+ <FormattedMessage id='web.header.back'/>
+ </a>
+ </div>
+ <div className='col-sm-12'>
+ <div className='signup-team__container'>
+ <div id='signup-team-complete'>
+ {React.cloneElement(this.props.children, {
+ state: this.state,
+ updateParent: this.updateParent
+ })}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+SignupTeamComplete.defaultProps = {
+};
+SignupTeamComplete.propTypes = {
+ location: React.PropTypes.object,
+ children: React.PropTypes.node
+};
diff --git a/web/react/components/team_signup_display_name_page.jsx b/web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx
index f07b50756..280e53ce4 100644
--- a/web/react/components/team_signup_display_name_page.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as utils from '../utils/utils.jsx';
-import * as client from '../utils/client.jsx';
+import * as utils from '../../../utils/utils.jsx';
+import * as client from '../../../utils/client.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
@@ -133,4 +133,4 @@ TeamSignupDisplayNamePage.propTypes = {
updateParent: React.PropTypes.func
};
-export default injectIntl(TeamSignupDisplayNamePage); \ No newline at end of file
+export default injectIntl(TeamSignupDisplayNamePage);
diff --git a/web/react/components/team_signup_email_item.jsx b/web/react/components/signup_team_complete/components/team_signup_email_item.jsx
index 790ec2e5d..c87d6ec07 100644
--- a/web/react/components/team_signup_email_item.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_email_item.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as Utils from '../utils/utils.jsx';
+import * as Utils from '../../../utils/utils.jsx';
import {intlShape, injectIntl, defineMessages} from 'mm-intl';
diff --git a/web/react/components/signup_team_complete/components/team_signup_finished.jsx b/web/react/components/signup_team_complete/components/team_signup_finished.jsx
new file mode 100644
index 000000000..fc5f756e7
--- /dev/null
+++ b/web/react/components/signup_team_complete/components/team_signup_finished.jsx
@@ -0,0 +1,15 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {FormattedMessage} from 'mm-intl';
+
+export default class FinishedPage extends React.Component {
+ render() {
+ return (
+ <FormattedMessage
+ id='signup_team_complete.completed'
+ defaultMessage="You've already completed the signup process for this invitation or this invitation has expired."
+ />
+ );
+ }
+}
diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/signup_team_complete/components/team_signup_password_page.jsx
index 06c04854f..490a11040 100644
--- a/web/react/components/team_signup_password_page.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_password_page.jsx
@@ -1,12 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as Client from '../utils/client.jsx';
-import BrowserStore from '../stores/browser_store.jsx';
-import UserStore from '../stores/user_store.jsx';
-import Constants from '../utils/constants.jsx';
+import * as Client from '../../../utils/client.jsx';
+import BrowserStore from '../../../stores/browser_store.jsx';
+import UserStore from '../../../stores/user_store.jsx';
+import Constants from '../../../utils/constants.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
+import {browserHistory} from 'react-router';
const holders = defineMessages({
passwordError: {
@@ -66,11 +67,11 @@ class TeamSignupPasswordPage extends React.Component {
props.state.wizard = 'finished';
props.updateParent(props.state, true);
- window.location.href = '/' + teamSignup.team.name + '/channels/town-square';
+ browserHistory.push('/' + teamSignup.team.name + '/channels/town-square');
},
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
- window.location.href = '/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name);
+ browserHistory.push('/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name));
} else {
this.setState({serverError: err.message});
$('#finish-button').button('reset');
@@ -211,4 +212,4 @@ TeamSignupPasswordPage.propTypes = {
updateParent: React.PropTypes.func
};
-export default injectIntl(TeamSignupPasswordPage); \ No newline at end of file
+export default injectIntl(TeamSignupPasswordPage);
diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx
index 55cfe5114..5e987ef2c 100644
--- a/web/react/components/team_signup_send_invites_page.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx
@@ -2,7 +2,7 @@
// See License.txt for license information.
import EmailItem from './team_signup_email_item.jsx';
-import * as Client from '../utils/client.jsx';
+import * as Client from '../../../utils/client.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/signup_team_complete/components/team_signup_url_page.jsx
index 2f6c3df49..ec50e2d25 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_url_page.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as Utils from '../utils/utils.jsx';
-import * as Client from '../utils/client.jsx';
-import Constants from '../utils/constants.jsx';
+import * as Utils from '../../../utils/utils.jsx';
+import * as Client from '../../../utils/client.jsx';
+import Constants from '../../../utils/constants.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
@@ -202,4 +202,4 @@ TeamSignupUrlPage.propTypes = {
updateParent: React.PropTypes.func
};
-export default injectIntl(TeamSignupUrlPage); \ No newline at end of file
+export default injectIntl(TeamSignupUrlPage);
diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/signup_team_complete/components/team_signup_username_page.jsx
index 0fa9cb103..e56aa4cd7 100644
--- a/web/react/components/team_signup_username_page.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_username_page.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as Utils from '../utils/utils.jsx';
-import * as Client from '../utils/client.jsx';
-import Constants from '../utils/constants.jsx';
+import * as Utils from '../../../utils/utils.jsx';
+import * as Client from '../../../utils/client.jsx';
+import Constants from '../../../utils/constants.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
@@ -161,4 +161,4 @@ TeamSignupUsernamePage.propTypes = {
updateParent: React.PropTypes.func
};
-export default injectIntl(TeamSignupUsernamePage); \ No newline at end of file
+export default injectIntl(TeamSignupUsernamePage);
diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx
index 9939c3ffd..97782e54a 100644
--- a/web/react/components/team_signup_welcome_page.jsx
+++ b/web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx
@@ -1,12 +1,14 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as Utils from '../utils/utils.jsx';
-import * as Client from '../utils/client.jsx';
-import BrowserStore from '../stores/browser_store.jsx';
+import * as Utils from '../../../utils/utils.jsx';
+import * as Client from '../../../utils/client.jsx';
+import BrowserStore from '../../../stores/browser_store.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
+import {browserHistory} from 'react-router';
+
const holders = defineMessages({
storageError: {
id: 'team_signup_welcome.storageError',
@@ -73,7 +75,7 @@ class TeamSignupWelcomePage extends React.Component {
} else {
this.props.state.wizard = 'finished';
this.props.updateParent(this.props.state);
- window.location.href = '/signup_team_confirm/?email=' + encodeURIComponent(email);
+ browserHistory.push('/signup_team_confirm/?email=' + encodeURIComponent(email));
}
}.bind(this),
function error(err) {
@@ -229,4 +231,4 @@ TeamSignupWelcomePage.propTypes = {
state: React.PropTypes.object
};
-export default injectIntl(TeamSignupWelcomePage); \ No newline at end of file
+export default injectIntl(TeamSignupWelcomePage);
diff --git a/web/react/components/signup_team_confirm.jsx b/web/react/components/signup_team_confirm.jsx
index 290d8e503..1afbb3d30 100644
--- a/web/react/components/signup_team_confirm.jsx
+++ b/web/react/components/signup_team_confirm.jsx
@@ -6,30 +6,41 @@ import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
export default class SignupTeamConfirm extends React.Component {
render() {
return (
- <div className='signup-team__container'>
- <h3>
- <FormattedMessage
- id='signup_team_confirm.title'
- defaultMessage='Sign up Complete'
- />
- </h3>
- <p>
- <FormattedHTMLMessage
- id='signup_team_confirm.checkEmail'
- defaultMessage='Please check your email: <strong>{email}</strong><br />Your email contains a link to set up your team'
- values={{
- email: this.props.email
- }}
- />
- </p>
+ <div>
+ <div className='signup-header'>
+ <a href='/'>
+ <span className='fa fa-chevron-left'/>
+ <FormattedMessage
+ id='web.header.back'
+ />
+ </a>
+ </div>
+ <div className='col-sm-12'>
+ <div classNameName='signup-team__container'>
+ <h3>
+ <FormattedMessage
+ id='signup_team_confirm.title'
+ defaultMessage='Sign up Complete'
+ />
+ </h3>
+ <p>
+ <FormattedHTMLMessage
+ id='signup_team_confirm.checkEmail'
+ defaultMessage='Please check your email: <strong>{email}</strong><br />Your email contains a link to set up your team'
+ values={{
+ email: this.props.location.query.email
+ }}
+ />
+ </p>
+ </div>
+ </div>
</div>
);
}
}
SignupTeamConfirm.defaultProps = {
- email: ''
};
SignupTeamConfirm.propTypes = {
- email: React.PropTypes.string
+ location: React.PropTypes.object
};
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index dbec3d02d..d2128a50f 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -2,83 +2,130 @@
// See License.txt for license information.
import * as Utils from '../utils/utils.jsx';
-import * as client from '../utils/client.jsx';
+import * as Client from '../utils/client.jsx';
import UserStore from '../stores/user_store.jsx';
import BrowserStore from '../stores/browser_store.jsx';
import Constants from '../utils/constants.jsx';
+import LoadingScreen from '../components/loading_screen.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
-
-const holders = defineMessages({
- required: {
- id: 'signup_user_completed.required',
- defaultMessage: 'This field is required'
- },
- validEmail: {
- id: 'signup_user_completed.validEmail',
- defaultMessage: 'Please enter a valid email address'
- },
- reserved: {
- id: 'signup_user_completed.reserved',
- defaultMessage: 'This username is reserved, please choose a new one.'
- },
- usernameLength: {
- id: 'signup_user_completed.usernameLength',
- defaultMessage: 'Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols \'.\', \'-\' and \'_\'.'
- },
- passwordLength: {
- id: 'signup_user_completed.passwordLength',
- defaultMessage: 'Please enter at least {min} characters'
- }
-});
+import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
+import {browserHistory} from 'react-router';
class SignupUserComplete extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
+ this.inviteInfoRecieved = this.inviteInfoRecieved.bind(this);
+
+ this.state = {
+ data: '',
+ hash: '',
+ usedBefore: false,
+ email: '',
+ teamDisplayName: '',
+ teamName: '',
+ teamId: ''
+ };
+ }
+ componentWillMount() {
+ let data = this.props.location.query.d;
+ let hash = this.props.location.query.h;
+ const inviteId = this.props.location.query.id;
+ let usedBefore = false;
+ let email = '';
+ let teamDisplayName = '';
+ let teamName = '';
+ let teamId = '';
+
+ // If we have a hash in the url then we are attempting to access a private team
+ if (hash) {
+ const parsedData = JSON.parse(data);
+ usedBefore = BrowserStore.getGlobalItem(hash);
+ email = parsedData.email;
+ teamDisplayName = parsedData.display_name;
+ teamName = parsedData.name;
+ teamId = parsedData.id;
+ } else {
+ Client.getInviteInfo(this.inviteInfoRecieved, null, inviteId);
+ data = '';
+ hash = '';
+ }
- var initialState = BrowserStore.getGlobalItem(this.props.hash);
-
- if (!initialState) {
- initialState = {};
- initialState.wizard = 'welcome';
- initialState.user = {};
- initialState.user.team_id = this.props.teamId;
- initialState.user.email = this.props.email;
- initialState.original_email = this.props.email;
+ this.setState({
+ data,
+ hash,
+ usedBefore,
+ email,
+ teamDisplayName,
+ teamName,
+ teamId
+ });
+ }
+ inviteInfoRecieved(data) {
+ if (!data) {
+ return;
}
- this.state = initialState;
+ this.setState({
+ teamDisplayName: data.display_name,
+ teamName: data.name,
+ teamId: data.id
+ });
}
handleSubmit(e) {
e.preventDefault();
- const {formatMessage} = this.props.intl;
const providedEmail = ReactDOM.findDOMNode(this.refs.email).value.trim();
if (!providedEmail) {
- this.setState({nameError: '', emailError: formatMessage(holders.required), passwordError: ''});
+ this.setState({
+ nameError: '',
+ emailError: (<FormattedMessage id='signup_user_completed.required'/>),
+ passwordError: '',
+ serverError: ''
+ });
return;
}
if (!Utils.isEmail(providedEmail)) {
- this.setState({nameError: '', emailError: formatMessage(holders.validEmail), passwordError: ''});
+ this.setState({
+ nameError: '',
+ emailError: (<FormattedMessage id='signup_user_completed.validEmail'/>),
+ passwordError: '',
+ serverError: ''
+ });
return;
}
const providedUsername = ReactDOM.findDOMNode(this.refs.name).value.trim().toLowerCase();
if (!providedUsername) {
- this.setState({nameError: formatMessage(holders.required), emailError: '', passwordError: '', serverError: ''});
+ this.setState({
+ nameError: (<FormattedMessage id='signup_user_completed.required'/>),
+ emailError: '',
+ passwordError: '',
+ serverError: ''
+ });
return;
}
const usernameError = Utils.isValidUsername(providedUsername);
if (usernameError === 'Cannot use a reserved word as a username.') {
- this.setState({nameError: formatMessage(holders.reserved), emailError: '', passwordError: '', serverError: ''});
+ this.setState({
+ nameError: (<FormattedMessage id='signup_user_completed.reserved'/>),
+ emailError: '',
+ passwordError: '',
+ serverError: ''
+ });
return;
} else if (usernameError) {
this.setState({
- nameError: formatMessage(holders.usernameLength, {min: Constants.MIN_USERNAME_LENGTH, max: Constants.MAX_USERNAME_LENGTH}),
+ nameError: (
+ <FormattedMessage
+ id='signup_user_completed.usernameLength'
+ min={Constants.MIN_USERNAME_LENGTH}
+ max={Constants.MAX_USERNAME_LENGTH}
+ />
+ ),
emailError: '',
passwordError: '',
serverError: ''
@@ -88,41 +135,50 @@ class SignupUserComplete extends React.Component {
const providedPassword = ReactDOM.findDOMNode(this.refs.password).value.trim();
if (!providedPassword || providedPassword.length < Constants.MIN_PASSWORD_LENGTH) {
- this.setState({nameError: '', emailError: '', passwordError: formatMessage(holders.passwordLength, {min: Constants.MIN_PASSWORD_LENGTH}), serverError: ''});
+ this.setState({
+ nameError: '',
+ emailError: '',
+ passwordError: (
+ <FormattedMessage
+ id='signup_user_completed.passwordLength'
+ min={Constants.MIN_PASSWORD_LENGTH}
+ />
+ ),
+ serverError: ''
+ });
return;
}
- const user = {
- team_id: this.props.teamId,
- email: providedEmail,
- username: providedUsername,
- password: providedPassword,
- allow_marketing: true
- };
-
this.setState({
- user,
nameError: '',
emailError: '',
passwordError: '',
serverError: ''
});
- client.createUser(user, this.props.data, this.props.hash,
+ const user = {
+ team_id: this.state.teamId,
+ email: providedEmail,
+ username: providedUsername,
+ password: providedPassword,
+ allow_marketing: true
+ };
+
+ Client.createUser(user, this.state.data, this.state.hash,
() => {
- client.track('signup', 'signup_user_02_complete');
+ Client.track('signup', 'signup_user_02_complete');
- client.loginByEmail(this.props.teamName, user.email, user.password,
+ Client.loginByEmail(this.state.teamName, user.email, user.password,
() => {
UserStore.setLastEmail(user.email);
- if (this.props.hash > 0) {
- BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'}));
+ if (this.state.hash > 0) {
+ BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true}));
}
- window.location.href = '/' + this.props.teamName + '/channels/town-square';
+ browserHistory.push('/' + this.state.teamName + '/channels/town-square');
},
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
- window.location.href = '/verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.props.teamName);
+ browserHistory.push('/should_verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.state.teamName));
} else {
this.setState({serverError: err.message});
}
@@ -135,9 +191,10 @@ class SignupUserComplete extends React.Component {
);
}
render() {
- client.track('signup', 'signup_user_01_welcome');
+ Client.track('signup', 'signup_user_01_welcome');
- if (this.state.wizard === 'finished') {
+ // If we have been used then just display a message
+ if (this.state.usedBefore) {
return (
<div>
<FormattedMessage
@@ -148,6 +205,12 @@ class SignupUserComplete extends React.Component {
);
}
+ // If we haven't got a team id yet we are waiting for
+ // the client so just show the standard loading screen
+ if (this.state.teamId === '') {
+ return (<LoadingScreen/>);
+ }
+
// set up error labels
var emailError = null;
var emailHelpText = (
@@ -160,7 +223,7 @@ class SignupUserComplete extends React.Component {
);
var emailDivStyle = 'form-group';
if (this.state.emailError) {
- emailError = <label className='control-label'>{this.state.emailError}</label>;
+ emailError = (<label className='control-label'>{this.state.emailError}</label>);
emailHelpText = '';
emailDivStyle += ' has-error';
}
@@ -203,13 +266,13 @@ class SignupUserComplete extends React.Component {
// set up the email entry and hide it if an email was provided
var yourEmailIs = '';
- if (this.state.user.email) {
+ if (this.state.email) {
yourEmailIs = (
<FormattedHTMLMessage
id='signup_user_completed.emailIs'
defaultMessage="Your email address is <strong>{email}</strong>. You'll use this address to sign in to {siteName}."
values={{
- email: this.state.user.email,
+ email: this.state.email,
siteName: global.window.mm_config.SiteName
}}
/>
@@ -217,7 +280,7 @@ class SignupUserComplete extends React.Component {
}
var emailContainerStyle = 'margin--extra';
- if (this.state.original_email) {
+ if (this.state.email) {
emailContainerStyle = 'hidden';
}
@@ -234,7 +297,7 @@ class SignupUserComplete extends React.Component {
type='email'
ref='email'
className='form-control'
- defaultValue={this.state.user.email}
+ defaultValue={this.state.email}
placeholder=''
maxLength='128'
autoFocus={true}
@@ -249,20 +312,20 @@ class SignupUserComplete extends React.Component {
var signupMessage = [];
if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
signupMessage.push(
- <a
- className='btn btn-custom-login gitlab'
- key='gitlab'
- href={'/' + this.props.teamName + '/signup/gitlab' + window.location.search}
- >
- <span className='icon'/>
- <span>
- <FormattedMessage
- id='signup_user_completed.gitlab'
- defaultMessage='with GitLab'
- />
- </span>
- </a>
- );
+ <a
+ className='btn btn-custom-login gitlab'
+ key='gitlab'
+ href={'/api/v1/oauth/gitlab/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)}
+ >
+ <span className='icon'/>
+ <span>
+ <FormattedMessage
+ id='signup_user_completed.gitlab'
+ defaultMessage='with GitLab'
+ />
+ </span>
+ </a>
+ );
}
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
@@ -270,7 +333,7 @@ class SignupUserComplete extends React.Component {
<a
className='btn btn-custom-login google'
key='google'
- href={'/' + this.props.teamName + '/signup/google' + window.location.search}
+ href={'/api/v1/oauth/google/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)}
>
<span className='icon'/>
<span>
@@ -318,16 +381,16 @@ class SignupUserComplete extends React.Component {
/>
</strong></h5>
<div className={passwordDivStyle}>
- <input
- type='password'
- ref='password'
- className='form-control'
- placeholder=''
- maxLength='128'
- spellCheck='false'
- />
- {passwordError}
- </div>
+ <input
+ type='password'
+ ref='password'
+ className='form-control'
+ placeholder=''
+ maxLength='128'
+ spellCheck='false'
+ />
+ {passwordError}
+ </div>
</div>
</div>
<p className='margin--extra'>
@@ -373,58 +436,56 @@ class SignupUserComplete extends React.Component {
return (
<div>
- <form>
- <img
- className='signup-team-logo'
- src='/static/images/logo.png'
- />
- <h5 className='margin--less'>
- <FormattedMessage
- id='signup_user_completed.welcome'
- defaultMessage='Welcome to:'
- />
- </h5>
- <h2 className='signup-team__name'>{this.props.teamDisplayName}</h2>
- <h2 className='signup-team__subdomain'>
- <FormattedMessage
- id='signup_user_completed.onSite'
- defaultMessage='on {siteName}'
- values={{
- siteName: global.window.mm_config.SiteName
- }}
- />
- </h2>
- <h4 className='color--light'>
- <FormattedMessage
- id='signup_user_completed.lets'
- defaultMessage="Let's create your account"
- />
- </h4>
- {signupMessage}
- {emailSignup}
- {serverError}
- </form>
+ <div className='signup-header'>
+ <a href='/'>
+ <span classNameNameName='fa fa-chevron-left'/>
+ <FormattedMessage id='web.header.back'/>
+ </a>
+ </div>
+ <div className='col-sm-12'>
+ <div className='signup-team__container padding--less'>
+ <form>
+ <img
+ className='signup-team-logo'
+ src='/static/images/logo.png'
+ />
+ <h5 className='margin--less'>
+ <FormattedMessage
+ id='signup_user_completed.welcome'
+ defaultMessage='Welcome to:'
+ />
+ </h5>
+ <h2 className='signup-team__name'>{this.state.teamName}</h2>
+ <h2 className='signup-team__subdomain'>
+ <FormattedMessage
+ id='signup_user_completed.onSite'
+ defaultMessage='on {siteName}'
+ values={{
+ siteName: global.window.mm_config.SiteName
+ }}
+ />
+ </h2>
+ <h4 className='color--light'>
+ <FormattedMessage
+ id='signup_user_completed.lets'
+ defaultMessage="Let's create your account"
+ />
+ </h4>
+ {signupMessage}
+ {emailSignup}
+ {serverError}
+ </form>
+ </div>
+ </div>
</div>
);
}
}
SignupUserComplete.defaultProps = {
- teamName: '',
- hash: '',
- teamId: '',
- email: '',
- data: null,
- teamDisplayName: ''
};
SignupUserComplete.propTypes = {
- intl: intlShape.isRequired,
- teamName: React.PropTypes.string,
- hash: React.PropTypes.string,
- teamId: React.PropTypes.string,
- email: React.PropTypes.string,
- data: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string
+ location: React.PropTypes.object
};
-export default injectIntl(SignupUserComplete); \ No newline at end of file
+export default SignupUserComplete;
diff --git a/web/react/components/suggestion/at_mention_provider.jsx b/web/react/components/suggestion/at_mention_provider.jsx
index 064b75ac5..c5bd13c26 100644
--- a/web/react/components/suggestion/at_mention_provider.jsx
+++ b/web/react/components/suggestion/at_mention_provider.jsx
@@ -40,7 +40,7 @@ class AtMentionSuggestion extends React.Component {
icon = (
<img
className='mention-img'
- src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at + '&' + Utils.getSessionIndex()}
+ src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at}
/>
);
}
diff --git a/web/react/components/suggestion/suggestion_box.jsx b/web/react/components/suggestion/suggestion_box.jsx
index ea9f835eb..12b098cbd 100644
--- a/web/react/components/suggestion/suggestion_box.jsx
+++ b/web/react/components/suggestion/suggestion_box.jsx
@@ -2,7 +2,7 @@
// See License.txt for license information.
import Constants from '../../utils/constants.jsx';
-import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../../action_creators/global_actions.jsx';
import SuggestionStore from '../../stores/suggestion_store.jsx';
import * as Utils from '../../utils/utils.jsx';
@@ -48,7 +48,7 @@ export default class SuggestionBox extends React.Component {
if (!(container.is(e.target) || container.has(e.target).length > 0)) {
// we can't just use blur for this because it fires and hides the children before
// their click handlers can be called
- EventHelpers.emitClearSuggestions(this.suggestionId);
+ GlobalActions.emitClearSuggestions(this.suggestionId);
}
}
@@ -57,7 +57,7 @@ export default class SuggestionBox extends React.Component {
const caret = Utils.getCaretPosition(textbox);
const pretext = textbox.value.substring(0, caret);
- EventHelpers.emitSuggestionPretextChanged(this.suggestionId, pretext);
+ GlobalActions.emitSuggestionPretextChanged(this.suggestionId, pretext);
if (this.props.onUserInput) {
this.props.onUserInput(textbox.value);
@@ -89,13 +89,13 @@ export default class SuggestionBox extends React.Component {
handleKeyDown(e) {
if (SuggestionStore.hasSuggestions(this.suggestionId)) {
if (e.which === KeyCodes.UP) {
- EventHelpers.emitSelectPreviousSuggestion(this.suggestionId);
+ GlobalActions.emitSelectPreviousSuggestion(this.suggestionId);
e.preventDefault();
} else if (e.which === KeyCodes.DOWN) {
- EventHelpers.emitSelectNextSuggestion(this.suggestionId);
+ GlobalActions.emitSelectNextSuggestion(this.suggestionId);
e.preventDefault();
} else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.TAB) {
- EventHelpers.emitCompleteWordSuggestion(this.suggestionId);
+ GlobalActions.emitCompleteWordSuggestion(this.suggestionId);
e.preventDefault();
} else if (this.props.onKeyDown) {
this.props.onKeyDown(e);
diff --git a/web/react/components/suggestion/suggestion_list.jsx b/web/react/components/suggestion/suggestion_list.jsx
index e3ccd0f08..ccebeb990 100644
--- a/web/react/components/suggestion/suggestion_list.jsx
+++ b/web/react/components/suggestion/suggestion_list.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../../action_creators/global_actions.jsx';
import SuggestionStore from '../../stores/suggestion_store.jsx';
export default class SuggestionList extends React.Component {
@@ -36,7 +36,7 @@ export default class SuggestionList extends React.Component {
}
handleItemClick(term, e) {
- EventHelpers.emitCompleteWordSuggestion(this.props.suggestionId, term);
+ GlobalActions.emitCompleteWordSuggestion(this.props.suggestionId, term);
e.preventDefault();
}
diff --git a/web/react/components/team_members_modal.jsx b/web/react/components/team_members_modal.jsx
index 9bdb16438..786e8f947 100644
--- a/web/react/components/team_members_modal.jsx
+++ b/web/react/components/team_members_modal.jsx
@@ -10,8 +10,36 @@ import {FormattedMessage} from 'mm-intl';
const Modal = ReactBootstrap.Modal;
export default class TeamMembersModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.teamChanged = this.teamChanged.bind(this);
+
+ this.state = {
+ team: TeamStore.getCurrent()
+ };
+ }
+ componentDidMount() {
+ if (this.props.show) {
+ this.onShow();
+ }
+
+ TeamStore.addChangeListener(this.teamChanged);
+ }
+
+ componentWillUnmount() {
+ TeamStore.removeChangeListener(this.teamChanged);
+ }
+
+ teamChanged() {
+ this.setState({team: TeamStore.getCurrent()});
+ }
+
render() {
- const team = TeamStore.getCurrent();
+ let teamDisplayName = '';
+ if (this.state.team) {
+ teamDisplayName = this.state.team.display_name;
+ }
let maxHeight = 1000;
if (Utils.windowHeight() <= 1200) {
@@ -29,7 +57,7 @@ export default class TeamMembersModal extends React.Component {
id='team_member_modal.members'
defaultMessage='{team} Members'
values={{
- team: team.display_name
+ team: teamDisplayName
}}
/>
</Modal.Header>
diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx
index e3207d573..0eb9d1211 100644
--- a/web/react/components/team_settings.jsx
+++ b/web/react/components/team_settings.jsx
@@ -28,6 +28,9 @@ export default class TeamSettings extends React.Component {
}
}
render() {
+ if (!this.state.team) {
+ return null;
+ }
var result;
switch (this.props.activeTab) {
case 'general':
diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx
index 7dd645b25..a81b22d90 100644
--- a/web/react/components/team_signup_with_email.jsx
+++ b/web/react/components/team_signup_with_email.jsx
@@ -5,6 +5,7 @@ import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
+import {browserHistory} from 'react-router';
const holders = defineMessages({
emailError: {
@@ -47,9 +48,9 @@ class EmailSignUpPage extends React.Component {
Client.signupTeam(team.email,
(data) => {
if (data.follow_link) {
- window.location.href = data.follow_link;
+ browserHistory.push(data.follow_link);
} else {
- window.location.href = `/signup_team_confirm/?email=${encodeURIComponent(team.email)}`;
+ browserHistory.push(`/signup_team_confirm/?email=${encodeURIComponent(team.email)}`);
}
},
(err) => {
@@ -117,4 +118,4 @@ EmailSignUpPage.propTypes = {
intl: intlShape.isRequired
};
-export default injectIntl(EmailSignUpPage); \ No newline at end of file
+export default injectIntl(EmailSignUpPage);
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index 46900e436..d4eb60676 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -61,7 +61,7 @@ export default class Textbox extends React.Component {
onRecievedError() {
const errorCount = ErrorStore.getConnectionErrorCount();
- if (errorCount > 0) {
+ if (errorCount > 1) {
this.setState({connection: 'bad-connection'});
} else {
this.setState({connection: ''});
@@ -194,7 +194,6 @@ export default class Textbox extends React.Component {
defaultMessage='>quote'
/>
</span>
- {previewLink}
</div>
);
@@ -230,16 +229,19 @@ export default class Textbox extends React.Component {
>
</div>
{helpText}
- <a
- target='_blank'
- href='http://docs.mattermost.com/help/getting-started/messaging-basics.html'
- className='textbox-help-link'
- >
- <FormattedMessage
- id='textbox.help'
- defaultMessage='Help'
- />
- </a>
+ <div className='help__text'>
+ {previewLink}
+ <a
+ target='_blank'
+ href='http://docs.mattermost.com/help/getting-started/messaging-basics.html'
+ className='textbox-help-link'
+ >
+ <FormattedMessage
+ id='textbox.help'
+ defaultMessage='Help'
+ />
+ </a>
+ </div>
</div>
);
}
diff --git a/web/react/components/user_list_row.jsx b/web/react/components/user_list_row.jsx
index d8442e770..1ca40687f 100644
--- a/web/react/components/user_list_row.jsx
+++ b/web/react/components/user_list_row.jsx
@@ -32,7 +32,7 @@ export default function UserListRow({user, actions}) {
>
<img
className='profile-img'
- src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
/>
<div
className='user-list-item__details'
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index 31b2b9907..e7a286b77 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -26,22 +26,27 @@ export default class UserProfile extends React.Component {
}
}
render() {
- var name = Utils.displayUsername(this.props.user.id);
- if (this.props.overwriteName) {
- name = this.props.overwriteName;
- } else if (!name) {
- name = '...';
+ let name = '...';
+ let email = '';
+ let profileImg = '';
+ if (this.props.user) {
+ name = Utils.displayUsername(this.props.user.id);
+ email = this.props.user.email;
+ profileImg = '/api/v1/users/' + this.props.user.id + '/image?time=' + this.props.user.update_at;
}
- if (this.props.disablePopover) {
- return <div>{name}</div>;
+ if (this.props.overwriteName) {
+ name = this.props.overwriteName;
}
- var profileImg = '/api/v1/users/' + this.props.user.id + '/image?time=' + this.props.user.update_at + '&' + Utils.getSessionIndex();
if (this.props.overwriteImage) {
profileImg = this.props.overwriteImage;
}
+ if (this.props.disablePopover) {
+ return <div>{name}</div>;
+ }
+
var dataContent = [];
dataContent.push(
<img
@@ -69,14 +74,14 @@ export default class UserProfile extends React.Component {
dataContent.push(
<div
data-toggle='tooltip'
- title={this.props.user.email}
+ title={email}
key='user-popover-email'
>
<a
- href={'mailto:' + this.props.user.email}
+ href={'mailto:' + email}
className='text-nowrap text-lowercase user-popover__email'
>
- {this.props.user.email}
+ {email}
</a>
</div>
);
@@ -114,7 +119,7 @@ UserProfile.defaultProps = {
disablePopover: false
};
UserProfile.propTypes = {
- user: React.PropTypes.object.isRequired,
+ user: React.PropTypes.object,
overwriteName: React.PropTypes.string,
overwriteImage: React.PropTypes.string,
disablePopover: React.PropTypes.bool
diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx
index 1e724bb6e..4ee9fd0e2 100644
--- a/web/react/components/user_settings/custom_theme_chooser.jsx
+++ b/web/react/components/user_settings/custom_theme_chooser.jsx
@@ -253,6 +253,8 @@ class CustomThemeChooser extends React.Component {
</div>
</div>
);
+
+ colors += theme[element.id] + ',';
} else if (element.group === 'sidebarElements') {
sidebarElements.push(
<div
diff --git a/web/react/components/user_settings/manage_languages.jsx b/web/react/components/user_settings/manage_languages.jsx
index 2d1c74717..6b00a65c7 100644
--- a/web/react/components/user_settings/manage_languages.jsx
+++ b/web/react/components/user_settings/manage_languages.jsx
@@ -5,6 +5,7 @@ import SettingItemMax from '../setting_item_max.jsx';
import * as Client from '../../utils/client.jsx';
import * as Utils from '../../utils/utils.jsx';
+import * as GlobalActions from '../../action_creators/global_actions.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -41,7 +42,7 @@ export default class ManageLanguage extends React.Component {
submitUser(user) {
Client.updateUser(user,
() => {
- window.location.reload(true);
+ GlobalActions.newLocalizationSelected(user.locale);
},
(err) => {
let serverError;
diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx
index 0acfd4a16..1dd564c8d 100644
--- a/web/react/components/user_settings/user_settings_developer.jsx
+++ b/web/react/components/user_settings/user_settings_developer.jsx
@@ -3,7 +3,7 @@
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
-import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../../action_creators/global_actions.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
@@ -28,7 +28,7 @@ class DeveloperTab extends React.Component {
}
register() {
this.props.closeModal();
- EventHelpers.showRegisterAppModal();
+ GlobalActions.showRegisterAppModal();
}
render() {
var appSection;
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index b0b1c414e..235892819 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -13,7 +13,7 @@ import Constants from '../../utils/constants.jsx';
import * as AsyncClient from '../../utils/async_client.jsx';
import * as Utils from '../../utils/utils.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
+import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl';
const holders = defineMessages({
usernameReserved: {
@@ -712,7 +712,7 @@ class UserSettingsGeneralTab extends React.Component {
<SettingPicture
title={formatMessage(holders.profilePicture)}
submit={this.submitPicture}
- src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update + '&' + Utils.getSessionIndex()}
+ src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update}
server_error={serverError}
client_error={clientError}
updateSection={(e) => {
@@ -729,7 +729,14 @@ class UserSettingsGeneralTab extends React.Component {
let minMessage = formatMessage(holders.uploadImage);
if (user.last_picture_update) {
minMessage = formatMessage(holders.imageUpdated, {
- date: new Date(user.last_picture_update).toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'})
+ date: (
+ <FormattedDate
+ value={new Date(user.last_picture_update)}
+ day='2-digit'
+ month='short'
+ year='numeric'
+ />
+ )
});
}
pictureSection = (
@@ -805,4 +812,4 @@ UserSettingsGeneralTab.propTypes = {
collapseModal: React.PropTypes.func.isRequired
};
-export default injectIntl(UserSettingsGeneralTab); \ No newline at end of file
+export default injectIntl(UserSettingsGeneralTab);
diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index fa3415988..0c4a3d526 100644
--- a/web/react/components/user_settings/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -73,27 +73,35 @@ class UserSettingsModal extends React.Component {
this.updateTab = this.updateTab.bind(this);
this.updateSection = this.updateSection.bind(this);
+ this.onUserChanged = this.onUserChanged.bind(this);
this.state = {
active_tab: 'general',
active_section: '',
showConfirmModal: false,
- enforceFocus: true
+ enforceFocus: true,
+ currentUser: UserStore.getCurrentUser()
};
this.requireConfirm = false;
}
+ onUserChanged() {
+ this.setState({currentUser: UserStore.getCurrentUser()});
+ }
+
componentDidMount() {
if (this.props.show) {
this.handleShow();
}
+ UserStore.addChangeListener(this.onUserChanged);
}
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
this.handleShow();
}
+ UserStore.removeChangeListener(this.onUserChanged);
}
handleShow() {
@@ -235,8 +243,10 @@ class UserSettingsModal extends React.Component {
render() {
const {formatMessage} = this.props.intl;
- var currentUser = UserStore.getCurrentUser();
- var isAdmin = Utils.isAdmin(currentUser.roles);
+ if (this.state.currentUser == null) {
+ return (<div/>);
+ }
+ var isAdmin = Utils.isAdmin(this.state.currentUser.roles);
var tabs = [];
tabs.push({name: 'general', uiName: formatMessage(holders.general), icon: 'glyphicon glyphicon-cog'});
diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx
index cba7ffdea..0b6b6c398 100644
--- a/web/react/components/user_settings/user_settings_security.jsx
+++ b/web/react/components/user_settings/user_settings_security.jsx
@@ -14,7 +14,7 @@ import * as AsyncClient from '../../utils/async_client.jsx';
import * as Utils from '../../utils/utils.jsx';
import Constants from '../../utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
+import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl';
const holders = defineMessages({
currentPasswordError: {
@@ -218,11 +218,24 @@ class SecurityTab extends React.Component {
var describe;
var d = new Date(this.props.user.last_password_update);
- const locale = global.window.mm_locale;
const hours12 = !Utils.isMilitaryTime();
describe = formatMessage(holders.lastUpdated, {
- date: d.toLocaleDateString(locale, {month: 'short', day: '2-digit', year: 'numeric'}),
- time: d.toLocaleTimeString(locale, {hour12: hours12, hour: '2-digit', minute: '2-digit'})
+ date: (
+ <FormattedDate
+ value={d}
+ day='2-digit'
+ month='short'
+ year='numeric'
+ />
+ ),
+ time: (
+ <FormattedTime
+ value={d}
+ hour12={hours12}
+ hour='2-digit'
+ minute='2-digit'
+ />
+ )
});
updateSectionStatus = function updateSection() {
@@ -251,7 +264,7 @@ class SecurityTab extends React.Component {
<div>
<a
className='btn btn-primary'
- href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email)}
+ href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service}
>
<FormattedMessage
id='user.settings.security.switchEmail'
@@ -269,7 +282,7 @@ class SecurityTab extends React.Component {
<div>
<a
className='btn btn-primary'
- href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&new_type=' + Constants.GITLAB_SERVICE}
+ href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service + '&new_type=' + Constants.GITLAB_SERVICE}
>
<FormattedMessage
id='user.settings.security.switchGitlab'
@@ -287,7 +300,7 @@ class SecurityTab extends React.Component {
<div>
<a
className='btn btn-primary'
- href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&new_type=' + Constants.GOOGLE_SERVICE}
+ href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service + '&new_type=' + Constants.GOOGLE_SERVICE}
>
<FormattedMessage
id='user.settings.security.switchGoogle'
@@ -456,4 +469,4 @@ SecurityTab.propTypes = {
setEnforceFocus: React.PropTypes.func.isRequired
};
-export default injectIntl(SecurityTab); \ No newline at end of file
+export default injectIntl(SecurityTab);
diff --git a/web/react/components/view_image_popover_bar.jsx b/web/react/components/view_image_popover_bar.jsx
index 819df76d8..18be5a3c5 100644
--- a/web/react/components/view_image_popover_bar.jsx
+++ b/web/react/components/view_image_popover_bar.jsx
@@ -51,6 +51,7 @@ export default class ViewImagePopoverBar extends React.Component {
href={this.props.fileURL}
download={this.props.filename}
className='text'
+ target='_blank'
>
<FormattedMessage
id='view_image_popover.download'
diff --git a/web/react/package.json b/web/react/package.json
index 07ffa0cdf..509c9967b 100644
--- a/web/react/package.json
+++ b/web/react/package.json
@@ -11,6 +11,8 @@
"marked": "mattermost/marked#cb85e5cc81bc7937dbb73c3c53d9532b1b97e3ca",
"mm-intl": "mattermost/mm-intl#805442fd474fa40cd586ddeda404dbbe8e60626d",
"object-assign": "4.0.1",
+ "react": "0.14.3",
+ "react-router": "2.0.0",
"twemoji": "1.4.1"
},
"devDependencies": {
diff --git a/web/react/pages/admin_console.jsx b/web/react/pages/admin_console.jsx
deleted file mode 100644
index 989936d9e..000000000
--- a/web/react/pages/admin_console.jsx
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import ErrorBar from '../components/error_bar.jsx';
-import SelectTeamModal from '../components/admin_console/select_team_modal.jsx';
-import AdminController from '../components/admin_console/admin_controller.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <div>
- <ErrorBar/>
- <AdminController
- tab={this.props.map.ActiveTab}
- teamId={this.props.map.TeamId}
- />
- <SelectTeamModal/>
- </div>
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_admin_console_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('admin_controller')
- );
-};
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
deleted file mode 100644
index bc78c049c..000000000
--- a/web/react/pages/channel.jsx
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import ChannelView from '../components/channel_view.jsx';
-import ChannelLoader from '../components/channel_loader.jsx';
-import ErrorBar from '../components/error_bar.jsx';
-import * as Client from '../utils/client.jsx';
-
-import GetPostLinkModal from '../components/get_post_link_modal.jsx';
-import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx';
-import EditPostModal from '../components/edit_post_modal.jsx';
-import DeletePostModal from '../components/delete_post_modal.jsx';
-import MoreChannelsModal from '../components/more_channels.jsx';
-import TeamSettingsModal from '../components/team_settings_modal.jsx';
-import RemovedFromChannelModal from '../components/removed_from_channel_modal.jsx';
-import RegisterAppModal from '../components/register_app_modal.jsx';
-import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx';
-import InviteMemberModal from '../components/invite_member_modal.jsx';
-
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <div className='channel-view'>
- <ChannelLoader/>
- <ErrorBar/>
- <ChannelView/>
- <GetPostLinkModal/>
- <GetTeamInviteLinkModal/>
- <InviteMemberModal/>
- <ImportThemeModal/>
- <TeamSettingsModal/>
- <MoreChannelsModal/>
- <EditPostModal/>
- <DeletePostModal/>
- <RemovedFromChannelModal/>
- <RegisterAppModal/>
- </div>
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_channel_page = function setup(props, team, channel) {
- if (props.PostId === '') {
- EventHelpers.emitChannelClickEvent(channel);
- } else {
- EventHelpers.emitPostFocusEvent(props.PostId);
- }
-
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('channel_view')
- );
-};
diff --git a/web/react/pages/claim_account.jsx b/web/react/pages/claim_account.jsx
deleted file mode 100644
index abbf72ea3..000000000
--- a/web/react/pages/claim_account.jsx
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import ClaimAccount from '../components/claim/claim_account.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <ClaimAccount
- email={this.props.map.Email}
- currentType={this.props.map.CurrentType}
- newType={this.props.map.NewType}
- teamName={this.props.map.TeamName}
- teamDisplayName={this.props.map.TeamDisplayName}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_claim_account_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('claim')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/docs.jsx b/web/react/pages/docs.jsx
deleted file mode 100644
index 2e47e3e6a..000000000
--- a/web/react/pages/docs.jsx
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import Docs from '../components/docs.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <Docs site={this.props.map.Site}/>
- </IntlProvider>
- );
- }
-}
-
-global.window.mm_user = global.window.mm_user || {};
-
-global.window.setup_documentation_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('docs')
- );
-};
diff --git a/web/react/pages/find_team.jsx b/web/react/pages/find_team.jsx
deleted file mode 100644
index 93394fcde..000000000
--- a/web/react/pages/find_team.jsx
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import FindTeam from '../components/find_team.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <FindTeam/>
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_find_team_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('find-team')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx
deleted file mode 100644
index ff81c4994..000000000
--- a/web/react/pages/home.jsx
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import TeamStore from '../stores/team_store.jsx';
-import Constants from '../utils/constants.jsx';
-
-function setupHomePage() {
- var last = null;
- if (last == null || last.length === 0) {
- window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL;
- } else {
- window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + last;
- }
-}
-
-global.window.setup_home_page = setupHomePage;
diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx
deleted file mode 100644
index ec9080945..000000000
--- a/web/react/pages/login.jsx
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import * as Client from '../utils/client.jsx';
-import Login from '../components/login.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <Login
- teamDisplayName={this.props.map.TeamDisplayName}
- teamName={this.props.map.TeamName}
- inviteId={this.props.map.InviteId}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_login_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('login')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/password_reset.jsx b/web/react/pages/password_reset.jsx
deleted file mode 100644
index 7caff5034..000000000
--- a/web/react/pages/password_reset.jsx
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import PasswordReset from '../components/password_reset.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <PasswordReset
- isReset={this.props.map.IsReset}
- teamDisplayName={this.props.map.TeamDisplayName}
- teamName={this.props.map.TeamName}
- hash={this.props.map.Hash}
- data={this.props.map.Data}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_password_reset_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('reset')
- );
-};
diff --git a/web/react/pages/root.jsx b/web/react/pages/root.jsx
new file mode 100644
index 000000000..d0b06e32e
--- /dev/null
+++ b/web/react/pages/root.jsx
@@ -0,0 +1,290 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router';
+import Root from '../components/root.jsx';
+import Login from '../components/login.jsx';
+import LoggedIn from '../components/logged_in.jsx';
+import NotLoggedIn from '../components/not_logged_in.jsx';
+import NeedsTeam from '../components/needs_team.jsx';
+import PasswordResetSendLink from '../components/password_reset_send_link.jsx';
+import PasswordResetForm from '../components/password_reset_form.jsx';
+import ChannelView from '../components/channel_view.jsx';
+import Sidebar from '../components/sidebar.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import ErrorStore from '../stores/error_store.jsx';
+import BrowserStore from '../stores/browser_store.jsx';
+import SignupTeam from '../components/signup_team.jsx';
+import * as Client from '../utils/client.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
+import SignupTeamConfirm from '../components/signup_team_confirm.jsx';
+import SignupUserComplete from '../components/signup_user_complete.jsx';
+import ShouldVerifyEmail from '../components/should_verify_email.jsx';
+import DoVerifyEmail from '../components/do_verify_email.jsx';
+import AdminConsole from '../components/admin_console/admin_controller.jsx';
+import ClaimAccount from '../components/claim/claim_account.jsx';
+
+import SignupTeamComplete from '../components/signup_team_complete/components/signup_team_complete.jsx';
+import WelcomePage from '../components/signup_team_complete/components/team_signup_welcome_page.jsx';
+import TeamDisplayNamePage from '../components/signup_team_complete/components/team_signup_display_name_page.jsx';
+import TeamURLPage from '../components/signup_team_complete/components/team_signup_url_page.jsx';
+import SendInivtesPage from '../components/signup_team_complete/components/team_signup_send_invites_page.jsx';
+import UsernamePage from '../components/signup_team_complete/components/team_signup_username_page.jsx';
+import PasswordPage from '../components/signup_team_complete/components/team_signup_password_page.jsx';
+import FinishedPage from '../components/signup_team_complete/components/team_signup_finished.jsx';
+
+// This is for anything that needs to be done for ALL react components.
+// This runs before we start to render anything.
+function preRenderSetup(callwhendone) {
+ const d1 = Client.getClientConfig(
+ (data, textStatus, xhr) => {
+ if (!data) {
+ return;
+ }
+
+ global.window.mm_config = data;
+
+ var serverVersion = xhr.getResponseHeader('X-Version-ID');
+
+ if (serverVersion !== BrowserStore.getLastServerVersion()) {
+ if (!BrowserStore.getLastServerVersion() || BrowserStore.getLastServerVersion() === '') {
+ BrowserStore.setLastServerVersion(serverVersion);
+ } else {
+ BrowserStore.setLastServerVersion(serverVersion);
+ window.location.reload(true);
+ console.log('Detected version update refreshing the page'); //eslint-disable-line no-console
+ }
+ }
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getClientConfig');
+ }
+ );
+
+ const d2 = Client.getClientLicenceConfig(
+ (data) => {
+ if (!data) {
+ return;
+ }
+
+ global.window.mm_license = data;
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getClientLicenceConfig');
+ }
+ );
+
+ // Set these here so they don't fail in client.jsx track
+ global.window.analytics = {};
+ global.window.analytics.page = () => {
+ // Do Nothing
+ };
+ global.window.analytics.track = () => {
+ // Do Nothing
+ };
+
+ $.when(d1, d2).done(callwhendone);
+}
+
+function preLoggedIn(nextState, replace, callback) {
+ const d1 = Client.getAllPreferences(
+ (data) => {
+ if (!data) {
+ return;
+ }
+
+ PreferenceStore.setPreferences(data);
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getAllPreferences');
+ }
+ );
+
+ const d2 = AsyncClient.getChannels();
+
+ $.when(d1, d2).done(() => callback());
+}
+
+function onChannelChange(nextState) {
+ const channelName = nextState.params.channel;
+
+ // Make sure we have all the channels
+ AsyncClient.getChannels(true);
+
+ // Get our channel's ID
+ const channel = ChannelStore.getByName(channelName);
+
+ // User clicked channel
+ GlobalActions.emitChannelClickEvent(channel);
+}
+
+function onRootEnter(nextState, replace, callback) {
+ if (nextState.location.pathname === '/') {
+ Client.getMeLoggedIn((data) => {
+ if (!data || data.logged_in === 'false') {
+ replace({pathname: '/signup_team'});
+ callback();
+ } else {
+ replace({pathname: '/' + data.team_name + '/channels/town-square'});
+ callback();
+ }
+ });
+ return;
+ }
+
+ callback();
+}
+
+function onPermalinkEnter(nextState) {
+ const postId = nextState.params.postid;
+
+ GlobalActions.emitPostFocusEvent(postId);
+}
+
+function onLoggedOut(nextState) {
+ const teamName = nextState.params.team;
+ Client.logout(
+ () => {
+ browserHistory.push('/' + teamName + '/login');
+ BrowserStore.signalLogout();
+ BrowserStore.clear();
+ ErrorStore.clearLastError();
+ },
+ () => {
+ browserHistory.push('/' + teamName + '/login');
+ }
+ );
+}
+
+function renderRootComponent() {
+ ReactDOM.render((
+ <Router
+ history={browserHistory}
+ >
+ <Route
+ path='/'
+ component={Root}
+ onEnter={onRootEnter}
+ >
+ <Route
+ component={LoggedIn}
+ onEnter={preLoggedIn}
+ >
+ <Route
+ path=':team/channels/:channel'
+ onEnter={onChannelChange}
+ components={{
+ sidebar: Sidebar,
+ center: ChannelView
+ }}
+ />
+ <Route
+ path=':team/pl/:postid'
+ onEnter={onPermalinkEnter}
+ components={{
+ sidebar: Sidebar,
+ center: ChannelView
+ }}
+ />
+ <Route
+ path=':team/logout'
+ onEnter={onLoggedOut}
+ components={{
+ sidebar: null,
+ center: null
+ }}
+ />
+ <Route
+ path='admin_console'
+ components={{
+ sidebar: null,
+ center: AdminConsole
+ }}
+ />
+ </Route>
+ <Route component={NotLoggedIn}>
+ <Route
+ path='signup_team'
+ component={SignupTeam}
+ />
+ <Route
+ path='signup_team_complete'
+ component={SignupTeamComplete}
+ >
+ <IndexRoute component={FinishedPage}/>
+ <Route
+ path='welcome'
+ component={WelcomePage}
+ />
+ <Route
+ path='team_display_name'
+ component={TeamDisplayNamePage}
+ />
+ <Route
+ path='team_url'
+ component={TeamURLPage}
+ />
+ <Route
+ path='invites'
+ component={SendInivtesPage}
+ />
+ <Route
+ path='username'
+ component={UsernamePage}
+ />
+ <Route
+ path='password'
+ component={PasswordPage}
+ />
+ </Route>
+ <Route
+ path='signup_user_complete'
+ component={SignupUserComplete}
+ />
+ <Route
+ path='signup_team_confirm'
+ component={SignupTeamConfirm}
+ />
+ <Route
+ path='should_verify_email'
+ component={ShouldVerifyEmail}
+ />
+ <Route
+ path='do_verify_email'
+ component={DoVerifyEmail}
+ />
+ <Route
+ path=':team'
+ component={NeedsTeam}
+ >
+ <IndexRedirect to='login'/>
+ <Route
+ path='login'
+ component={Login}
+ />
+ <Route
+ path='claim'
+ component={ClaimAccount}
+ />
+ <Route
+ path='reset_password'
+ component={PasswordResetSendLink}
+ />
+ <Route
+ path='reset_password_complete'
+ component={PasswordResetForm}
+ />
+ </Route>
+ </Route>
+ </Route>
+ </Router>
+ ),
+ document.getElementById('root'));
+}
+
+global.window.setup_root = () => {
+ // Do the pre-render setup and call renderRootComponent when done
+ preRenderSetup(renderRootComponent);
+};
diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx
deleted file mode 100644
index f276c3ff7..000000000
--- a/web/react/pages/signup_team.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import SignupTeam from '../components/signup_team.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired,
- teams: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <SignupTeam teams={this.props.teams}/>
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_signup_team_page = function setup(props) {
- var teams = [];
-
- for (var prop in props) {
- if (props.hasOwnProperty(prop)) {
- if (prop !== 'Title' && prop !== 'Locale' && prop !== 'Info') {
- teams.push({name: prop, display_name: props[prop]});
- }
- }
- }
-
- ReactDOM.render(
- <Root
- map={props}
- teams={teams}
- />,
- document.getElementById('signup-team')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/signup_team_complete.jsx b/web/react/pages/signup_team_complete.jsx
deleted file mode 100644
index 8c237f698..000000000
--- a/web/react/pages/signup_team_complete.jsx
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import SignupTeamComplete from '../components/signup_team_complete.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <SignupTeamComplete
- email={this.props.map.Email}
- hash={this.props.map.Hash}
- data={this.props.map.Data}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_signup_team_complete_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('signup-team-complete')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/signup_team_confirm.jsx b/web/react/pages/signup_team_confirm.jsx
deleted file mode 100644
index 13c8f3fd0..000000000
--- a/web/react/pages/signup_team_confirm.jsx
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import SignupTeamConfirm from '../components/signup_team_confirm.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <SignupTeamConfirm
- email={this.props.map.Email}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_signup_team_confirm_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('signup-team-confirm')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx
deleted file mode 100644
index a14f2140b..000000000
--- a/web/react/pages/signup_user_complete.jsx
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import SignupUserComplete from '../components/signup_user_complete.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <SignupUserComplete
- teamId={this.props.map.TeamId}
- teamName={this.props.map.TeamName}
- teamDisplayName={this.props.map.TeamDisplayName}
- email={this.props.map.Email}
- hash={this.props.map.Hash}
- data={this.props.map.Data}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setup_signup_user_complete_page = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('signup-user-complete')
- );
-}; \ No newline at end of file
diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx
deleted file mode 100644
index 6b336daa1..000000000
--- a/web/react/pages/verify.jsx
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import EmailVerify from '../components/email_verify.jsx';
-import * as Client from '../utils/client.jsx';
-
-var IntlProvider = ReactIntl.IntlProvider;
-
-class Root extends React.Component {
- constructor() {
- super();
- this.state = {
- translations: null,
- loaded: false
- };
- }
-
- static propTypes() {
- return {
- map: React.PropTypes.object.isRequired
- };
- }
-
- componentWillMount() {
- Client.getTranslations(
- this.props.map.Locale,
- (data) => {
- this.setState({
- translations: data,
- loaded: true
- });
- },
- () => {
- this.setState({
- loaded: true
- });
- }
- );
- }
-
- render() {
- if (!this.state.loaded) {
- return <div></div>;
- }
-
- return (
- <IntlProvider
- locale={this.props.map.Locale}
- messages={this.state.translations}
- >
- <EmailVerify
- isVerified={this.props.map.IsVerified}
- teamURL={this.props.map.TeamURL}
- userEmail={this.props.map.UserEmail}
- resendSuccess={this.props.map.ResendSuccess}
- />
- </IntlProvider>
- );
- }
-}
-
-global.window.setupVerifyPage = function setup(props) {
- ReactDOM.render(
- <Root map={props}/>,
- document.getElementById('verify')
- );
-};
diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx
index eb3254cfe..9f7f6e7ff 100644
--- a/web/react/stores/admin_store.jsx
+++ b/web/react/stores/admin_store.jsx
@@ -121,7 +121,11 @@ class AdminStoreClass extends EventEmitter {
}
getSelectedTeams() {
- return BrowserStore.getItem('seleted_teams');
+ const result = BrowserStore.getItem('seleted_teams');
+ if (!result) {
+ return {};
+ }
+ return result;
}
saveSelectedTeams(teams) {
diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx
index 3417faaaf..3b35916b3 100644
--- a/web/react/stores/browser_store.jsx
+++ b/web/react/stores/browser_store.jsx
@@ -4,8 +4,8 @@
import {generateId} from '../utils/utils.jsx';
function getPrefix() {
- if (global.window.mm_user) {
- return global.window.mm_user.id + '_';
+ if (global.window.mm_current_user_id) {
+ return global.window.mm_current_user_id + '_';
}
return 'unknown_';
@@ -31,7 +31,9 @@ class BrowserStoreClass {
this.isSignallingLogout = this.isSignallingLogout.bind(this);
this.signalLogin = this.signalLogin.bind(this);
this.isSignallingLogin = this.isSignallingLogin.bind(this);
+ }
+ checkVersion() {
var currentVersion = sessionStorage.getItem('storage_version');
if (currentVersion !== global.window.mm_config.Version) {
sessionStorage.clear();
diff --git a/web/react/stores/localization_store.jsx b/web/react/stores/localization_store.jsx
new file mode 100644
index 000000000..0e3a63724
--- /dev/null
+++ b/web/react/stores/localization_store.jsx
@@ -0,0 +1,60 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import EventEmitter from 'events';
+import Constants from '../utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
+
+const CHANGE_EVENT = 'change';
+
+class LocalizationStoreClass extends EventEmitter {
+ constructor() {
+ super();
+
+ this.currentLocale = 'en';
+ this.currentTranslations = null;
+ }
+
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ setCurrentLocale(locale, translations) {
+ this.currentLocale = locale;
+ this.currentTranslations = translations;
+ }
+
+ getLocale() {
+ return this.currentLocale;
+ }
+
+ getTranslations() {
+ return this.currentTranslations;
+ }
+}
+
+var LocalizationStore = new LocalizationStoreClass();
+LocalizationStore.setMaxListeners(0);
+
+LocalizationStore.dispatchToken = AppDispatcher.register((payload) => {
+ var action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECEIVED_LOCALE:
+ LocalizationStore.setCurrentLocale(action.locale, action.translations);
+ LocalizationStore.emitChange();
+ break;
+ default:
+ }
+});
+
+export default LocalizationStore;
diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx
index 9b2b049b7..181de53d7 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -10,7 +10,7 @@ import EventEmitter from 'events';
import * as Utils from '../utils/utils.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import Constants from '../utils/constants.jsx';
const SocketEvents = Constants.SocketEvents;
@@ -31,6 +31,7 @@ class SocketStoreClass extends EventEmitter {
this.close = this.close.bind(this);
this.failCount = 0;
+ this.isInitialize = false;
this.translations = this.getDefaultTranslations();
@@ -42,10 +43,6 @@ class SocketStoreClass extends EventEmitter {
return;
}
- if (!global.window.hasOwnProperty('mm_session_token_index')) {
- return;
- }
-
this.setMaxListeners(0);
if (window.WebSocket && !conn) {
@@ -54,28 +51,27 @@ class SocketStoreClass extends EventEmitter {
protocol = 'wss://';
}
- var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket?' + Utils.getSessionIndex();
+ var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket';
if (this.failCount === 0) {
console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console
- if (ErrorStore.getConnectionErrorCount() > 0) {
- ErrorStore.setConnectionErrorCount(0);
- ErrorStore.emitChange();
- }
}
+
conn = new WebSocket(connUrl);
conn.onopen = () => {
if (this.failCount > 0) {
console.log('websocket re-established connection'); //eslint-disable-line no-console
+ AsyncClient.getChannels();
+ AsyncClient.getPosts(ChannelStore.getCurrentId());
+ }
+ if (this.isInitialize) {
ErrorStore.clearLastError();
ErrorStore.emitChange();
-
- AsyncClient.getChannels();
- AsyncClient.getPosts(ChannelStore.getCurrentId());
}
+ this.isInitialize = true;
this.failCount = 0;
};
@@ -204,7 +200,7 @@ class SocketStoreClass extends EventEmitter {
function handleNewPostEvent(msg, translations) {
// Store post
const post = JSON.parse(msg.props.post);
- EventHelpers.emitPostRecievedEvent(post);
+ GlobalActions.emitPostRecievedEvent(post);
// Update channel state
if (ChannelStore.getCurrentId() === msg.channel_id) {
@@ -291,7 +287,7 @@ function handlePostEditEvent(msg) {
function handlePostDeleteEvent(msg) {
const post = JSON.parse(msg.props.post);
- EventHelpers.emitPostDeletedEvent(post);
+ GlobalActions.emitPostDeletedEvent(post);
}
function handleNewUserEvent() {
@@ -337,7 +333,7 @@ function handleChannelViewedEvent(msg) {
function handlePreferenceChangedEvent(msg) {
const preference = JSON.parse(msg.props.preference);
- EventHelpers.emitPreferenceChangedEvent(preference);
+ GlobalActions.emitPreferenceChangedEvent(preference);
}
var SocketStore = new SocketStoreClass();
diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx
index 7a1a2ef42..354a07b72 100644
--- a/web/react/stores/team_store.jsx
+++ b/web/react/stores/team_store.jsx
@@ -6,7 +6,6 @@ import EventEmitter from 'events';
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
-import BrowserStore from '../stores/browser_store.jsx';
const CHANGE_EVENT = 'change';
@@ -33,6 +32,9 @@ class TeamStoreClass extends EventEmitter {
this.getCurrentTeamUrl = this.getCurrentTeamUrl.bind(this);
this.getCurrentInviteLink = this.getCurrentInviteLink.bind(this);
this.saveTeam = this.saveTeam.bind(this);
+
+ this.teams = {};
+ this.currentTeamId = '';
}
emitChange() {
@@ -65,11 +67,11 @@ class TeamStoreClass extends EventEmitter {
}
getAll() {
- return BrowserStore.getItem('user_teams', {});
+ return this.teams;
}
getCurrentId() {
- var team = global.window.mm_team;
+ var team = this.get(this.currentTeamId);
if (team) {
return team.id;
@@ -79,11 +81,13 @@ class TeamStoreClass extends EventEmitter {
}
getCurrent() {
- if (global.window.mm_team != null && this.get(global.window.mm_team.id) == null) {
- this.saveTeam(global.window.mm_team);
+ const team = this.teams[this.currentTeamId];
+
+ if (team) {
+ return team;
}
- return global.window.mm_team;
+ return null;
}
getCurrentTeamUrl() {
@@ -104,9 +108,16 @@ class TeamStoreClass extends EventEmitter {
}
saveTeam(team) {
- var teams = this.getAll();
- teams[team.id] = team;
- BrowserStore.setItem('user_teams', teams);
+ this.teams[team.id] = team;
+ }
+
+ saveTeams(teams) {
+ this.teams = teams;
+ }
+
+ saveMyTeam(team) {
+ this.saveTeam(team);
+ this.currentTeamId = team.id;
}
}
@@ -116,11 +127,14 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
- case ActionTypes.RECEIVED_TEAM:
- TeamStore.saveTeam(action.team);
+ case ActionTypes.RECEIVED_MY_TEAM:
+ TeamStore.saveMyTeam(action.team);
+ TeamStore.emitChange();
+ break;
+ case ActionTypes.RECEIVED_ALL_TEAMS:
+ TeamStore.saveTeams(action.teams);
TeamStore.emitChange();
break;
-
default:
}
});
diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx
index 75a87d424..c1e5c75dc 100644
--- a/web/react/stores/user_store.jsx
+++ b/web/react/stores/user_store.jsx
@@ -11,13 +11,13 @@ import BrowserStore from './browser_store.jsx';
const CHANGE_EVENT = 'change';
const CHANGE_EVENT_SESSIONS = 'change_sessions';
const CHANGE_EVENT_AUDITS = 'change_audits';
-const CHANGE_EVENT_TEAMS = 'change_teams';
const CHANGE_EVENT_STATUSES = 'change_statuses';
class UserStoreClass extends EventEmitter {
constructor() {
super();
this.profileCache = null;
+ this.currentUserId = '';
}
emitChange(userId) {
@@ -56,18 +56,6 @@ class UserStoreClass extends EventEmitter {
this.removeListener(CHANGE_EVENT_AUDITS, callback);
}
- emitTeamsChange() {
- this.emit(CHANGE_EVENT_TEAMS);
- }
-
- addTeamsChangeListener(callback) {
- this.on(CHANGE_EVENT_TEAMS, callback);
- }
-
- removeTeamsChangeListener(callback) {
- this.removeListener(CHANGE_EVENT_TEAMS, callback);
- }
-
emitStatusesChange() {
this.emit(CHANGE_EVENT_STATUSES);
}
@@ -81,26 +69,17 @@ class UserStoreClass extends EventEmitter {
}
getCurrentUser() {
- if (this.getProfiles()[global.window.mm_user.id] == null) {
- this.saveProfile(global.window.mm_user);
- }
-
- return global.window.mm_user;
+ return this.getProfiles()[this.currentUserId];
}
setCurrentUser(user) {
- var oldUser = global.window.mm_user;
-
- if (oldUser.id === user.id) {
- global.window.mm_user = user;
- this.saveProfile(user);
- } else {
- throw new Error('Problem with setCurrentUser old_user_id=' + oldUser.id + ' new_user_id=' + user.id);
- }
+ this.saveProfile(user);
+ this.currentUserId = user.id;
+ global.window.mm_current_user_id = this.currentUserId;
}
getCurrentId() {
- var user = global.window.mm_user;
+ var user = this.getCurrentUser();
if (user) {
return user.id;
@@ -200,11 +179,22 @@ class UserStoreClass extends EventEmitter {
saveProfiles(profiles) {
const currentId = this.getCurrentId();
- if (currentId in profiles) {
- delete profiles[currentId];
+ if (this.profileCache) {
+ const currentUser = this.profileCache[currentId];
+ if (currentUser) {
+ if (currentId in profiles) {
+ delete profiles[currentId];
+ }
+
+ this.profileCache = profiles;
+ this.profileCache[currentId] = currentUser;
+ } else {
+ this.profileCache = profiles;
+ }
+ } else {
+ this.profileCache = profiles;
}
- this.profileCache = profiles;
BrowserStore.setItem('profiles', profiles);
}
@@ -224,14 +214,6 @@ class UserStoreClass extends EventEmitter {
return BrowserStore.getItem('audits', {loading: true});
}
- setTeams(teams) {
- BrowserStore.setItem('teams', teams);
- }
-
- getTeams() {
- return BrowserStore.getItem('teams', []);
- }
-
getCurrentMentionKeys() {
return this.getMentionKeys(this.getCurrentId());
}
@@ -312,10 +294,6 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => {
UserStore.setAudits(action.audits);
UserStore.emitAuditsChange();
break;
- case ActionTypes.RECEIVED_TEAMS:
- UserStore.setTeams(action.teams);
- UserStore.emitTeamsChange();
- break;
case ActionTypes.RECEIVED_STATUSES:
UserStore.pSetStatuses(action.statuses);
UserStore.emitStatusesChange();
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 7d5e1bd0f..b9770a6e9 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import * as client from './client.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import BrowserStore from '../stores/browser_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
@@ -44,15 +45,19 @@ function isCallInProgress(callName) {
export function getChannels(checkVersion) {
if (isCallInProgress('getChannels')) {
- return;
+ return null;
}
callTracker.getChannels = utils.getTimestamp();
- client.getChannels(
+ return client.getChannels(
(data, textStatus, xhr) => {
callTracker.getChannels = 0;
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
if (checkVersion) {
var serverVersion = xhr.getResponseHeader('X-Version-ID');
@@ -67,10 +72,6 @@ export function getChannels(checkVersion) {
}
}
- if (xhr.status === 304 || !data) {
- return;
- }
-
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_CHANNELS,
channels: data.channels,
@@ -392,36 +393,6 @@ export function getAllTeams() {
);
}
-export function findTeams(email) {
- if (isCallInProgress('findTeams_' + email)) {
- return;
- }
-
- var user = UserStore.getCurrentUser();
- if (user) {
- callTracker['findTeams_' + email] = utils.getTimestamp();
- client.findTeams(
- user.email,
- function findTeamsSuccess(data, textStatus, xhr) {
- callTracker['findTeams_' + email] = 0;
-
- if (xhr.status === 304 || !data) {
- return;
- }
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_TEAMS,
- teams: data
- });
- },
- function findTeamsFailure(err) {
- callTracker['findTeams_' + email] = 0;
- dispatchError(err, 'findTeams');
- }
- );
- }
-}
-
export function search(terms) {
if (isCallInProgress('search_' + String(terms))) {
return;
@@ -645,11 +616,11 @@ export function getPostsAfter(postId, offset, numPost) {
export function getMe() {
if (isCallInProgress('getMe')) {
- return;
+ return null;
}
callTracker.getMe = utils.getTimestamp();
- client.getMe(
+ return client.getMe(
(data, textStatus, xhr) => {
callTracker.getMe = 0;
@@ -661,6 +632,8 @@ export function getMe() {
type: ActionTypes.RECEIVED_ME,
me: data
});
+
+ GlobalActions.newLocalizationSelected(data.locale);
},
(err) => {
callTracker.getMe = 0;
@@ -706,11 +679,11 @@ export function getStatuses() {
export function getMyTeam() {
if (isCallInProgress('getMyTeam')) {
- return;
+ return null;
}
callTracker.getMyTeam = utils.getTimestamp();
- client.getMyTeam(
+ return client.getMyTeam(
function getMyTeamSuccess(data, textStatus, xhr) {
callTracker.getMyTeam = 0;
@@ -719,7 +692,7 @@ export function getMyTeam() {
}
AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_TEAM,
+ type: ActionTypes.RECEIVED_MY_TEAM,
team: data
});
},
diff --git a/web/react/utils/channel_intro_messages.jsx b/web/react/utils/channel_intro_messages.jsx
index ed94f94b8..94f3f0ce0 100644
--- a/web/react/utils/channel_intro_messages.jsx
+++ b/web/react/utils/channel_intro_messages.jsx
@@ -8,8 +8,7 @@ import ToggleModalButton from '../components/toggle_modal_button.jsx';
import UserProfile from '../components/user_profile.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import Constants from '../utils/constants.jsx';
-import TeamStore from '../stores/team_store.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import {FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'mm-intl';
@@ -40,7 +39,7 @@ export function createDMIntroMessage(channel) {
<div className='post-profile-img__container channel-intro-img'>
<img
className='post-profile-img'
- src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at + '&' + Utils.getSessionIndex()}
+ src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at}
height='50'
width='50'
/>
@@ -93,37 +92,19 @@ export function createOffTopicIntroMessage(channel) {
}
export function createDefaultIntroMessage(channel) {
- const team = TeamStore.getCurrent();
- let inviteModalLink;
- if (team.type === Constants.INVITE_TEAM) {
- inviteModalLink = (
- <a
- className='intro-links'
- href='#'
- onClick={EventHelpers.showInviteMemberModal}
- >
- <i className='fa fa-user-plus'></i>
- <FormattedMessage
- id='intro_messages.inviteOthers'
- defaultMessage='Invite others to this team'
- />
- </a>
- );
- } else {
- inviteModalLink = (
- <a
- className='intro-links'
- href='#'
- onClick={EventHelpers.showGetTeamInviteLinkModal}
- >
- <i className='fa fa-user-plus'></i>
- <FormattedMessage
- id='intro_messages.inviteOthers'
- defaultMessage='Invite others to this team'
- />
- </a>
- );
- }
+ const inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ onClick={GlobalActions.showGetTeamInviteLinkModal}
+ >
+ <i className='fa fa-user-plus'></i>
+ <FormattedMessage
+ id='intro_messages.inviteOthers'
+ defaultMessage='Invite others to this team'
+ />
+ </a>
+ );
return (
<div className='channel-intro'>
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 76d42137a..e00f28a14 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -1,8 +1,8 @@
// See License.txt for license information.
import BrowserStore from '../stores/browser_store.jsx';
-import TeamStore from '../stores/team_store.jsx';
-import ErrorStore from '../stores/error_store.jsx';
+
+import {browserHistory} from 'react-router';
let translations = {
connectionError: 'There appears to be a problem with your internet connection.',
@@ -50,10 +50,10 @@ function handleError(methodName, xhr, status, err) {
if (xhr.status === 401) {
if (window.location.href.indexOf('/channels') === 0) {
- window.location.pathname = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search);
+ browserHistory.push('/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search));
} else {
- var teamURL = window.location.href.split('/channels')[0];
- window.location.href = teamURL + '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search);
+ var teamURL = window.location.pathname.split('/channels')[0];
+ browserHistory.push(teamURL + '/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search));
}
}
@@ -289,13 +289,17 @@ export function switchToEmail(data, success, error) {
track('api', 'api_users_switch_to_email');
}
-export function logout() {
+export function logout(success, error) {
track('api', 'api_users_logout');
- var currentTeamUrl = TeamStore.getCurrentTeamUrl();
- BrowserStore.signalLogout();
- BrowserStore.clear();
- ErrorStore.clearLastError();
- window.location.href = currentTeamUrl + '/logout';
+ $.ajax({
+ url: '/api/v1/users/logout',
+ type: 'POST',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('logout', xhr, status, err);
+ error(e);
+ }
+ });
}
export function loginByEmail(name, email, password, success, error) {
@@ -437,7 +441,7 @@ export function getServerAudits(success, error) {
}
export function getConfig(success, error) {
- $.ajax({
+ return $.ajax({
url: '/api/v1/admin/config',
dataType: 'json',
contentType: 'application/json',
@@ -457,7 +461,6 @@ export function getAnalytics(name, teamId, success, error) {
} else {
url += teamId + '/' + name;
}
-
$.ajax({
url,
dataType: 'json',
@@ -471,6 +474,34 @@ export function getAnalytics(name, teamId, success, error) {
});
}
+export function getClientConfig(success, error) {
+ return $.ajax({
+ url: '/api/v1/admin/client_props',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getClientConfig', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getTeamAnalytics(teamId, name, success, error) {
+ $.ajax({
+ url: '/api/v1/admin/analytics/' + teamId + '/' + name,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('getTeamAnalytics', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function saveConfig(config, success, error) {
$.ajax({
url: '/api/v1/admin/save_config',
@@ -529,6 +560,21 @@ export function getAllTeams(success, error) {
});
}
+export function getMeLoggedIn(success, error) {
+ return $.ajax({
+ cache: false,
+ url: '/api/v1/users/me_logged_in',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getMeLoggedIn', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getMe(success, error) {
var currentUser = null;
$.ajax({
@@ -635,38 +681,6 @@ export function findTeamByName(teamName, success, error) {
});
}
-export function findTeamsSendEmail(email, success, error) {
- $.ajax({
- url: '/api/v1/teams/email_teams',
- dataType: 'json',
- contentType: 'application/json',
- type: 'POST',
- data: JSON.stringify({email: email}),
- success,
- error: function onError(xhr, status, err) {
- var e = handleError('findTeamsSendEmail', xhr, status, err);
- error(e);
- }
- });
-
- track('api', 'api_teams_email_teams');
-}
-
-export function findTeams(email, success, error) {
- $.ajax({
- url: '/api/v1/teams/find_teams',
- dataType: 'json',
- contentType: 'application/json',
- type: 'POST',
- data: JSON.stringify({email: email}),
- success,
- error: function onError(xhr, status, err) {
- var e = handleError('findTeams', xhr, status, err);
- error(e);
- }
- });
-}
-
export function createChannel(channel, success, error) {
$.ajax({
url: '/api/v1/channels/create',
@@ -835,7 +849,7 @@ export function updateLastViewedAt(channelId, success, error) {
}
export function getChannels(success, error) {
- $.ajax({
+ return $.ajax({
cache: false,
url: '/api/v1/channels/',
dataType: 'json',
@@ -901,7 +915,7 @@ export function getChannelExtraInfo(id, memberLimit, success, error) {
url += '/' + memberLimit;
}
- $.ajax({
+ return $.ajax({
url,
dataType: 'json',
contentType: 'application/json',
@@ -1018,7 +1032,7 @@ export function getPostsPage(channelId, offset, limit, success, error, complete)
}
export function getPosts(channelId, since, success, error, complete) {
- $.ajax({
+ return $.ajax({
url: '/api/v1/channels/' + channelId + '/posts/' + since,
dataType: 'json',
type: 'GET',
@@ -1347,7 +1361,7 @@ export function getStatuses(ids, success, error) {
}
export function getMyTeam(success, error) {
- $.ajax({
+ return $.ajax({
url: '/api/v1/teams/me',
dataType: 'json',
type: 'GET',
@@ -1437,7 +1451,7 @@ export function listIncomingHooks(success, error) {
}
export function getAllPreferences(success, error) {
- $.ajax({
+ return $.ajax({
url: '/api/v1/preferences/',
dataType: 'json',
type: 'GET',
@@ -1569,3 +1583,68 @@ export function removeLicenseFile(success, error) {
track('api', 'api_license_upload');
}
+
+export function getClientLicenceConfig(success, error) {
+ return $.ajax({
+ url: '/api/v1/license/client_config',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getClientLicenceConfig', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getInviteInfo(success, error, id) {
+ $.ajax({
+ url: '/api/v1/teams/get_invite_info',
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json',
+ data: JSON.stringify({invite_id: id}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getInviteInfo', xhr, status, err);
+ if (error) {
+ error(e);
+ }
+ }
+ });
+}
+
+export function verifyEmail(success, error, uid, hid) {
+ $.ajax({
+ url: '/api/v1/users/verify_email',
+ type: 'POST',
+ contentType: 'application/json',
+ dataType: 'text',
+ data: JSON.stringify({uid, hid}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('verifyEmail', xhr, status, err);
+ if (error) {
+ error(e);
+ }
+ }
+ });
+}
+
+export function resendVerification(success, error, teamName, email) {
+ $.ajax({
+ url: '/api/v1/users/resend_verification',
+ type: 'POST',
+ contentType: 'application/json',
+ dataType: 'text',
+ data: JSON.stringify({team_name: teamName, email}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('resendVerification', xhr, status, err);
+ if (error) {
+ error(e);
+ }
+ }
+ });
+}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index daea9f43e..3de562b7b 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -42,13 +42,15 @@ export default {
RECEIVED_MSG: null,
- RECEIVED_TEAM: null,
+ RECEIVED_MY_TEAM: null,
RECEIVED_CONFIG: null,
RECEIVED_LOGS: null,
RECEIVED_SERVER_AUDITS: null,
RECEIVED_ALL_TEAMS: null,
+ RECEIVED_LOCALE: null,
+
SHOW_SEARCH: null,
TOGGLE_IMPORT_THEME_MODAL: null,
@@ -143,6 +145,7 @@ export default {
EMAIL_SERVICE: 'email',
SIGNIN_CHANGE: 'signin_change',
SIGNIN_VERIFIED: 'verified',
+ SESSION_EXPIRED: 'expired',
POST_CHUNK_SIZE: 60,
MAX_POST_CHUNKS: 3,
POST_FOCUS_CONTEXT_RADIUS: 10,
@@ -267,7 +270,7 @@ export default {
buttonColor: '#FFFFFF',
mentionHighlightBg: '#984063',
mentionHighlightLink: '#A4FFEB',
- codeTheme: 'solarized_dark'
+ codeTheme: 'solarized-dark'
},
windows10: {
type: 'Windows Dark',
@@ -371,21 +374,6 @@ export default {
uiName: 'New Message Separator'
},
{
- group: 'linkAndButtonElements',
- id: 'linkColor',
- uiName: 'Link Color'
- },
- {
- group: 'linkAndButtonElements',
- id: 'buttonBg',
- uiName: 'Button BG'
- },
- {
- group: 'linkAndButtonElements',
- id: 'buttonColor',
- uiName: 'Button Text'
- },
- {
group: 'centerChannelElements',
id: 'mentionHighlightBg',
uiName: 'Mention Highlight BG'
@@ -401,11 +389,11 @@ export default {
uiName: 'Code Theme',
themes: [
{
- id: 'solarized_dark',
+ id: 'solarized-dark',
uiName: 'Solarized Dark'
},
{
- id: 'solarized_light',
+ id: 'solarized-light',
uiName: 'Solarized Light'
},
{
@@ -417,6 +405,21 @@ export default {
uiName: 'Monokai'
}
]
+ },
+ {
+ group: 'linkAndButtonElements',
+ id: 'linkColor',
+ uiName: 'Link Color'
+ },
+ {
+ group: 'linkAndButtonElements',
+ id: 'buttonBg',
+ uiName: 'Button BG'
+ },
+ {
+ group: 'linkAndButtonElements',
+ id: 'buttonColor',
+ uiName: 'Button Text'
}
],
DEFAULT_CODE_THEME: 'github',
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index 493916058..2b1aed9c0 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -193,6 +193,16 @@ class MattermostMarkdownRenderer extends marked.Renderer {
outHref = outHref.substring(1, outHref.length - 1);
}
+ try {
+ const unescaped = decodeURIComponent(unescape(href)).replace(/[^\w:]/g, '').toLowerCase();
+
+ if (unescaped.indexOf('javascript:') === 0 || unescaped.indexOf('vbscript:') === 0) { // eslint-disable-line no-script-url
+ return '';
+ }
+ } catch (e) {
+ return '';
+ }
+
if (!(/[a-z+.-]+:/i).test(outHref)) {
outHref = `http://${outHref}`;
}
@@ -548,3 +558,18 @@ export function format(text, options) {
return new MattermostParser(markdownOptions).parse(tokens);
}
+// Marked helper functions that should probably just be exported
+
+function unescape(html) {
+ return html.replace(/&([#\w]+);/g, (_, m) => {
+ const n = m.toLowerCase();
+ if (n === 'colon') {
+ return ':';
+ } else if (n.charAt(0) === '#') {
+ return n.charAt(1) === 'x' ?
+ String.fromCharCode(parseInt(n.substring(2), 16)) :
+ String.fromCharCode(+n.substring(1));
+ }
+ return '';
+ });
+}
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 6942a8e08..88777164b 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -2,9 +2,10 @@
// See License.txt for license information.
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as GlobalActions from '../action_creators/global_actions.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx';
+import LocalizationStore from '../stores/localization_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
import TeamStore from '../stores/team_store.jsx';
import Constants from '../utils/constants.jsx';
@@ -941,7 +942,7 @@ export function updateAddressBar(channelName) {
}
export function switchChannel(channel) {
- EventHelpers.emitChannelClickEvent(channel);
+ GlobalActions.emitChannelClickEvent(channel);
updateAddressBar(channel.name);
@@ -1130,8 +1131,8 @@ export function fileSizeToString(bytes) {
// Converts a filename (like those attached to Post objects) to a url that can be used to retrieve attachments from the server.
export function getFileUrl(filename, isDownload) {
- const downloadParam = isDownload ? '&download=1' : '';
- return getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + getSessionIndex() + downloadParam;
+ const downloadParam = isDownload ? '?download=1' : '';
+ return getWindowLocationOrigin() + '/api/v1/files/get' + filename + downloadParam;
}
// Gets the name of a file (including extension) from a given url or file path.
@@ -1151,14 +1152,6 @@ export function getWebsocketPort(protocol) {
return '';
}
-export function getSessionIndex() {
- if (global.window.mm_session_token_index >= 0) {
- return 'session_token_index=' + global.window.mm_session_token_index;
- }
-
- return '';
-}
-
// Generates a RFC-4122 version 4 compliant globally unique identifier.
export function generateId() {
// implementation taken from http://stackoverflow.com/a/2117523
@@ -1405,3 +1398,19 @@ export function isPostEphemeral(post) {
export function getRootId(post) {
return post.root_id === '' ? post.id : post.root_id;
}
+
+export function localizeMessage(id, defaultMessage) {
+ const translations = LocalizationStore.getTranslations();
+ if (translations) {
+ const value = translations[id];
+ if (value) {
+ return value;
+ }
+ }
+
+ if (defaultMessage) {
+ return defaultMessage;
+ }
+
+ return id;
+}
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index f782da36b..76081710f 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -145,12 +145,27 @@
.form-group {
margin-bottom: 25px;
}
+ .file__upload {
+ position: relative;
+ margin: 0 10px 10px 0;
+ display: inline-block;
+ input {
+ position: absolute;
+ @include opacity(0);
+ width: 100%;
+ height: 100%;
+ z-index: 5;
+ top: 0;
+ left: 0;
+ }
+ }
.help-text {
+ &.no-margin {
+ margin: 0;
+ }
ul, ol {
padding-left: 23px;
}
- }
- .help-text {
margin: 10px 0 0 15px;
color: #777;
.help-link {
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 39cebe856..209f8e27f 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -58,23 +58,27 @@ body.ios {
box-shadow: none;
white-space: normal;
}
- .textbox-help-link {
+ .help__text {
+ right: 0;
position: absolute;
z-index: 3;
bottom: -23px;
font-size: 13px;
cursor: pointer;
}
- .textbox-help-link {
- right: 0;
+ .textbox-preview-link {
+ margin-right: 8px;
}
min-height:36px;
}
.help_format_text {
+ display: none !important;
position: absolute;
bottom: -23px;
- right: 40px;
+ left: 0px;
+ overflow: hidden;
+ text-overflow: ellipsis;
font-size: 0.85em;
@include opacity(0);
@include single-transition(all 0.2s ease);
@@ -391,15 +395,16 @@ body.ios {
top: -5px;
position: relative;
}
- .msg-typing {
- min-height: 25px;
- display: block;
- @include opacity(0.7);
- white-space: nowrap;
- width: 80%;
- overflow: hidden;
- text-overflow: ellipsis;
- }
+ }
+ .msg-typing {
+ display: block;
+ @include opacity(0.7);
+ white-space: nowrap;
+ margin-bottom: 5px;
+ overflow: hidden;
+ font-size: 0.95em;
+ text-overflow: ellipsis;
+ height: 20px;
}
}
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index a9a572768..06ce17041 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -573,8 +573,7 @@
.glyphicon-refresh-animate {
right: 33px;
top: 15px;
- color: #fff;
- color: rgba(255,255,255,0.5);
+ color: #aaa;
}
.form-control {
border: none;
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 4c6fa0eae..2a536925c 100644
--- a/web/static/i18n/en.json
+++ b/web/static/i18n/en.json
@@ -1,11 +1,12 @@
{
"about.close": "Close",
"about.date": "Build Date:",
- "about.enterpriseEdition": "Enterprise Edition",
+ "about.enterpriseEditione1": "Enterprise Edition E1",
"about.hash": "Build Hash:",
"about.licensed": "Licensed by:",
"about.number": "Build Number:",
- "about.teamEdtion": "Team Edition",
+ "about.teamEditiont0": "Team Edition T0",
+ "about.teamEditiont1": "Team Edition T1",
"about.title": "About Mattermost",
"about.version": "Version:",
"access_history.title": "Access History",
@@ -152,14 +153,14 @@
"admin.ldap.bannerDesc": "If a user attribute changes on the LDAP server it will be updated the next time the user enters their credentials to log in to Mattermost. This includes if a user is made inactive or removed from an LDAP server. Synchronization with LDAP servers is planned in a future release.",
"admin.ldap.bannerHeading": "Note:",
"admin.ldap.baseDesc": "The Base DN is the Distinguished Name of the location where Mattermost should start its search for users in the LDAP tree.",
- "admin.ldap.baseEx": "Ex \"dc=mydomain,dc=com\"",
+ "admin.ldap.baseEx": "Ex \"ou=Unit Name,dc=corp,dc=example,dc=com\"",
"admin.ldap.baseTitle": "BaseDN:",
"admin.ldap.bindPwdDesc": "Password of the user given in \"Bind Username\".",
"admin.ldap.bindPwdTitle": "Bind Password:",
"admin.ldap.bindUserDesc": "The username used to perform the LDAP search. This should typically be an account created specifically for use with Mattermost. It should have access limited to read the portion of the LDAP tree specified in the BaseDN field.",
"admin.ldap.bindUserTitle": "Bind Username:",
"admin.ldap.emailAttrDesc": "The attribute in the LDAP server that will be used to populate the email addresses of users in Mattermost.",
- "admin.ldap.emailAttrEx": "Ex \"mail\"",
+ "admin.ldap.emailAttrEx": "Ex \"mail\" or \"userPrincipalName\"",
"admin.ldap.emailAttrTitle": "Email Attribute:",
"admin.ldap.enableDesc": "When true, Mattermost allows login using LDAP",
"admin.ldap.enableTitle": "Enable Login With LDAP:",
@@ -191,11 +192,13 @@
"admin.ldap.usernameAttrEx": "Ex \"sAMAccountName\"",
"admin.ldap.usernameAttrTitle": "Username Attribute:",
"admin.licence.keyMigration": "If you’re migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, <a href=\"http://mattermost.com\" target=\"_blank\">disable all Enterprise Edition features on this server</a>. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.",
+ "admin.license.chooseFile": "Choose File",
"admin.license.edition": "Edition: ",
"admin.license.enterpriseEdition": "Mattermost Enterprise Edition. Designed for enterprise-scale communication.",
"admin.license.enterpriseType": "<div><p>This compiled release of Mattermost platform is provided under a <a href=\"http://mattermost.com\" target=\"_blank\">commercial license</a> from Mattermost, Inc. based on your subscription level and is subject to the <a href=\"{terms}\" target=\"_blank\">Terms of Service.</a></p><p>Your subscription details are as follows:</p>Name: {name}<br />Company or organization name: {company}<br/>Number of users: {users}<br/>License issued: {issued}<br/>Start date of license: {start}<br/>Expiry date of license: {expires}<br/>LDAP: {ldap}<br/></div>",
"admin.license.key": "License Key: ",
"admin.license.keyRemove": "Remove Enterprise License and Downgrade Server",
+ "admin.license.noFile": "No file uploaded",
"admin.license.removing": "Removing License...",
"admin.license.teamEdition": "Mattermost Team Edition. Designed for teams from 5 to 50 users.",
"admin.license.teamType": "<span><p>This compiled release of Mattermost platform is offered under an MIT license.</p><p>See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.</p></span>",
@@ -658,8 +661,10 @@
"email_signup.find": "Find my teams",
"email_verify.almost": "{siteName}: You are almost done",
"email_verify.notVerifiedBody": "Please verify your email address. Check your inbox for an email.",
+ "email_verify.verifyFailed": "Failed to verify your email.",
"email_verify.resend": "Resend Email",
"email_verify.sent": " Verification email sent.",
+ "email_verify.failed": " Failed to send verification email.",
"email_verify.verified": "{siteName} Email Verified",
"email_verify.verifiedBody": "<p>Your email has been verified! Click <a href={url}>here</a> to log in.</p>",
"error_bar.preview_mode": "Preview Mode: Email notifications have not been configured",
@@ -755,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",
@@ -819,16 +825,16 @@
"navbar_dropdown.teamSettings": "Team Settings",
"password_form.change": "Change my password",
"password_form.click": "Click <a href={url}>here</a> to log in.",
- "password_form.enter": "Enter a new password for your {teamDisplayName} {siteName} account.",
+ "password_form.enter": "Enter a new password for your {siteName} account.",
"password_form.error": "Please enter at least {chars} characters.",
"password_form.pwd": "Password",
"password_form.title": "Password Reset",
"password_form.update": "Your password has been updated successfully.",
"password_send.checkInbox": "Please check your inbox.",
- "password_send.description": "To reset your password, enter the email address you used to sign up for {teamName}.",
+ "password_send.description": "To reset your password, enter the email address you used to sign up.",
"password_send.email": "Email",
"password_send.error": "Please enter a valid email address.",
- "password_send.link": "<p>A password reset link has been sent to <b>{email}</b> for your <b>{teamDisplayName}</b> team on {hostname}.</p>",
+ "password_send.link": "<p>A password reset link has been sent to <b>{email}</b></p>",
"password_send.reset": "Reset my password",
"password_send.title": "Password Reset",
"post_attachment.collapse": "▲ collapse text",
@@ -1300,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 b14470872..f42dc879a 100644
--- a/web/static/i18n/es.json
+++ b/web/static/i18n/es.json
@@ -1,11 +1,12 @@
{
"about.close": "Cerrar",
"about.date": "Fecha de compilación:",
- "about.enterpriseEdition": "Edición Enterprise",
+ "about.enterpriseEditione1": "Edición Enterprise E1",
"about.hash": "Hash de compilación:",
"about.licensed": "Licenciado por:",
"about.number": "Número de compilación:",
- "about.teamEdtion": "Edición Team",
+ "about.teamEditiont0": "Edición Team T0",
+ "about.teamEditiont1": "Edición Team T1",
"about.title": "Acerca de Mattermost",
"about.version": "Versión:",
"access_history.title": "Historial de Acceso",
@@ -152,14 +153,14 @@
"admin.ldap.bannerDesc": "Si el atributo de un usuario cambia en el servidor LDAP será actualizado la próxima vez que el usuario ingrese sus credenciales para iniciar sesión en Mattermost. Esto incluye si un usuario se inactiva o se remueve en el servidor LDAP. Sincronización con servidores LDAP está planificado para futuras versiones.",
"admin.ldap.bannerHeading": "Nota:",
"admin.ldap.baseDesc": "El DN Base es el Nombre Distinguido de la ubicación donde Mattermost debe comenzar a buscar a los usuarios en el árbol del LDAP.",
- "admin.ldap.baseEx": "Ex \"dc=midominio,dc=com\"",
+ "admin.ldap.baseEx": "Ej \"ou=Unit Name,dc=corp,dc=example,dc=com\"",
"admin.ldap.baseTitle": "DN Base:",
"admin.ldap.bindPwdDesc": "Contraseña del usuario asignado en \"Usuario de Enlace\".",
"admin.ldap.bindPwdTitle": "Contraseña de Enlace:",
"admin.ldap.bindUserDesc": "El usuario que realizará las busquedas LDAP. Normalmente este debería ser una cuenta específicamente creada para ser utilizada por Mattermost. Debería contat con acceso limitado para leer la porción del árbol LDAP especificada en el campo DN Base.",
"admin.ldap.bindUserTitle": "Usuario de Enlace:",
"admin.ldap.emailAttrDesc": "El atributo en el servidor LDAP que será utilizado para poblar la dirección de correo electrónico de los usuarios en Mattermost.",
- "admin.ldap.emailAttrEx": "Ej \"mail\"",
+ "admin.ldap.emailAttrEx": "Ej \"mail\" o \"userPrincipalName\"",
"admin.ldap.emailAttrTitle": "Atributo de Correo Electrónico:",
"admin.ldap.enableDesc": "Cuando es verdadero, Mattermost permite realizar inicio de sesión utilizando LDAP",
"admin.ldap.enableTitle": "Habilitar inicio de sesión con LDAP:",
@@ -191,11 +192,13 @@
"admin.ldap.usernameAttrEx": "Ej \"sAMAccountName\"",
"admin.ldap.usernameAttrTitle": "Atributo Usuario:",
"admin.licence.keyMigration": "Si estás migrando servidores es posible que necesites remover tu licencia de este servidor para poder instalarlo en un servidor nuevo. Para empezar, <a href=\"http://mattermost.com\" target=\"_blank\">deshabilita todas las características de la Edición Enterprise de este servidor</a>. Esta operación habilitará la opción para remover la licencia y degradar este servidor de la Edición Enterprise a la Edición Team.",
+ "admin.license.chooseFile": "Escoger Archivo",
"admin.license.edition": "Edición: ",
"admin.license.enterpriseEdition": "Mattermost Edición Enterprise. Diseñada para comunicación de escala empresarial.",
"admin.license.enterpriseType": "<div><p>Esta versión compilada de la plataforma de Mattermost es provista bajo una <a href=\"http://mattermost.com\" target=\"_blank\">licencia comercial</a> de Mattermost, Inc. en función en su nivel de subscripción y bajo los <a href=\"{terms}\" target=\"_blank\">Términos del Servicio.</a></p><p>Los detalles de tu subscripción son los siguientes:</p>Nombre: {name}<br />Nombre compañía u organización: {company}<br/>Cantidad de usuarios: {users}<br/>Licencia emitida: {issued}<br/>Fecha de inicio: {start}<br/>Fecha de expiración: {expires}<br/>LDAP: {ldap}<br/></div>",
"admin.license.key": "Llave de la Licencia: ",
"admin.license.keyRemove": "Remover la Licencia Enterprise y Degradar el Servidor",
+ "admin.license.noFile": "No se subió ningún archivo",
"admin.license.removing": "Removiendo Licencia...",
"admin.license.teamEdition": "Mattermost Edición Team. Diseñado para equipos desde 5 hasta 50 usuarios.",
"admin.license.teamType": "<span><p>Esta versión compilada de la plataforma de Mattermost es proporcionada bajo la licencia MIT.</p><p>Lea MIT-COMPILED-LICENSE.txt en el directorio raíz de la instalación para más detalles. Lea NOTICES.txt para información sobre software libre utilizado en este sistema.</p></span>",
@@ -819,16 +822,16 @@
"navbar_dropdown.teamSettings": "Configurar Equipo",
"password_form.change": "Cambiar mi contraseña",
"password_form.click": " Pincha <a href={url}>aquí</a> para iniciar sesión.",
- "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {teamDisplayName} {siteName}.",
+ "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {siteName}.",
"password_form.error": "Por favor ingresa al menos {chars} caracteres.",
"password_form.pwd": "Contraseña",
"password_form.title": "Restablecer Contraseña",
"password_form.update": "Tu contraseña ha sido actualizada satisfactoriamente.",
"password_send.checkInbox": "Por favor revisa tu bandeja de entrada.",
- "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte en {teamName}.",
+ "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte.",
"password_send.email": "Correo electrónico",
"password_send.error": "Por favor ingresa una dirección correo electrónico válida.",
- "password_send.link": "<p>Se ha enviado un enlace para restablecer la contraseña a <b>{email}</b> para tu equipo <b>{teamDisplayName}</b> en {hostname}.</p>",
+ "password_send.link": "<p>Se ha enviado un enlace para restablecer la contraseña a <b>{email}</b></p>",
"password_send.reset": "Restablecer mi contraseña",
"password_send.title": "Restablecer Contraseña",
"post_attachment.collapse": "▲ colapsar texto",
@@ -1300,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 f4b998ded..f6210a6ae 100644
--- a/web/static/i18n/pt.json
+++ b/web/static/i18n/pt.json
@@ -1,11 +1,12 @@
{
"about.close": "Fechar",
"about.date": "Data De Criação:",
- "about.enterpriseEdition": "Enterprise Edition",
+ "about.enterpriseEditione1": "Enterprise Edition E1",
"about.hash": "Hash de Compilação:",
"about.licensed": "Licenciado pela:",
"about.number": "O Número De Compilação:",
- "about.teamEdtion": "Team Edition",
+ "about.teamEditiont0": "Team Edition T0",
+ "about.teamEditiont1": "Team Edition T1",
"about.title": "Sobre o Mattermost",
"about.version": "Versão:",
"access_history.title": "Histórico de Acesso",
@@ -26,7 +27,7 @@
"admin.email.allowEmailSignInDescription": "Quando verdadeiro, Mattermost permite aos usuários fazer login usando o e-mail e senha.",
"admin.email.allowEmailSignInTitle": "Permitir Login Com E-mail: ",
"admin.email.allowSignupDescription": "Quando verdadeiro, Mattermost permite a criação de equipe e conta de inscrição através de e-mail e senha. Este valor deve ser falso somente quando você deseja limitar a entrada para o single-sign-on service como OAuth ou LDAP.",
- "admin.email.allowSignupTitle": "Permitir Login com E-Mail: ",
+ "admin.email.allowSignupTitle": "Permitir Inscrição com E-Mail: ",
"admin.email.allowUsernameSignInDescription": "Quando verdadeiro, Mattermost permite os usuários fazer login usando seu nome de usuário e senha. Esta configuração é normalmente utilizado apenas quando a verificação de e-mail está desativada.",
"admin.email.allowUsernameSignInTitle": "Permitir Login Com Usuário: ",
"admin.email.connectionSecurityNone": "Nenhum",
@@ -43,7 +44,7 @@
"admin.email.false": "falso",
"admin.email.inviteSaltDescription": "32-caracteres salt adicionados a assinatura de convites por e-mail. Aleatoriamente gerados na instalação. Click \"Re-Gerar\" para criar um novo salt.",
"admin.email.inviteSaltExample": "Ex \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"",
- "admin.email.inviteSaltTitle": "Convidar Salt:",
+ "admin.email.inviteSaltTitle": "Salt Convite:",
"admin.email.notificationDisplayDescription": "Mostra o nome da conta de e-mail usada quando a notificação de e-mail é enviado do Mattermost.",
"admin.email.notificationDisplayExample": "Ex: \"Mattermost Notificação\", \"Sistema\", \"Não-Responda\"",
"admin.email.notificationDisplayTitle": "Notificação Nome de Exibição:",
@@ -152,14 +153,14 @@
"admin.ldap.bannerDesc": "Se um atributo de usuário de mudar no servidor LDAP, ele será atualizado na próxima vez que o usuário inserir suas credenciais para iniciar sessão no Mattermost. Isso inclui se um usuário estiver inativo ou removido de um servidor LDAP. Sincronização com servidores LDAP está prevista para um lançamento futuro.",
"admin.ldap.bannerHeading": "Nota:",
"admin.ldap.baseDesc": "Base DN é o nome distinto do local onde Mattermost deve começar sua busca para os usuários na árvore LDAP.",
- "admin.ldap.baseEx": "Ex \"dc=mydomain,dc=com\"",
+ "admin.ldap.baseEx": "Ex \"ou=Unit Name,dc=corp,dc=example,dc=com\"",
"admin.ldap.baseTitle": "BaseDN:",
"admin.ldap.bindPwdDesc": "Senha do usuário fornecido em \"Bind Username\".",
"admin.ldap.bindPwdTitle": "Vincular Senha:",
"admin.ldap.bindUserDesc": "O nome de usuário usado para realizar a pesquisa LDAP. Isso deve ser tipicamente uma conta criada especificamente para uso do Mattermost. Deve ter acesso limitado a ler a parte da árvore LDAP especificado no campo BaseDN.",
"admin.ldap.bindUserTitle": "Bind Username:",
"admin.ldap.emailAttrDesc": "O atributo no servidor LDAP que será usado para preencher os endereços de e-mail de usuários no Mattermost.",
- "admin.ldap.emailAttrEx": "Ex \"mail\"",
+ "admin.ldap.emailAttrEx": "Ex \"mail\" ou \"userPrincipalName\"",
"admin.ldap.emailAttrTitle": "Atributo de E-mail:",
"admin.ldap.enableDesc": "Quando verdadeiro, Mattermost permite login utilizando LDAP",
"admin.ldap.enableTitle": "Ativar Login With LDAP:",
@@ -191,11 +192,13 @@
"admin.ldap.usernameAttrEx": "Ex \"sAMAccountName\"",
"admin.ldap.usernameAttrTitle": "Atributo do Usuário:",
"admin.licence.keyMigration": "Se você estiver migrando seu servidor você pode precisar remover sua chave da licença deste servidor a pedido para instala-la em um novo servidor. Para iniciar, <a href=\"http://mattermost.com\" target=\"_blank\">desativar todos os recursos Enterprise Edition deste servidor</a>. Isto irá habilitar para remover a chave da licença e fazer downgrade deste servidor Enterprise Edition para Team Edition.",
+ "admin.license.chooseFile": "Escolha um Arquivo",
"admin.license.edition": "Edição: ",
"admin.license.enterpriseEdition": "Mattermost Enterprise Edition. Desenvolvido para escala empresarial de comunicação.",
"admin.license.enterpriseType": "<div><p>Esta versão compilada da plataforma Mattermost é fornecida sob a <a href=\"http://mattermost.com\" target=\"_blank\">licença comercial</a> para Mattermost, Inc. com base em seu nível de subscrição e está sujeito a <a href=\"{terms}\" target=\"_blank\">Termos de Serviço.</a></p><p>Os detalhes de sua assinatura, são como segue:</p>Nome: {name}<br />Nome da Empresa ou organização: {company}<br/>Número de usuários: {users}<br/>Licença emitida: {issued}<br/>Data de Início da licença: {start}<br/>Data de expiração da licença: {expires}<br/>LDAP: {ldap}<br/></div>",
"admin.license.key": "Chave da Licença: ",
"admin.license.keyRemove": "Remover a Licença Enterprise e fazer Downgrade do Servidor",
+ "admin.license.noFile": "Nenhum arquivo enviado",
"admin.license.removing": "Removendo a Licença...",
"admin.license.teamEdition": "Mattermost Team Edition. Desenvolvido para equipes de 5 a 50 usuários.",
"admin.license.teamType": "<span><p>Esta versão compilada da plataforma Mattermost é oferecido sob uma licença MIT.</p><p>Ver MIT-COMPILED-LICENSE.txt no raiz do diretório de instalação para obter detalhes. Ver NOTICES.txt para obter informações sobre o software open source usados neste sistema.</p></span>",
@@ -205,7 +208,7 @@
"admin.license.uploadDesc": "Enviar uma chave da licença para Mattermost Enterprise Edition para fazer upgrade deste servidor. <a href=\"http://mattermost.com\" target=\"_blank\">Visite-nos online</a> para saber mais sobre os beneficios da Enterprise Edition ou para comprar uma chave.",
"admin.license.uploading": "Enviando Licença...",
"admin.log.consoleDescription": "Normalmente definido como falso em produção. Os desenvolvedores podem definir este campo como verdadeiro para mensagens de log de saída no console baseado na opção de nível de console. Se verdadeiro, o servidor escreve mensagens para o fluxo de saída padrão (stdout).",
- "admin.log.consoleTitle": "Log Do Console: ",
+ "admin.log.consoleTitle": "Log Para o Console: ",
"admin.log.false": "falso",
"admin.log.fileDescription": "Normalmente definido como verdadeiro em produção. Quando verdadeiro, arquivos de log são gravados no arquivo de log especificado no campo de localização abaixo.",
"admin.log.fileLevelDescription": "Esta configuração determina o nível de detalhe que são gravados no log de eventos no console. ERROR: Saídas somente mensagens de erro. INFO: Saídas de mensagens de erro e informações em torno de inicialização. DEBUG: Impressões de alto detalhe para desenvolvedores que trabalham na depuração de problemas.",
@@ -277,6 +280,9 @@
"admin.service.attemptTitle": "Máxima Tentativas de Login:",
"admin.service.cmdsDesc": "Quando verdadeiro, comandos slash criados por usuários serão permitidos.",
"admin.service.cmdsTitle": "Ativar Comandos Slash: ",
+ "admin.service.corsDescription": "Ativar requisição de origem HTTP Cross dos domínios especificados (separados por espaço). Usar \"*\" se você quiser permitir CORS de qualquer domínio ou deixe em branco para desativar.",
+ "admin.service.corsEx": "http://example.com https://example.com",
+ "admin.service.corsTitle": "Permitir Requisição Cross-origin de:",
"admin.service.developerDesc": "(Opção dos desenvolvedores) Quando verdadeira, a informação extra em torno dos erros será exibida na UI.",
"admin.service.developerTitle": "Ativar o Modo Desenvolvedor: ",
"admin.service.false": "falso",
@@ -558,6 +564,7 @@
"channel_loader.wrote": " escreveu: ",
"channel_members_modal.addNew": " Adicionar Novos Membros",
"channel_members_modal.close": "Fechar",
+ "channel_members_modal.remove": "Remover",
"channel_memebers_modal.members": " Membros",
"channel_modal.cancel": "Cancelar",
"channel_modal.channel": "Canal",
@@ -589,6 +596,7 @@
"choose_auth_page.find": "Encontrar minhas equipes",
"choose_auth_page.gitlabCreate": "Criar uma equipe com uma conta GitLab",
"choose_auth_page.googleCreate": "Criar nova equipe com a Conta do Google Apps",
+ "choose_auth_page.ldapCreate": "Criar uma nova equipe com uma conta LDAP",
"choose_auth_page.noSignup": "Nenhum método de inscrição configurado, por favor contate seu administrador do sistema.",
"claim.account.noEmail": "Nenhum email específicado",
"claim.email_to_sso.enterPwd": "Entre a senha para o sua conta {team} {site}",
@@ -665,8 +673,8 @@
"file_upload.filesAbove": "Arquivos acima {max}MB não podem ser enviados: {filenames}",
"file_upload.limited": "Limite máximo de uploads de {count} arquivos. Por favor use um post adicional para mais arquivos.",
"file_upload.pasted": "Imagem Colada em ",
- "filtered_user_list.count": "{count, number} {count, plural, one {Membro} other {Membros}}",
- "filtered_user_list.countTotal": "{count, number} {count, plural, one {Membro} other {Membros}} de {total} Total",
+ "filtered_user_list.count": "{count, number} {count, plural, one {membro} other {membros}}",
+ "filtered_user_list.countTotal": "{count, number} {count, plural, one {membro} other {membros}} de {total} Total",
"filtered_user_list.search": "Procurar membros",
"find_team.email": "E-mail",
"find_team.findDescription": "Foi enviado um e-mail com links para todas as equipes do qual você é membro.",
@@ -690,7 +698,7 @@
"general_tab.regenerate": "Re-Gerar",
"general_tab.required": "Este campo é obrigatório",
"general_tab.teamName": "Nome da Equipe",
- "general_tab.teamNameInfo": "Defina o nome da equipe como aparece na sua tela inicial e no topo na lateral esquerda.",
+ "general_tab.teamNameInfo": "Defina o nome da equipe como aparece na sua tela de login e no topo na lateral esquerda.",
"general_tab.title": "Definições Gerais",
"general_tab.yes": "Sim",
"get_link.clipboard": " Link copiado para a área de transferência.",
@@ -698,6 +706,7 @@
"get_link.copy": "Copiar Link",
"get_post_link_modal.help": "O link abaixo permite que usuários autorizados possam ver seus posts.",
"get_post_link_modal.title": "Copiar Permalink",
+ "get_team_invite_link_modal.help": "Enviar o link abaixo para sua equipe de trabalho para que eles se inscrevam no site da sua equipe. O Link de Convite de Equipe como ele não muda pode ser compartilhado com várias pessoas ao menos que seja re-gerado em Configurações de Equipe pelo Administrador de Equipe.",
"get_team_invite_link_modal.helpDisabled": "Criação de usuários está desabilitada para sua equipe. Por favor peça ao administrador de equipe por detalhes.",
"get_team_invite_link_modal.title": "Link para Convite de Equipe",
"intro_messages.DM": "Este é o início do seu histórico de mensagens diretas com {teammate}.<br />Mensagens diretas e arquivos compartilhados aqui não são mostrados para pessoas de fora desta área.",
@@ -731,6 +740,11 @@
"invite_member.send2": "Enviar Convites",
"invite_member.sending": " Enviando",
"invite_member.teamInviteLink": "Você também pode convidar pessoas usando o {link}",
+ "ldap_signup.find": "Encontrar minhas equipes",
+ "ldap_signup.ldap": "Criar uma equipe com uma conta LDAP",
+ "ldap_signup.length_error": "O nome deve ser de 3 ou mais caracteres até um máximo de 15",
+ "ldap_signup.teamName": "Entre o nome da nova equipe",
+ "ldap_signup.team_error": "Por favor entre o nome da equipe",
"loading_screen.loading": "Carregando",
"login.changed": " Método de login alterada com sucesso",
"login.create": "Crie um agora",
@@ -742,7 +756,7 @@
"login.noAccount": "Não tem uma conta? ",
"login.on": "no {siteName}",
"login.or": "ou",
- "login.signTo": "Entrar em:",
+ "login.signTo": "Login em:",
"login.verified": " Email Verificado",
"login_email.badTeam": "Nome ruim de equipe",
"login_email.email": "E-mail",
@@ -766,6 +780,7 @@
"login_username.verifyEmailError": "Por favor verifique seu endereço de email. Verifique por um email na sua caixa de entrada.",
"member_item.makeAdmin": "Tornar Admin",
"member_item.member": "Membro",
+ "member_list.noUsersAdd": "Nenhum usuário para adicionar.",
"members_popover.msg": "Mensagem",
"members_popover.title": "Membros",
"more_channels.close": "Fechar",
@@ -807,16 +822,15 @@
"navbar_dropdown.teamSettings": "Configurações da Equipe",
"password_form.change": "Alterar minha senha",
"password_form.click": "Clique <a href={url}>aqui</a> para logar.",
- "password_form.enter": "Entre uma nova senha para sua conta {teamDisplayName} {siteName}.",
+ "password_form.enter": "Entre uma nova senha para sua conta {siteName}.",
"password_form.error": "Por favor, insira pelo menos {chars} caracteres.",
"password_form.pwd": "Senha",
"password_form.title": "Resetar Senha",
"password_form.update": "Sua senha foi atualizada com sucesso.",
"password_send.checkInbox": "Por favor verifique sua caixa de entrada.",
- "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever em {teamName}.",
+ "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever.",
"password_send.email": "E-mail",
"password_send.error": "Por favor entre um endereço de e-mail válido.",
- "password_send.link": "<p>Um link para resetar a sua senha na equipe <b>{teamDisplayName}</b> em {hostname} foi enviado para <b>{email}</b>.</p>",
"password_send.reset": "Resetar minha senha",
"password_send.title": "Resetar Senha",
"post_attachment.collapse": "▲ recolher texto",
@@ -936,7 +950,7 @@
"signup_user_completed.chooseUser": "Escolha o seu nome de usuário",
"signup_user_completed.create": "Criar Conta",
"signup_user_completed.emailHelp": "Email valido necessário para inscrição",
- "signup_user_completed.emailIs": "Seu endereço de e-mail é <strong>{email}</strong>. Você irá usar esse endereço para entrar em {siteName}.",
+ "signup_user_completed.emailIs": "Seu endereço de e-mail é <strong>{email}</strong>. Você irá usar esse endereço para logar no {siteName}.",
"signup_user_completed.expired": "Você já concluiu o processo de inscrição para este convite ou este convite expirou.",
"signup_user_completed.gitlab": "com GitLab",
"signup_user_completed.google": "com Google",
@@ -960,6 +974,7 @@
"sso_signup.team_error": "Por favor entre o nome da equipe",
"suggestion.mention.all": "Notificar todo mundo na equipe",
"suggestion.mention.channel": "Notifica todos no canal",
+ "suggestion.search.private": "Grupos Privados",
"suggestion.search.public": "Canais Públicos",
"team_export_tab.download": "download",
"team_export_tab.export": "Exportar",
@@ -999,7 +1014,7 @@
"team_signup_display_name.required": "Este campo é obrigatório",
"team_signup_display_name.teamName": "Nome Da Equipe",
"team_signup_email.address": "Endereço de E-mail",
- "team_signup_email.different": "Por favor, use um e-mail diferente do que o usado no login",
+ "team_signup_email.different": "Por favor, use um e-mail diferente do que o usado na inscrição",
"team_signup_email.validEmail": "Por favor entre um endereço de e-mail válido",
"team_signup_password.agreement": "Ao prosseguir para criar sua conta e usar {siteName}, você concorda com nosso <a href='/static/help/terms.html'>Termo de Serviço</a> e <a href='/static/help/privacy.html'>Politica de Privacidade</a>. Se você não concorda, você não pode usar {siteName}.",
"team_signup_password.back": "Voltar para o passo anterior",
@@ -1052,9 +1067,9 @@
"textbox.help": "Ajuda",
"textbox.inlinecode": "`código`",
"textbox.italic": "_itálico_",
- "textbox.preformatted": "```pre-formatado```",
+ "textbox.preformatted": "```pré-formatado```",
"textbox.preview": "Pré-visualização",
- "textbox.quote": ">citado",
+ "textbox.quote": ">citar",
"textbox.strike": "tachado",
"tutorial_intro.allSet": "Está tudo pronto",
"tutorial_intro.end": "Clique em “Próximo” para entrar Town Square. Este é o primeiro canal que sua equipe de trabalho vê quando eles se inscrevem. Use para postar atualizações que todos precisam saber.",
@@ -1272,7 +1287,7 @@
"user.settings.security.logoutActiveSessions": "Ver e fazer Logout das Sessões Ativas",
"user.settings.security.method": "Método de Login",
"user.settings.security.newPassword": "Nova Senha",
- "user.settings.security.oneSignin": "Você pode ter somente um método de inscrição por vez. Trocando o método de inscrição será enviado um email de notificação se você alterar com sucesso.",
+ "user.settings.security.oneSignin": "Você pode ter somente um método de login por vez. Trocando o método de login será enviado um email de notificação se você alterar com sucesso.",
"user.settings.security.password": "Senha",
"user.settings.security.passwordLengthError": "Novas senhas precisam ter pelo menos {chars} characters",
"user.settings.security.passwordMatchError": "As novas senhas que você inseriu não correspondem",
diff --git a/web/static/images/themes/code_themes/solarized_dark.png b/web/static/images/themes/code_themes/solarized-dark.png
index 582df48f9..582df48f9 100644
--- a/web/static/images/themes/code_themes/solarized_dark.png
+++ b/web/static/images/themes/code_themes/solarized-dark.png
Binary files differ
diff --git a/web/static/images/themes/code_themes/solarized_light.png b/web/static/images/themes/code_themes/solarized-light.png
index d2c2702fb..d2c2702fb 100644
--- a/web/static/images/themes/code_themes/solarized_light.png
+++ b/web/static/images/themes/code_themes/solarized-light.png
Binary files differ
diff --git a/web/templates/admin_console.html b/web/templates/admin_console.html
deleted file mode 100644
index 08c90493e..000000000
--- a/web/templates/admin_console.html
+++ /dev/null
@@ -1,21 +0,0 @@
-
-{{define "admin_console"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body>
-<script src="/static/js/Chart.min.js"></script>
-
-<div id='admin_controller'></div>
-
-<script>
- window.setup_admin_console_page({{ .Props }});
-
- $(document).ready(function(){
- $('[data-toggle="tooltip"]').tooltip();
- $('[data-toggle="popover"]').popover();
- });
-</script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/channel.html b/web/templates/channel.html
deleted file mode 100644
index 94d79a022..000000000
--- a/web/templates/channel.html
+++ /dev/null
@@ -1,21 +0,0 @@
-
-{{define "channel"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body>
- <div id="channel_view" class='channel-view'></div>
-<script>
- window.setup_channel_page({{ .Props }}, {{ .Team }}, {{ .Channel }});
- $('body').tooltip( {selector: '[data-toggle=tooltip]'} );
- var modals = $('.modal-body').not('.edit-modal-body');
- if($(window).height() > 1200){
- modals.css('max-height', 1000);
- } else {
- modals.css('max-height', $(window).height() - 200);
- }
- modals.perfectScrollbar();
-</script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/claim_account.html b/web/templates/claim_account.html
deleted file mode 100644
index 2a9126d1b..000000000
--- a/web/templates/claim_account.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{{define "claim_account"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="signup-team__container">
- <img class="signup-team-logo" src="/static/images/logo.png" />
- <div id="claim"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- <div>
- </div>
- <script>
- window.setup_claim_account_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/docs.html b/web/templates/docs.html
deleted file mode 100644
index dc18e5cb6..000000000
--- a/web/templates/docs.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{{define "docs"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
-<div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="docs__page" id="docs"></div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
-</div>
-<script>
- window.setup_documentation_page({{ .Props }});
-</script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/find_team.html b/web/templates/find_team.html
deleted file mode 100644
index b7e1d7eca..000000000
--- a/web/templates/find_team.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{{define "find_team"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="signup-team__container">
- <img class="signup-team-logo" src="/static/images/logo.png" />
- <div id="find-team"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
- window.setup_find_team_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/footer.html b/web/templates/footer.html
deleted file mode 100644
index 5b11328fb..000000000
--- a/web/templates/footer.html
+++ /dev/null
@@ -1,39 +0,0 @@
-{{define "footer"}}
-<div class="footer-pane col-xs-12">
- <div class="col-xs-12">
- <span class="pull-right footer-site-name">{{ .ClientCfg.SiteName }}</span>
- </div>
- <div class="col-xs-12">
- <span class="pull-right footer-link copyright">© 2015 Mattermost, Inc.</span>
- <a id="help_link" class="pull-right footer-link" href="#">{{ .ClientCfg.FooterHelp }}</a>
- <a id="terms_link" class="pull-right footer-link" href="#">{{ .ClientCfg.FooterTerms }}</a>
- <a id="privacy_link" class="pull-right footer-link" href="#">{{ .ClientCfg.FooterPrivacy }}</a>
- <a id="about_link" class="pull-right footer-link" href="#">{{ .ClientCfg.FooterAbout }}</a>
- </div>
-</div>
-<script>
- if (window.mm_config.HelpLink) {
- document.getElementById("help_link").setAttribute("href", window.mm_config.HelpLink);
- } else {
- $("#help_link").remove();
- }
-
- if (window.mm_config.TermsOfServiceLink) {
- document.getElementById("terms_link").setAttribute("href", window.mm_config.TermsOfServiceLink);
- } else {
- $("#terms_link").remove();
- }
-
- if (window.mm_config.PrivacyPolicyLink) {
- document.getElementById("privacy_link").setAttribute("href", window.mm_config.PrivacyPolicyLink);
- } else {
- $("#privacy_link").remove();
- }
-
- if (window.mm_config.AboutLink) {
- document.getElementById("about_link").setAttribute("href", window.mm_config.AboutLink);
- } else {
- $("#about_link").remove();
- }
-</script>
-{{end}}
diff --git a/web/templates/head.html b/web/templates/head.html
deleted file mode 100644
index 61b1aa12b..000000000
--- a/web/templates/head.html
+++ /dev/null
@@ -1,191 +0,0 @@
-{{define "head"}}
-<head>
- <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
- <meta name="robots" content="noindex, nofollow">
- <meta name="referrer" content="no-referrer">
-
- <title>{{ .Props.Title }}</title>
-
- <!-- iOS add to homescreen -->
- <meta name="apple-mobile-web-app-capable" content="yes" />
- <meta name="apple-mobile-web-app-status-bar-style" content="default">
- <meta name="mobile-web-app-capable" content="yes" />
- <meta name="apple-mobile-web-app-title" content="{{ .Props.Title }}">
- <meta name="application-name" content="{{ .Props.Title }}">
- <meta name="format-detection" content="telephone=no">
- <!-- iOS add to homescreen -->
-
- <!-- Android add to homescreen -->
- <link rel="apple-touch-icon" sizes="57x57" href="/static/images/favicon/apple-touch-icon-57x57.png">
- <link rel="apple-touch-icon" sizes="60x60" href="/static/images/favicon/apple-touch-icon-60x60.png">
- <link rel="apple-touch-icon" sizes="72x72" href="/static/images/favicon/apple-touch-icon-72x72.png">
- <link rel="apple-touch-icon" sizes="76x76" href="/static/images/favicon/apple-touch-icon-76x76.png">
- <link rel="apple-touch-icon" sizes="114x114" href="/static/images/favicon/apple-touch-icon-114x114.png">
- <link rel="apple-touch-icon" sizes="120x120" href="/static/images/favicon/apple-touch-icon-120x120.png">
- <link rel="apple-touch-icon" sizes="144x144" href="/static/images/favicon/apple-touch-icon-144x144.png">
- <link rel="apple-touch-icon" sizes="152x152" href="/static/images/favicon/apple-touch-icon-152x152.png">
- <link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicon/apple-touch-icon-180x180.png">
- <link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon/favicon-32x32.png">
- <link rel="icon" type="image/png" sizes="192x192" href="/static/images/favicon/android-chrome-192x192.png">
- <link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon/favicon-96x96.png">
- <link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon/favicon-16x16.png">
- <link rel="manifest" href="/static/config/manifest.json">
- <!-- Android add to homescreen -->
-
- <!-- CSS Should always go first -->
- <link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
- <link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css">
- <link rel="stylesheet" href="/static/css/bootstrap-colorpicker.min.css">
- <link rel="stylesheet" href="/static/css/styles.css">
- <link rel="stylesheet" href="/static/css/google-fonts.css">
- <link rel="stylesheet" href="/static/css/katex.min.css">
- <link rel="stylesheet" class="code_theme" href="">
-
- <script src="/static/js/intl-1.0.0/Intl.js"></script>
- <script src="/static/js/intl-1.0.0/locale-data/jsonp/en.js"></script>
- <script src="/static/js/intl-1.0.0/locale-data/jsonp/es.js"></script>
- <script src="/static/js/intl-1.0.0/locale-data/jsonp/pt.js"></script>
-
- <script src="/static/js/react-0.14.3.js"></script>
- <script src="/static/js/react-dom-0.14.3.js"></script>
- <script src="/static/js/react-intl-2.0.0-beta-2/react-intl.js"></script>
- <script src="/static/js/react-intl-2.0.0-beta-2/locale-data/en.js"></script>
- <script src="/static/js/react-intl-2.0.0-beta-2/locale-data/es.js"></script>
- <script src="/static/js/react-intl-2.0.0-beta-2/locale-data/pt.js"></script>
- <script src="/static/js/jquery-2.1.4.js"></script>
- <script src="/static/js/bootstrap-3.3.5.js"></script>
- <script src="/static/js/bootstrap-colorpicker.min.js"></script>
- <script src="/static/js/react-bootstrap-0.28.1.js"></script>
- <script src="/static/js/velocity.min.js"></script>
- <script src="/static/js/perfect-scrollbar-0.6.7.jquery.min.js"></script>
- <script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
- <script src="/static/js/babel-polyfill-6.1.18.min.js"></script>
- <script src="/static/js/katex.min.js"></script>
-
- <style id="antiClickjack">body{display:none !important;}</style>
-
- <script>
- if ('ReactIntl' in window && 'ReactIntlLocaleData' in window) {
- Object.keys(ReactIntlLocaleData).forEach(function(lang) {
- ReactIntl.addLocaleData(ReactIntlLocaleData[lang]);
- });
- }
-
- window.mm_config = {{ .ClientCfg }};
- window.mm_license = {{ .ClientLicense }};
- window.mm_team = {{ .Team }};
- window.mm_user = {{ .User }};
- window.mm_channel = {{ .Channel }};
- window.mm_locale = {{ .Locale }};
- window.mm_preferences = {{ .Preferences }};
-
- $(function() {
- if (window.mm_preferences != null) {
- PreferenceStore.setPreferences(window.mm_preferences);
- PreferenceStore.emitChange();
- }
- });
-
- if ({{.SessionTokenIndex}} >= 0) {
- window.mm_session_token_index = {{.SessionTokenIndex}};
- $.ajaxSetup({
- headers: {
- 'X-MM-TokenIndex': mm_session_token_index,
- 'Accept-Language': mm_locale
- }
- });
- } else {
- $.ajaxSetup({
- headers: {
- 'Accept-Language': mm_locale
- }
- });
- }
-
- $(function () {
- $(window).bind('storage', function (e) {
- // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
- if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
- // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
- if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
- return;
- }
-
- console.log('detected logout from a different tab');
- window.location.href = '/' + window.mm_team.name;
- }
-
- if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
- // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
- if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
- return;
- }
-
- console.log('detected login from a different tab');
- location.reload();
- }
- });
- });
-
- $(window).on('beforeunload', function(){
- if (window.SocketStore) {
- SocketStore.close();
- }
- });
- </script>
-
- <script>
- window.onerror = function(msg, url, line, column, stack) {
- var l = {};
- l.level = 'ERROR';
- l.message = 'msg: ' + msg + ' row: ' + line + ' col: ' + column + ' stack: ' + stack + ' url: ' + url;
-
- $.ajax({
- url: '/api/v1/admin/log_client',
- dataType: 'json',
- contentType: 'application/json',
- type: 'POST',
- data: JSON.stringify(l)
- });
-
- if (window.mm_config.EnableDeveloper === 'true') {
- window.ErrorStore.storeLastError({message: 'DEVELOPER MODE: A javascript error has occured. Please use the javascript console to capture and report the error (row: ' + line + ' col: ' + column + ').'});
- window.ErrorStore.emitChange();
- }
- }
- </script>
-
- <script src="/static/js/libs.min.js"></script>
- <script src="/static/js/bundle.js"></script>
-
- <script type="text/javascript">
- if (self === top) {
- var blocker = document.getElementById("antiClickjack");
- blocker.parentNode.removeChild(blocker);
- }
- </script>
-
- <script type="text/javascript">
- if (window.mm_config.SegmentDeveloperKey != null && window.mm_config.SegmentDeveloperKey !== "") {
- !function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1";
- analytics.load(window.mm_config.SegmentDeveloperKey);
- if (window.mm_user) {
- analytics.identify(window.mm_user.id, {
- name: window.mm_user.nickname,
- email: window.mm_user.email,
- createdAt: window.mm_user.create_at,
- username: window.mm_user.username,
- team_id: window.mm_user.team_id,
- id: window.mm_user.id
- });
- }
- analytics.page();
- }}();
- } else {
- analytics = {};
- analytics.page = function(){};
- analytics.track = function(){};
- }
- </script>
-</head>
-{{end}}
diff --git a/web/templates/home.html b/web/templates/home.html
deleted file mode 100644
index 08876d41d..000000000
--- a/web/templates/home.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{{define "home"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body>
- <div class="container-fluid">
- <div class="sidebar--right" id="sidebar-right"></div>
- <div class="sidebar--left" id="sidebar-left"></div>
- <div class="inner__wrap">
- <div class="row header">
- <div id="navbar"></div>
- </div>
- <div class="row main">
- <div class="hidden-xs" id="sidebar"></div>
- <div class="app__content"></div>
- </div>
- </div>
- </div>
- <script>
- window.setup_home_page();
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/login.html b/web/templates/login.html
deleted file mode 100644
index 88540a906..000000000
--- a/web/templates/login.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{{define "login"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div id="login"></div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
- window.setup_login_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/password_reset.html b/web/templates/password_reset.html
deleted file mode 100644
index e68f8b693..000000000
--- a/web/templates/password_reset.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{{define "password_reset"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="signup-team__container">
- <img class="signup-team-logo" src="/static/images/logo.png" />
- <div id="reset"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
- window.setup_password_reset_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/signup_team.html b/web/templates/signup_team.html
deleted file mode 100644
index afba58066..000000000
--- a/web/templates/signup_team.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{{define "signup_team"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="col-sm-12">
- <div class="signup-team__container">
- <img class="signup-team-logo" src="/static/images/logo.png" />
- <h1>{{ .ClientCfg.SiteName }}</h1>
- <h4 class="color--light">{{.Props.Info}}</h4>
- <div id="signup-team"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
-window.setup_signup_team_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/signup_team_complete.html b/web/templates/signup_team_complete.html
deleted file mode 100644
index 3873d8978..000000000
--- a/web/templates/signup_team_complete.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{{define "signup_team_complete"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="signup-team__container">
- <div id="signup-team-complete"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
-window.setup_signup_team_complete_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/signup_team_confirm.html b/web/templates/signup_team_confirm.html
deleted file mode 100644
index 31f1ba95b..000000000
--- a/web/templates/signup_team_confirm.html
+++ /dev/null
@@ -1,26 +0,0 @@
-{{define "signup_team_confirm"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div id="signup-team-confirm"></div>
- </div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
-window.setup_signup_team_confirm_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/signup_user_complete.html b/web/templates/signup_user_complete.html
deleted file mode 100644
index 937a89dd2..000000000
--- a/web/templates/signup_user_complete.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{{define "signup_user_complete"}}
-<!DOCTYPE html>
-<html>
-{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="signup-team__container padding--less">
- <div id="signup-user-complete"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
- window.setup_signup_user_complete_page({{ .Props }});
- </script>
-</body>
-</html>
-{{end}}
diff --git a/web/templates/verify.html b/web/templates/verify.html
deleted file mode 100644
index 2e5496d7a..000000000
--- a/web/templates/verify.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{{define "verify"}}
-<!DOCTYPE html>
-<html>
- {{template "head" . }}
- <body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- <a href="/{{.Props.TeamName}}"><span class='fa fa-chevron-left'></span>{{ .ClientCfg.HeaderBack }}</a>
- </div>
- <div class="col-sm-12">
- <div class="signup-team__container">
- <img class="signup-team-logo" src="/static/images/logo.png" />
- <div id="verify"></div>
- </div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
- window.setupVerifyPage({{ .Props }});
- </script>
- </body>
-</html>
-{{end}}
diff --git a/web/web.go b/web/web.go
index 09450b976..2a44ece00 100644
--- a/web/web.go
+++ b/web/web.go
@@ -4,67 +4,16 @@
package web
import (
- "fmt"
+ "net/http"
+ "strings"
+
l4g "github.com/alecthomas/log4go"
- "github.com/gorilla/mux"
"github.com/mattermost/platform/api"
"github.com/mattermost/platform/model"
- "github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
"github.com/mssola/user_agent"
- "gopkg.in/fsnotify.v1"
- "html/template"
- "net/http"
- "net/url"
- "strconv"
- "strings"
)
-var Templates *template.Template
-
-type HtmlTemplatePage api.Page
-
-func NewHtmlTemplatePage(templateName string, title string, locale string) *HtmlTemplatePage {
-
- if len(title) > 0 {
- title = utils.Cfg.TeamSettings.SiteName + " - " + title
- }
-
- props := make(map[string]string)
- props["Title"] = title
- return &HtmlTemplatePage{
- TemplateName: templateName,
- Props: props,
- ClientCfg: utils.ClientCfg,
- ClientLicense: utils.ClientLicense,
- Locale: locale,
- }
-}
-
-func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) {
- if me.Team != nil {
- me.Team.Sanitize()
- }
-
- if me.User != nil {
- me.User.Sanitize(map[string]bool{})
- me.Locale = me.User.Locale
- }
-
- me.Props["Locale"] = me.Locale
- me.SessionTokenIndex = c.SessionTokenIndex
-
- me.ClientCfg["HeaderBack"] = c.T("web.header.back")
- me.ClientCfg["FooterHelp"] = c.T("web.footer.help")
- me.ClientCfg["FooterTerms"] = c.T("web.footer.terms")
- me.ClientCfg["FooterPrivacy"] = c.T("web.footer.privacy")
- me.ClientCfg["FooterAbout"] = c.T("web.footer.about")
-
- if err := Templates.ExecuteTemplate(w, me.TemplateName, me); err != nil {
- c.SetUnknownError(me.TemplateName, err.Error())
- }
-}
-
func InitWeb() {
l4g.Debug(utils.T("web.init.debug"))
@@ -74,81 +23,7 @@ func InitWeb() {
l4g.Debug("Using static directory at %v", staticDir)
mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))))
- mainrouter.Handle("/", api.AppHandlerIndependent(root)).Methods("GET")
- mainrouter.Handle("/oauth/authorize", api.UserRequired(authorizeOAuth)).Methods("GET")
- mainrouter.Handle("/oauth/access_token", api.ApiAppHandler(getAccessToken)).Methods("POST")
-
- mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET")
- mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET")
- mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET")
- mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET")
- mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET")
- mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET")
- mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8)
- mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8)
- mainrouter.Handle("/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET")
-
- mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET")
- mainrouter.Handle("/admin_console/", api.UserRequired(adminConsole)).Methods("GET")
- mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}", api.UserRequired(adminConsole)).Methods("GET")
- mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}/{team:[A-Za-z0-9-]*}", api.UserRequired(adminConsole)).Methods("GET")
-
- mainrouter.Handle("/hooks/{id:[A-Za-z0-9]+}", api.ApiAppHandler(incomingWebhook)).Methods("POST")
-
- mainrouter.Handle("/docs/{doc:[A-Za-z0-9]+}", api.AppHandlerIndependent(docs)).Methods("GET")
-
- // ----------------------------------------------------------------------------------------------
- // *ANYTHING* team specific should go below this line
- // ----------------------------------------------------------------------------------------------
-
- mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET")
- mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET")
- mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET")
- mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET")
- mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET")
- mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/claim", api.AppHandler(claimAccount)).Methods("GET")
- mainrouter.Handle("/{team}/pl/{postid}", api.AppHandler(postPermalink)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
- mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
- mainrouter.Handle("/{team}/channels/{channelname}", api.AppHandler(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
- mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
-
- watchAndParseTemplates()
-}
-
-func watchAndParseTemplates() {
-
- templatesDir := utils.FindDir("web/templates")
- l4g.Debug(utils.T("web.parsing_templates.debug"), templatesDir)
- var err error
- if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil {
- l4g.Error(utils.T("web.parsing_templates.error"), err)
- }
-
- watcher, err := fsnotify.NewWatcher()
- if err != nil {
- l4g.Error(utils.T("web.create_dir.error"), err)
- }
-
- go func() {
- for {
- select {
- case event := <-watcher.Events:
- if event.Op&fsnotify.Write == fsnotify.Write {
- l4g.Info(utils.T("web.reparse_templates.info"), event.Name)
- if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil {
- l4g.Error(utils.T("web.parsing_templates.error"), err)
- }
- }
- case err := <-watcher.Errors:
- l4g.Error(utils.T("web.dir_fail.error"), err)
- }
- }
- }()
-
- err = watcher.Add(templatesDir)
- if err != nil {
- l4g.Error(utils.T("web.watcher_fail.error"), err)
- }
+ mainrouter.Handle("/{anything:.*}", api.AppHandlerIndependent(root)).Methods("GET")
}
var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7;Safari/8"
@@ -177,1026 +52,9 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) {
return
}
- if len(c.Session.UserId) == 0 {
- page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale)
- page.Props["Info"] = c.T("web.root.singup_info")
-
- if result := <-api.Srv.Store.Team().GetAllTeamListing(); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- teams := result.Data.([]*model.Team)
- for _, team := range teams {
- page.Props[team.Name] = team.DisplayName
- }
-
- if len(teams) == 1 && *utils.Cfg.TeamSettings.EnableTeamListing && !utils.Cfg.TeamSettings.EnableTeamCreation {
- http.Redirect(w, r, c.GetSiteURL()+"/"+teams[0].Name, http.StatusTemporaryRedirect)
- return
- }
- }
-
- page.Render(c, w)
- } else {
- teamChan := api.Srv.Store.Team().Get(c.Session.TeamId)
- userChan := api.Srv.Store.User().Get(c.Session.UserId)
-
- var team *model.Team
- if tr := <-teamChan; tr.Err != nil {
- c.Err = tr.Err
- return
- } else {
- team = tr.Data.(*model.Team)
-
- }
-
- var user *model.User
- if ur := <-userChan; ur.Err != nil {
- c.Err = ur.Err
- return
- } else {
- user = ur.Data.(*model.User)
- }
-
- page := NewHtmlTemplatePage("home", c.T("web.root.home_title"), c.Locale)
- page.Team = team
- page.User = user
- page.Render(c, w)
- }
-}
-
-func signup(c *api.Context, w http.ResponseWriter, r *http.Request) {
-
- if !CheckBrowserCompatability(c, r) {
- return
- }
-
- page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale)
- page.Render(c, w)
-}
-
-func login(c *api.Context, w http.ResponseWriter, r *http.Request) {
- if !CheckBrowserCompatability(c, r) {
- return
- }
- params := mux.Vars(r)
- teamName := params["team"]
-
- var team *model.Team
- if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil {
- l4g.Error(utils.T("web.login.error"), teamName, tResult.Err.Message)
- http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
- return
- } else {
- team = tResult.Data.(*model.Team)
- }
-
- // We still might be able to switch to this team because we've logged in before
- _, session := api.FindMultiSessionForTeamId(r, team.Id)
- if session != nil {
- w.Header().Set(model.HEADER_TOKEN, session.Token)
- lastViewChannelName := "town-square"
- if lastViewResult := <-api.Srv.Store.Preference().Get(session.UserId, model.PREFERENCE_CATEGORY_LAST, model.PREFERENCE_NAME_LAST_CHANNEL); lastViewResult.Err == nil {
- if lastViewChannelResult := <-api.Srv.Store.Channel().Get(lastViewResult.Data.(model.Preference).Value); lastViewChannelResult.Err == nil {
- lastViewChannelName = lastViewChannelResult.Data.(*model.Channel).Name
- }
- }
-
- http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/"+lastViewChannelName, http.StatusTemporaryRedirect)
- return
- }
-
- page := NewHtmlTemplatePage("login", c.T("web.login.login_title"), c.Locale)
- page.Props["TeamDisplayName"] = team.DisplayName
- page.Props["TeamName"] = team.Name
-
- if team.AllowOpenInvite {
- page.Props["InviteId"] = team.InviteId
- }
-
- page.Render(c, w)
-}
-
-func signupTeamConfirm(c *api.Context, w http.ResponseWriter, r *http.Request) {
- email := r.FormValue("email")
-
- page := NewHtmlTemplatePage("signup_team_confirm", c.T("web.signup_team_confirm.title"), c.Locale)
- page.Props["Email"] = email
- page.Render(c, w)
-}
-
-func signupTeamComplete(c *api.Context, w http.ResponseWriter, r *http.Request) {
- data := r.FormValue("d")
- hash := r.FormValue("h")
-
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
- c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.invalid_link.app_error", nil, "")
- return
- }
-
- props := model.MapFromJson(strings.NewReader(data))
-
- t, err := strconv.ParseInt(props["time"], 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60*24*30 { // 30 days
- c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.link_expired.app_error", nil, "")
- return
- }
-
- page := NewHtmlTemplatePage("signup_team_complete", c.T("web.signup_team_complete.title"), c.Locale)
- page.Props["Email"] = props["email"]
- page.Props["Data"] = data
- page.Props["Hash"] = hash
- page.Render(c, w)
-}
-
-func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) {
-
- id := r.FormValue("id")
- data := r.FormValue("d")
- hash := r.FormValue("h")
- var props map[string]string
-
- if len(id) > 0 {
- props = make(map[string]string)
-
- if result := <-api.Srv.Store.Team().GetByInviteId(id); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team := result.Data.(*model.Team)
- if !(team.Type == model.TEAM_OPEN || (team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0)) {
- c.Err = model.NewLocAppError("signupUserComplete", "web.signup_user_complete.no_invites.app_error", nil, "id="+id)
- return
- }
-
- props["email"] = ""
- props["display_name"] = team.DisplayName
- props["name"] = team.Name
- props["id"] = team.Id
- data = model.MapToJson(props)
- hash = ""
- }
- } else {
-
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
- c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_invalid.app_error", nil, "")
- return
- }
-
- props = model.MapFromJson(strings.NewReader(data))
-
- t, err := strconv.ParseInt(props["time"], 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hour
- c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_expired.app_error", nil, "")
- return
- }
- }
-
- page := NewHtmlTemplatePage("signup_user_complete", c.T("web.signup_user_complete.title"), c.Locale)
- page.Props["Email"] = props["email"]
- page.Props["TeamDisplayName"] = props["display_name"]
- page.Props["TeamName"] = props["name"]
- page.Props["TeamId"] = props["id"]
- page.Props["Data"] = data
- page.Props["Hash"] = hash
- page.Render(c, w)
-}
-
-func logout(c *api.Context, w http.ResponseWriter, r *http.Request) {
- api.Logout(c, w, r)
- http.Redirect(w, r, c.GetTeamURL(), http.StatusTemporaryRedirect)
-}
-
-func postPermalink(c *api.Context, w http.ResponseWriter, r *http.Request) {
- params := mux.Vars(r)
- teamName := params["team"]
- postId := params["postid"]
-
- if len(postId) != 26 {
- c.Err = model.NewLocAppError("postPermalink", "web.post_permalink.app_error", nil, "id="+postId)
- return
- }
-
- team := checkSessionSwitch(c, w, r, teamName)
- if team == nil {
- // Error already set by getTeam
- return
- }
-
- var post *model.Post
- if result := <-api.Srv.Store.Post().Get(postId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- postlist := result.Data.(*model.PostList)
- post = postlist.Posts[postlist.Order[0]]
- }
-
- var channel *model.Channel
- if result := <-api.Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- if result.Data.(int64) == 0 {
- if channel = autoJoinChannelId(c, w, r, post.ChannelId); channel == nil {
- http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
- return
- }
- } else {
- if result := <-api.Srv.Store.Channel().Get(post.ChannelId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- channel = result.Data.(*model.Channel)
- }
- }
- }
-
- doLoadChannel(c, w, r, team, channel, post.Id)
-}
-
-func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
- params := mux.Vars(r)
- name := params["channelname"]
- teamName := params["team"]
-
- team := checkSessionSwitch(c, w, r, teamName)
- if team == nil {
- // Error already set by getTeam
- return
- }
-
- var channel *model.Channel
- if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- channelId := result.Data.(string)
- if len(channelId) == 0 {
- if channel = autoJoinChannelName(c, w, r, name); channel == nil {
- http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
- return
- }
- } else {
- if result := <-api.Srv.Store.Channel().Get(channelId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- channel = result.Data.(*model.Channel)
- }
- }
- }
-
- doLoadChannel(c, w, r, team, channel, "")
-}
-
-func autoJoinChannelName(c *api.Context, w http.ResponseWriter, r *http.Request, channelName string) *model.Channel {
- if strings.Index(channelName, "__") > 0 {
- // It's a direct message channel that doesn't exist yet so let's create it
- ids := strings.Split(channelName, "__")
- otherUserId := ""
- if ids[0] == c.Session.UserId {
- otherUserId = ids[1]
- } else {
- otherUserId = ids[0]
- }
-
- if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil {
- api.Handle404(w, r)
- return nil
- } else {
- return sc
- }
- } else {
- // We will attempt to auto-join open channels
- return joinOpenChannel(c, w, r, api.Srv.Store.Channel().GetByName(c.Session.TeamId, channelName))
- }
-
- return nil
-}
-
-func autoJoinChannelId(c *api.Context, w http.ResponseWriter, r *http.Request, channelId string) *model.Channel {
- return joinOpenChannel(c, w, r, api.Srv.Store.Channel().Get(channelId))
-}
-
-func joinOpenChannel(c *api.Context, w http.ResponseWriter, r *http.Request, channel store.StoreChannel) *model.Channel {
- if cr := <-channel; cr.Err != nil {
- http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
- return nil
- } else {
- channel := cr.Data.(*model.Channel)
- if channel.Type == model.CHANNEL_OPEN {
- api.JoinChannel(c, channel.Id, "")
- if c.Err != nil {
- return nil
- }
- } else {
- http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
- return nil
- }
- return channel
- }
-}
-
-func checkSessionSwitch(c *api.Context, w http.ResponseWriter, r *http.Request, teamName string) *model.Team {
- var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
- c.Err = result.Err
- return nil
- } else {
- team = result.Data.(*model.Team)
- }
-
- // We are logged into a different team. Lets see if we have another
- // session in the cookie that will give us access.
- if c.Session.TeamId != team.Id {
- index, session := api.FindMultiSessionForTeamId(r, team.Id)
- if session == nil {
- // redirect to login
- http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/?redirect="+url.QueryEscape(r.URL.Path), http.StatusTemporaryRedirect)
- } else {
- c.Session = *session
- c.SessionTokenIndex = index
- }
- }
-
- return team
-}
-
-func doLoadChannel(c *api.Context, w http.ResponseWriter, r *http.Request, team *model.Team, channel *model.Channel, postid string) {
- userChan := api.Srv.Store.User().Get(c.Session.UserId)
- prefChan := api.Srv.Store.Preference().GetAll(c.Session.UserId)
-
- var user *model.User
- if ur := <-userChan; ur.Err != nil {
- c.Err = ur.Err
- c.RemoveSessionCookie(w, r)
- l4g.Error(utils.T("web.do_load_channel.error"), c.Session.UserId)
- return
- } else {
- user = ur.Data.(*model.User)
- }
-
- var preferences model.Preferences
- if result := <-prefChan; result.Err != nil {
- l4g.Error("Error in getting preferences for id=%v", c.Session.UserId)
- } else {
- preferences = result.Data.(model.Preferences)
- }
-
- page := NewHtmlTemplatePage("channel", "", c.Locale)
- page.Props["Title"] = channel.DisplayName + " - " + team.DisplayName + " " + page.ClientCfg["SiteName"]
- page.Props["TeamDisplayName"] = team.DisplayName
- page.Props["ChannelName"] = channel.Name
- page.Props["ChannelId"] = channel.Id
- page.Props["PostId"] = postid
- page.Team = team
- page.User = user
- page.Channel = channel
- page.Preferences = &preferences
- page.Render(c, w)
-}
-
-func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) {
- resend := r.URL.Query().Get("resend")
- resendSuccess := r.URL.Query().Get("resend_success")
- name := r.URL.Query().Get("teamname")
- email := r.URL.Query().Get("email")
- hashedId := r.URL.Query().Get("hid")
- userId := r.URL.Query().Get("uid")
-
- var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(name); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team = result.Data.(*model.Team)
- }
-
- if resend == "true" {
- if result := <-api.Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- user := result.Data.(*model.User)
-
- if user.LastActivityAt > 0 {
- api.SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
- } else {
- api.SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
- }
-
- newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1)
- http.Redirect(w, r, newAddress, http.StatusFound)
- return
- }
- }
-
- if len(userId) == 26 && len(hashedId) != 0 && model.ComparePassword(hashedId, userId) {
- if c.Err = (<-api.Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil {
- return
- } else {
- c.LogAudit("Email Verified")
- http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?extra=verified&email="+url.QueryEscape(email), http.StatusTemporaryRedirect)
- return
- }
- }
-
- page := NewHtmlTemplatePage("verify", c.T("web.email_verified.title"), c.Locale)
- page.Props["TeamURL"] = c.GetTeamURLFromTeam(team)
- page.Props["UserEmail"] = email
- page.Props["ResendSuccess"] = resendSuccess
- page.Render(c, w)
-}
-
-func findTeam(c *api.Context, w http.ResponseWriter, r *http.Request) {
- page := NewHtmlTemplatePage("find_team", c.T("web.find_team.title"), c.Locale)
- page.Render(c, w)
-}
-
-func docs(c *api.Context, w http.ResponseWriter, r *http.Request) {
- params := mux.Vars(r)
- doc := params["doc"]
-
- var user *model.User
- if len(c.Session.UserId) != 0 {
- userChan := api.Srv.Store.User().Get(c.Session.UserId)
- if userChan := <-userChan; userChan.Err == nil {
- user = userChan.Data.(*model.User)
- }
- }
-
- page := NewHtmlTemplatePage("docs", c.T("web.doc.title"), c.Locale)
- page.Props["Site"] = doc
- page.User = user
- page.Render(c, w)
-}
-
-func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) {
- isResetLink := true
- hash := r.URL.Query().Get("h")
- data := r.URL.Query().Get("d")
- params := mux.Vars(r)
- teamName := params["team"]
-
- if len(hash) == 0 || len(data) == 0 {
- isResetLink = false
- } else {
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) {
- c.Err = model.NewLocAppError("resetPassword", "web.reset_password.invalid_link.app_error", nil, "")
- return
- }
-
- props := model.MapFromJson(strings.NewReader(data))
-
- t, err := strconv.ParseInt(props["time"], 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour
- c.Err = model.NewLocAppError("resetPassword", "web.reset_password.expired_link.app_error", nil, "")
- return
- }
- }
-
- teamDisplayName := "Developer/Beta"
- var team *model.Team
- if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil {
- c.Err = tResult.Err
- return
- } else {
- team = tResult.Data.(*model.Team)
- }
-
- if team != nil {
- teamDisplayName = team.DisplayName
- }
-
- page := NewHtmlTemplatePage("password_reset", "", c.Locale)
- page.Props["Title"] = "Reset Password " + page.ClientCfg["SiteName"]
- page.Props["TeamDisplayName"] = teamDisplayName
- page.Props["TeamName"] = teamName
- page.Props["Hash"] = hash
- page.Props["Data"] = data
- page.Props["TeamName"] = teamName
- page.Props["IsReset"] = strconv.FormatBool(isResetLink)
- page.Render(c, w)
-}
-
-func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
- params := mux.Vars(r)
- service := params["service"]
- teamName := params["team"]
-
- if !utils.Cfg.TeamSettings.EnableUserCreation {
- c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "")
- c.Err.StatusCode = http.StatusNotImplemented
- return
- }
-
- if len(teamName) == 0 {
- c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName)
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
-
- hash := r.URL.Query().Get("h")
-
- var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team = result.Data.(*model.Team)
- }
-
- if api.IsVerifyHashRequired(nil, team, hash) {
- data := r.URL.Query().Get("d")
- props := model.MapFromJson(strings.NewReader(data))
-
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
- c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "")
- return
- }
-
- t, err := strconv.ParseInt(props["time"], 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
- c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "")
- return
- }
-
- if team.Id != props["id"] {
- c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data)
- return
- }
- }
-
- stateProps := map[string]string{}
- stateProps["action"] = model.OAUTH_ACTION_SIGNUP
-
- if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil {
- c.Err = err
- return
- } else {
- http.Redirect(w, r, authUrl, http.StatusFound)
- }
-}
-
-func completeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
- params := mux.Vars(r)
- service := params["service"]
-
- code := r.URL.Query().Get("code")
- state := r.URL.Query().Get("state")
-
- uri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8)
-
- if body, team, props, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil {
- c.Err = err
- return
- } else {
- action := props["action"]
- switch action {
- case model.OAUTH_ACTION_SIGNUP:
- api.CreateOAuthUser(c, w, r, service, body, team)
- if c.Err == nil {
- root(c, w, r)
- }
- break
- case model.OAUTH_ACTION_LOGIN:
- api.LoginByOAuth(c, w, r, service, body, team)
- if c.Err == nil {
- root(c, w, r)
- }
- break
- case model.OAUTH_ACTION_EMAIL_TO_SSO:
- api.CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"])
- if c.Err == nil {
- http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect)
- }
- break
- case model.OAUTH_ACTION_SSO_TO_EMAIL:
- api.LoginByOAuth(c, w, r, service, body, team)
- if c.Err == nil {
- http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect)
- }
- break
- default:
- api.LoginByOAuth(c, w, r, service, body, team)
- if c.Err == nil {
- root(c, w, r)
- }
- break
- }
- }
-}
-
-func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
- params := mux.Vars(r)
- service := params["service"]
- teamName := params["team"]
- loginHint := r.URL.Query().Get("login_hint")
-
- if len(teamName) == 0 {
- c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName)
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
-
- // Make sure team exists
- if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
- c.Err = result.Err
- return
- }
-
- stateProps := map[string]string{}
- stateProps["action"] = model.OAUTH_ACTION_LOGIN
-
- if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil {
- c.Err = err
- return
- } else {
- http.Redirect(w, r, authUrl, http.StatusFound)
- }
-}
-
-func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) {
-
- if !c.HasSystemAdminPermissions("adminConsole") {
- return
- }
-
- teamChan := api.Srv.Store.Team().Get(c.Session.TeamId)
- userChan := api.Srv.Store.User().Get(c.Session.UserId)
-
- var team *model.Team
- if tr := <-teamChan; tr.Err != nil {
- c.Err = tr.Err
- return
- } else {
- team = tr.Data.(*model.Team)
-
- }
-
- var user *model.User
- if ur := <-userChan; ur.Err != nil {
- c.Err = ur.Err
- return
- } else {
- user = ur.Data.(*model.User)
- }
-
- params := mux.Vars(r)
- activeTab := params["tab"]
- teamId := params["team"]
-
- page := NewHtmlTemplatePage("admin_console", c.T("web.admin_console.title"), c.Locale)
- page.User = user
- page.Team = team
- page.Props["ActiveTab"] = activeTab
- page.Props["TeamId"] = teamId
- page.Render(c, w)
-}
-
-func authorizeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
- if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
- c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "")
- c.Err.StatusCode = http.StatusNotImplemented
- return
- }
-
- if !CheckBrowserCompatability(c, r) {
- return
- }
-
- responseType := r.URL.Query().Get("response_type")
- clientId := r.URL.Query().Get("client_id")
- redirect := r.URL.Query().Get("redirect_uri")
- scope := r.URL.Query().Get("scope")
- state := r.URL.Query().Get("state")
-
- if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 {
- c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "")
- return
- }
-
- var app *model.OAuthApp
- if result := <-api.Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- app = result.Data.(*model.OAuthApp)
- }
-
- var team *model.Team
- if result := <-api.Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team = result.Data.(*model.Team)
+ page := utils.NewHTMLTemplate("root", c.Locale)
+ page.Props["Title"] = c.T("web.root.home_title")
+ if err := page.RenderToWriter(w); err != nil {
+ c.SetUnknownError(page.TemplateName, err.Error())
}
-
- page := NewHtmlTemplatePage("authorize", c.T("web.authorize_oauth.title"), c.Locale)
- page.Props["TeamName"] = team.Name
- page.Props["AppName"] = app.Name
- page.Props["ResponseType"] = responseType
- page.Props["ClientId"] = clientId
- page.Props["RedirectUri"] = redirect
- page.Props["Scope"] = scope
- page.Props["State"] = state
- page.Render(c, w)
-}
-
-func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) {
- if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "")
- c.Err.StatusCode = http.StatusNotImplemented
- return
- }
-
- c.LogAudit("attempt")
-
- r.ParseForm()
-
- grantType := r.FormValue("grant_type")
- if grantType != model.ACCESS_TOKEN_GRANT_TYPE {
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "")
- return
- }
-
- clientId := r.FormValue("client_id")
- if len(clientId) != 26 {
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "")
- return
- }
-
- secret := r.FormValue("client_secret")
- if len(secret) == 0 {
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "")
- return
- }
-
- code := r.FormValue("code")
- if len(code) == 0 {
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "")
- return
- }
-
- redirectUri := r.FormValue("redirect_uri")
-
- achan := api.Srv.Store.OAuth().GetApp(clientId)
- tchan := api.Srv.Store.OAuth().GetAccessDataByAuthCode(code)
-
- authData := api.GetAuthData(code)
-
- if authData == nil {
- c.LogAudit("fail - invalid auth code")
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
- return
- }
-
- uchan := api.Srv.Store.User().Get(authData.UserId)
-
- if authData.IsExpired() {
- c.LogAudit("fail - auth code expired")
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
- return
- }
-
- if authData.RedirectUri != redirectUri {
- c.LogAudit("fail - redirect uri provided did not match previous redirect uri")
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "")
- return
- }
-
- if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) {
- c.LogAudit("fail - auth code is invalid")
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
- return
- }
-
- var app *model.OAuthApp
- if result := <-achan; result.Err != nil {
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "")
- return
- } else {
- app = result.Data.(*model.OAuthApp)
- }
-
- if !model.ComparePassword(app.ClientSecret, secret) {
- c.LogAudit("fail - invalid client credentials")
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "")
- return
- }
-
- callback := redirectUri
- if len(callback) == 0 {
- callback = app.CallbackUrls[0]
- }
-
- if result := <-tchan; result.Err != nil {
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "")
- return
- } else if result.Data != nil {
- c.LogAudit("fail - auth code has been used previously")
- accessData := result.Data.(*model.AccessData)
-
- // Revoke access token, related auth code, and session from DB as well as from cache
- if err := api.RevokeAccessToken(accessData.Token); err != nil {
- l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message)
- }
-
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "")
- return
- }
-
- var user *model.User
- if result := <-uchan; result.Err != nil {
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "")
- return
- } else {
- user = result.Data.(*model.User)
- }
-
- session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true}
-
- if result := <-api.Srv.Store.Session().Save(session); result.Err != nil {
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "")
- return
- } else {
- session = result.Data.(*model.Session)
- api.AddSessionToCache(session)
- }
-
- accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback}
-
- if result := <-api.Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil {
- l4g.Error(result.Err)
- c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "")
- return
- }
-
- accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)}
-
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("Cache-Control", "no-store")
- w.Header().Set("Pragma", "no-cache")
-
- c.LogAuditWithUserId(user.Id, "success")
-
- w.Write([]byte(accessRsp.ToJson()))
-}
-
-func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
- if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.disabled.app_error", nil, "")
- c.Err.StatusCode = http.StatusNotImplemented
- return
- }
-
- params := mux.Vars(r)
- id := params["id"]
-
- hchan := api.Srv.Store.Webhook().GetIncoming(id)
-
- r.ParseForm()
-
- var parsedRequest *model.IncomingWebhookRequest
- contentType := r.Header.Get("Content-Type")
- if strings.Split(contentType, "; ")[0] == "application/json" {
- parsedRequest = model.IncomingWebhookRequestFromJson(r.Body)
- } else {
- parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload")))
- }
-
- if parsedRequest == nil {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.parse.app_error", nil, "")
- return
- }
-
- text := parsedRequest.Text
- if len(text) == 0 && parsedRequest.Attachments == nil {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.text.app_error", nil, "")
- return
- }
-
- channelName := parsedRequest.ChannelName
- webhookType := parsedRequest.Type
-
- //attachments is in here for slack compatibility
- if parsedRequest.Attachments != nil {
- if len(parsedRequest.Props) == 0 {
- parsedRequest.Props = make(model.StringInterface)
- }
- parsedRequest.Props["attachments"] = parsedRequest.Attachments
- webhookType = model.POST_SLACK_ATTACHMENT
- }
-
- var hook *model.IncomingWebhook
- if result := <-hchan; result.Err != nil {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.invalid.app_error", nil, "err="+result.Err.Message)
- return
- } else {
- hook = result.Data.(*model.IncomingWebhook)
- }
-
- var channel *model.Channel
- var cchan store.StoreChannel
-
- if len(channelName) != 0 {
- if channelName[0] == '@' {
- if result := <-api.Srv.Store.User().GetByUsername(hook.TeamId, channelName[1:]); result.Err != nil {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.user.app_error", nil, "err="+result.Err.Message)
- return
- } else {
- channelName = model.GetDMNameFromIds(result.Data.(*model.User).Id, hook.UserId)
- }
- } else if channelName[0] == '#' {
- channelName = channelName[1:]
- }
-
- cchan = api.Srv.Store.Channel().GetByName(hook.TeamId, channelName)
- } else {
- cchan = api.Srv.Store.Channel().Get(hook.ChannelId)
- }
-
- overrideUsername := parsedRequest.Username
- overrideIconUrl := parsedRequest.IconURL
-
- if result := <-cchan; result.Err != nil {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message)
- return
- } else {
- channel = result.Data.(*model.Channel)
- }
-
- pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId)
-
- // create a mock session
- c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false}
-
- if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {
- c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "")
- return
- }
-
- if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil {
- c.Err = err
- return
- }
-
- w.Header().Set("Content-Type", "text/plain")
- w.Write([]byte("ok"))
-}
-
-func claimAccount(c *api.Context, w http.ResponseWriter, r *http.Request) {
- if !CheckBrowserCompatability(c, r) {
- return
- }
-
- params := mux.Vars(r)
- teamName := params["team"]
- email := r.URL.Query().Get("email")
- newType := r.URL.Query().Get("new_type")
-
- var team *model.Team
- if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil {
- l4g.Error(utils.T("web.claim_account.team.error"), teamName, tResult.Err.Message)
- http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
- return
- } else {
- team = tResult.Data.(*model.Team)
- }
-
- authType := ""
- if len(email) != 0 {
- if uResult := <-api.Srv.Store.User().GetByEmail(team.Id, email); uResult.Err != nil {
- l4g.Error(utils.T("web.claim_account.user.error"), team.Id, email, uResult.Err.Message)
- http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
- return
- } else {
- user := uResult.Data.(*model.User)
- authType = user.AuthService
-
- // if user is not logged in to their SSO account, ask them to log in
- if len(authType) != 0 && user.Id != c.Session.UserId {
- stateProps := map[string]string{}
- stateProps["action"] = model.OAUTH_ACTION_SSO_TO_EMAIL
- stateProps["email"] = email
-
- if authUrl, err := api.GetAuthorizationCode(c, authType, team.Name, stateProps, ""); err != nil {
- c.Err = err
- return
- } else {
- http.Redirect(w, r, authUrl, http.StatusFound)
- }
- }
- }
- }
-
- page := NewHtmlTemplatePage("claim_account", c.T("web.claim_account.title"), c.Locale)
- page.Props["Email"] = email
- page.Props["CurrentType"] = authType
- page.Props["NewType"] = newType
- page.Props["TeamDisplayName"] = team.DisplayName
- page.Props["TeamName"] = team.Name
-
- page.Render(c, w)
}