diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles new file mode 100644 index 0000000000..ffd492d491 --- /dev/null +++ b/.eslintignore.errorfiles @@ -0,0 +1,184 @@ +# autogenerated file: run scripts/generate-eslint-error-ignore-file to update. + +src/AddThreepid.js +src/async-components/views/dialogs/EncryptedEventDialog.js +src/autocomplete/AutocompleteProvider.js +src/autocomplete/Autocompleter.js +src/autocomplete/Components.js +src/autocomplete/DuckDuckGoProvider.js +src/autocomplete/EmojiProvider.js +src/autocomplete/RoomProvider.js +src/autocomplete/UserProvider.js +src/Avatar.js +src/BasePlatform.js +src/CallHandler.js +src/component-index.js +src/components/structures/ContextualMenu.js +src/components/structures/CreateRoom.js +src/components/structures/FilePanel.js +src/components/structures/InteractiveAuth.js +src/components/structures/LoggedInView.js +src/components/structures/login/ForgotPassword.js +src/components/structures/login/Login.js +src/components/structures/login/PostRegistration.js +src/components/structures/login/Registration.js +src/components/structures/MessagePanel.js +src/components/structures/NotificationPanel.js +src/components/structures/RoomStatusBar.js +src/components/structures/RoomView.js +src/components/structures/ScrollPanel.js +src/components/structures/TimelinePanel.js +src/components/structures/UploadBar.js +src/components/views/avatars/BaseAvatar.js +src/components/views/avatars/MemberAvatar.js +src/components/views/avatars/RoomAvatar.js +src/components/views/create_room/CreateRoomButton.js +src/components/views/create_room/Presets.js +src/components/views/create_room/RoomAlias.js +src/components/views/dialogs/ChatCreateOrReuseDialog.js +src/components/views/dialogs/ChatInviteDialog.js +src/components/views/dialogs/DeactivateAccountDialog.js +src/components/views/dialogs/InteractiveAuthDialog.js +src/components/views/dialogs/SetMxIdDialog.js +src/components/views/dialogs/UnknownDeviceDialog.js +src/components/views/elements/AccessibleButton.js +src/components/views/elements/ActionButton.js +src/components/views/elements/AddressSelector.js +src/components/views/elements/AddressTile.js +src/components/views/elements/CreateRoomButton.js +src/components/views/elements/DeviceVerifyButtons.js +src/components/views/elements/DirectorySearchBox.js +src/components/views/elements/Dropdown.js +src/components/views/elements/EditableText.js +src/components/views/elements/EditableTextContainer.js +src/components/views/elements/HomeButton.js +src/components/views/elements/LanguageDropdown.js +src/components/views/elements/MemberEventListSummary.js +src/components/views/elements/PowerSelector.js +src/components/views/elements/ProgressBar.js +src/components/views/elements/RoomDirectoryButton.js +src/components/views/elements/SettingsButton.js +src/components/views/elements/StartChatButton.js +src/components/views/elements/TintableSvg.js +src/components/views/elements/TruncatedList.js +src/components/views/elements/UserSelector.js +src/components/views/login/CaptchaForm.js +src/components/views/login/CasLogin.js +src/components/views/login/CountryDropdown.js +src/components/views/login/CustomServerDialog.js +src/components/views/login/InteractiveAuthEntryComponents.js +src/components/views/login/LoginHeader.js +src/components/views/login/PasswordLogin.js +src/components/views/login/RegistrationForm.js +src/components/views/login/ServerConfig.js +src/components/views/messages/MAudioBody.js +src/components/views/messages/MessageEvent.js +src/components/views/messages/MFileBody.js +src/components/views/messages/MImageBody.js +src/components/views/messages/MVideoBody.js +src/components/views/messages/RoomAvatarEvent.js +src/components/views/messages/TextualBody.js +src/components/views/messages/TextualEvent.js +src/components/views/room_settings/AliasSettings.js +src/components/views/room_settings/ColorSettings.js +src/components/views/room_settings/UrlPreviewSettings.js +src/components/views/rooms/Autocomplete.js +src/components/views/rooms/AuxPanel.js +src/components/views/rooms/EntityTile.js +src/components/views/rooms/EventTile.js +src/components/views/rooms/LinkPreviewWidget.js +src/components/views/rooms/MemberDeviceInfo.js +src/components/views/rooms/MemberInfo.js +src/components/views/rooms/MemberList.js +src/components/views/rooms/MemberTile.js +src/components/views/rooms/MessageComposer.js +src/components/views/rooms/MessageComposerInput.js +src/components/views/rooms/MessageComposerInputOld.js +src/components/views/rooms/PresenceLabel.js +src/components/views/rooms/ReadReceiptMarker.js +src/components/views/rooms/RoomHeader.js +src/components/views/rooms/RoomList.js +src/components/views/rooms/RoomNameEditor.js +src/components/views/rooms/RoomPreviewBar.js +src/components/views/rooms/RoomSettings.js +src/components/views/rooms/RoomTile.js +src/components/views/rooms/RoomTopicEditor.js +src/components/views/rooms/SearchableEntityList.js +src/components/views/rooms/SearchResultTile.js +src/components/views/rooms/TabCompleteBar.js +src/components/views/rooms/TopUnreadMessagesBar.js +src/components/views/rooms/UserTile.js +src/components/views/settings/AddPhoneNumber.js +src/components/views/settings/ChangeAvatar.js +src/components/views/settings/ChangeDisplayName.js +src/components/views/settings/ChangePassword.js +src/components/views/settings/DevicesPanel.js +src/components/views/settings/DevicesPanelEntry.js +src/components/views/settings/EnableNotificationsButton.js +src/components/views/voip/CallView.js +src/components/views/voip/IncomingCallBox.js +src/components/views/voip/VideoFeed.js +src/components/views/voip/VideoView.js +src/ContentMessages.js +src/createRoom.js +src/DateUtils.js +src/email.js +src/Entities.js +src/extend.js +src/HtmlUtils.js +src/ImageUtils.js +src/Invite.js +src/languageHandler.js +src/linkify-matrix.js +src/Login.js +src/Markdown.js +src/MatrixClientPeg.js +src/Modal.js +src/Notifier.js +src/ObjectUtils.js +src/PasswordReset.js +src/PlatformPeg.js +src/Presence.js +src/ratelimitedfunc.js +src/Resend.js +src/RichText.js +src/Roles.js +src/RoomListSorter.js +src/RoomNotifs.js +src/Rooms.js +src/ScalarAuthClient.js +src/ScalarMessaging.js +src/SdkConfig.js +src/Skinner.js +src/SlashCommands.js +src/stores/LifecycleStore.js +src/TabComplete.js +src/TabCompleteEntries.js +src/TextForEvent.js +src/Tinter.js +src/UiEffects.js +src/Unread.js +src/UserActivity.js +src/utils/DecryptFile.js +src/utils/DMRoomMap.js +src/utils/FormattingUtils.js +src/utils/MultiInviter.js +src/utils/Receipt.js +src/Velociraptor.js +src/VelocityBounce.js +src/WhoIsTyping.js +src/wrappers/WithMatrixClient.js +test/all-tests.js +test/components/structures/login/Registration-test.js +test/components/structures/MessagePanel-test.js +test/components/structures/ScrollPanel-test.js +test/components/structures/TimelinePanel-test.js +test/components/stub-component.js +test/components/views/dialogs/InteractiveAuthDialog-test.js +test/components/views/elements/MemberEventListSummary-test.js +test/components/views/login/RegistrationForm-test.js +test/components/views/rooms/MessageComposerInput-test.js +test/mock-clock.js +test/skinned-sdk.js +test/stores/RoomViewStore-test.js +test/test-utils.js diff --git a/.travis.yml b/.travis.yml index a405b9ef35..918cec696b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,4 @@ install: - npm install - (cd node_modules/matrix-js-sdk && npm install) script: - # don't run the riot tests unless the react-sdk tests pass, otherwise - # the output is confusing. - - npm run test && ./.travis-test-riot.sh + ./scripts/travis.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 66e4627afd..ed6fb3ba36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,177 @@ +Changes in [0.9.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.4) (2017-06-14) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.3...v0.9.4) + + * Ask for email address after setting password for the first time + [\#1090](https://github.com/matrix-org/matrix-react-sdk/pull/1090) + * DM guessing: prefer oldest joined member + [\#1087](https://github.com/matrix-org/matrix-react-sdk/pull/1087) + * More translations + +Changes in [0.9.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.3) (2017-06-12) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.3-rc.2...v0.9.3) + + * Add more translations & fix some existing ones + +Changes in [0.9.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.3-rc.2) (2017-06-09) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.3-rc.1...v0.9.3-rc.2) + + * Fix flux dependency + * Fix translations on conference call bar + +Changes in [0.9.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.3-rc.1) (2017-06-09) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.2...v0.9.3-rc.1) + + * When ChatCreateOrReuseDialog is cancelled by a guest, go home + [\#1069](https://github.com/matrix-org/matrix-react-sdk/pull/1069) + * Update from Weblate. + [\#1065](https://github.com/matrix-org/matrix-react-sdk/pull/1065) + * Goto /home when forgetting the last room + [\#1067](https://github.com/matrix-org/matrix-react-sdk/pull/1067) + * Default to home page when settings is closed + [\#1066](https://github.com/matrix-org/matrix-react-sdk/pull/1066) + * Update from Weblate. + [\#1063](https://github.com/matrix-org/matrix-react-sdk/pull/1063) + * When joining, use a roomAlias if we have it + [\#1062](https://github.com/matrix-org/matrix-react-sdk/pull/1062) + * Control currently viewed event via RoomViewStore + [\#1058](https://github.com/matrix-org/matrix-react-sdk/pull/1058) + * Better error messages for login + [\#1060](https://github.com/matrix-org/matrix-react-sdk/pull/1060) + * Add remaining translations + [\#1056](https://github.com/matrix-org/matrix-react-sdk/pull/1056) + * Added button that copies code to clipboard + [\#1040](https://github.com/matrix-org/matrix-react-sdk/pull/1040) + * de-lint MegolmExportEncryption + test + [\#1059](https://github.com/matrix-org/matrix-react-sdk/pull/1059) + * Better RTL support + [\#1021](https://github.com/matrix-org/matrix-react-sdk/pull/1021) + * make mels emoji capable + [\#1057](https://github.com/matrix-org/matrix-react-sdk/pull/1057) + * Make travis check for lint on files which are clean to start with + [\#1055](https://github.com/matrix-org/matrix-react-sdk/pull/1055) + * Update from Weblate. + [\#1053](https://github.com/matrix-org/matrix-react-sdk/pull/1053) + * Add some logging around switching rooms + [\#1054](https://github.com/matrix-org/matrix-react-sdk/pull/1054) + * Update from Weblate. + [\#1052](https://github.com/matrix-org/matrix-react-sdk/pull/1052) + * Use user_directory endpoint to populate ChatInviteDialog + [\#1050](https://github.com/matrix-org/matrix-react-sdk/pull/1050) + * Various Analytics changes/fixes/improvements + [\#1046](https://github.com/matrix-org/matrix-react-sdk/pull/1046) + * Use an arrow function to allow `this` + [\#1051](https://github.com/matrix-org/matrix-react-sdk/pull/1051) + * New guest access + [\#937](https://github.com/matrix-org/matrix-react-sdk/pull/937) + * Translate src/components/structures + [\#1048](https://github.com/matrix-org/matrix-react-sdk/pull/1048) + * Cancel 'join room' action if 'log in' is clicked + [\#1049](https://github.com/matrix-org/matrix-react-sdk/pull/1049) + * fix copy and paste derp and rip out unused imports + [\#1015](https://github.com/matrix-org/matrix-react-sdk/pull/1015) + * Update from Weblate. + [\#1042](https://github.com/matrix-org/matrix-react-sdk/pull/1042) + * Reset 'first sync' flag / promise on log in + [\#1041](https://github.com/matrix-org/matrix-react-sdk/pull/1041) + * Remove DM-guessing code (again) + [\#1036](https://github.com/matrix-org/matrix-react-sdk/pull/1036) + * Cancel deferred actions + [\#1039](https://github.com/matrix-org/matrix-react-sdk/pull/1039) + * Merge develop, add i18n for SetMxIdDialog + [\#1034](https://github.com/matrix-org/matrix-react-sdk/pull/1034) + * Defer an intention for creating a room + [\#1038](https://github.com/matrix-org/matrix-react-sdk/pull/1038) + * Fix 'create room' button + [\#1037](https://github.com/matrix-org/matrix-react-sdk/pull/1037) + * Always show the spinner during the first sync + [\#1033](https://github.com/matrix-org/matrix-react-sdk/pull/1033) + * Only view welcome user if we are not looking at a room + [\#1032](https://github.com/matrix-org/matrix-react-sdk/pull/1032) + * Update from Weblate. + [\#1030](https://github.com/matrix-org/matrix-react-sdk/pull/1030) + * Keep deferred actions for view_user_settings and view_create_chat + [\#1031](https://github.com/matrix-org/matrix-react-sdk/pull/1031) + * Don't do a deferred start chat if user is welcome user + [\#1029](https://github.com/matrix-org/matrix-react-sdk/pull/1029) + * Introduce state `peekLoading` to avoid collision with `roomLoading` + [\#1028](https://github.com/matrix-org/matrix-react-sdk/pull/1028) + * Update from Weblate. + [\#1016](https://github.com/matrix-org/matrix-react-sdk/pull/1016) + * Fix accepting a 3pid invite + [\#1013](https://github.com/matrix-org/matrix-react-sdk/pull/1013) + * Propagate room join errors to the UI + [\#1007](https://github.com/matrix-org/matrix-react-sdk/pull/1007) + * Implement /user/@userid:domain?action=chat + [\#1006](https://github.com/matrix-org/matrix-react-sdk/pull/1006) + * Show People/Rooms emptySubListTip even when total rooms !== 0 + [\#967](https://github.com/matrix-org/matrix-react-sdk/pull/967) + * Fix to show the correct room + [\#995](https://github.com/matrix-org/matrix-react-sdk/pull/995) + * Remove cachedPassword from localStorage on_logged_out + [\#977](https://github.com/matrix-org/matrix-react-sdk/pull/977) + * Add /start to show the setMxId above HomePage + [\#964](https://github.com/matrix-org/matrix-react-sdk/pull/964) + * Allow pressing Enter to submit setMxId + [\#961](https://github.com/matrix-org/matrix-react-sdk/pull/961) + * add login link to SetMxIdDialog + [\#954](https://github.com/matrix-org/matrix-react-sdk/pull/954) + * Block user settings with view_set_mxid + [\#936](https://github.com/matrix-org/matrix-react-sdk/pull/936) + * Show "Something went wrong!" when errcode undefined + [\#935](https://github.com/matrix-org/matrix-react-sdk/pull/935) + * Reset store state when logging out + [\#930](https://github.com/matrix-org/matrix-react-sdk/pull/930) + * Set the displayname to the mxid once PWLU + [\#933](https://github.com/matrix-org/matrix-react-sdk/pull/933) + * Fix view_next_room, view_previous_room and view_indexed_room + [\#929](https://github.com/matrix-org/matrix-react-sdk/pull/929) + * Use RVS to indicate "joining" when setting a mxid + [\#928](https://github.com/matrix-org/matrix-react-sdk/pull/928) + * Don't show notif nag bar if guest + [\#932](https://github.com/matrix-org/matrix-react-sdk/pull/932) + * Show "Password" instead of "New Password" + [\#927](https://github.com/matrix-org/matrix-react-sdk/pull/927) + * Remove warm-fuzzy after setting mxid + [\#926](https://github.com/matrix-org/matrix-react-sdk/pull/926) + * Allow teamServerConfig to be missing + [\#925](https://github.com/matrix-org/matrix-react-sdk/pull/925) + * Remove GuestWarningBar + [\#923](https://github.com/matrix-org/matrix-react-sdk/pull/923) + * Make left panel better for new users (mk III) + [\#924](https://github.com/matrix-org/matrix-react-sdk/pull/924) + * Implement default welcome page and allow custom URL /w config + [\#922](https://github.com/matrix-org/matrix-react-sdk/pull/922) + * Implement a store for RoomView + [\#921](https://github.com/matrix-org/matrix-react-sdk/pull/921) + * Add prop to toggle whether new password input is autoFocused + [\#915](https://github.com/matrix-org/matrix-react-sdk/pull/915) + * Implement warm-fuzzy success dialog for SetMxIdDialog + [\#905](https://github.com/matrix-org/matrix-react-sdk/pull/905) + * Write some tests for the RTS UI + [\#893](https://github.com/matrix-org/matrix-react-sdk/pull/893) + * Make confirmation optional on ChangePassword + [\#890](https://github.com/matrix-org/matrix-react-sdk/pull/890) + * Remove "Current Password" input if mx_pass exists + [\#881](https://github.com/matrix-org/matrix-react-sdk/pull/881) + * Replace NeedToRegisterDialog /w SetMxIdDialog + [\#889](https://github.com/matrix-org/matrix-react-sdk/pull/889) + * Invite the welcome user after registration if configured + [\#882](https://github.com/matrix-org/matrix-react-sdk/pull/882) + * Prevent ROUs from creating new chats/new rooms + [\#879](https://github.com/matrix-org/matrix-react-sdk/pull/879) + * Redesign mxID chooser, add availability checking + [\#877](https://github.com/matrix-org/matrix-react-sdk/pull/877) + * Show password nag bar when user is PWLU + [\#864](https://github.com/matrix-org/matrix-react-sdk/pull/864) + * fix typo + [\#858](https://github.com/matrix-org/matrix-react-sdk/pull/858) + * Initial implementation: SetDisplayName -> SetMxIdDialog + [\#849](https://github.com/matrix-org/matrix-react-sdk/pull/849) + Changes in [0.9.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.2) (2017-06-06) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.1...v0.9.2) diff --git a/jenkins.sh b/jenkins.sh index 6a77911c27..d9bb62855b 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -21,6 +21,11 @@ npm run test # run eslint npm run lintall -- -f checkstyle -o eslint.xml || true +# re-run the linter, excluding any files known to have errors or warnings. +./node_modules/.bin/eslint --max-warnings 0 \ + --ignore-path .eslintignore.errorfiles \ + src test + # delete the old tarball, if it exists rm -f matrix-react-sdk-*.tgz diff --git a/package.json b/package.json index 15a903c25a..151b6d6170 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.9.2", + "version": "0.9.4", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -57,14 +57,14 @@ "emojione": "2.2.3", "file-saver": "^1.3.3", "filesize": "3.5.6", - "flux": "^2.0.3", + "flux": "2.1.1", "fuse.js": "^2.2.0", "glob": "^5.0.14", "highlight.js": "^8.9.1", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "0.7.10", + "matrix-js-sdk": "0.7.11", "optimist": "^0.6.1", "prop-types": "^15.5.8", "q": "^1.4.1", diff --git a/scripts/copy-i18n.py b/scripts/copy-i18n.py new file mode 100755 index 0000000000..07b1271239 --- /dev/null +++ b/scripts/copy-i18n.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +import json +import sys +import os + +if len(sys.argv) < 3: + print "Usage: %s " % (sys.argv[0],) + print "eg. %s pt_BR.json pt.json" % (sys.argv[0],) + print + print "Adds any translations to that exist in but not " + sys.exit(1) + +srcpath = sys.argv[1] +dstpath = sys.argv[2] +tmppath = dstpath + ".tmp" + +with open(srcpath) as f: + src = json.load(f) + +with open(dstpath) as f: + dst = json.load(f) + +toAdd = {} +for k,v in src.iteritems(): + if k not in dst: + print "Adding %s" % (k,) + toAdd[k] = v + +# don't just json.dumps as we'll probably re-order all the keys (and they're +# not in any given order so we can't just sort_keys). Append them to the end. +with open(dstpath) as ifp: + with open(tmppath, 'w') as ofp: + for line in ifp: + strippedline = line.strip() + if strippedline in ('{', '}'): + ofp.write(line) + elif strippedline.endswith(','): + ofp.write(line) + else: + ofp.write(' '+strippedline+',') + toAddStr = json.dumps(toAdd, indent=4, separators=(',', ': '), ensure_ascii=False, encoding="utf8").strip("{}\n") + ofp.write("\n") + ofp.write(toAddStr.encode('utf8')) + ofp.write("\n") + +os.rename(tmppath, dstpath) diff --git a/scripts/fix-i18n.pl b/scripts/fix-i18n.pl index 247b2b663f..def352463d 100755 --- a/scripts/fix-i18n.pl +++ b/scripts/fix-i18n.pl @@ -61,6 +61,16 @@ You are already in a call. You cannot place VoIP calls in this browser. You cannot place a call with yourself. Your email address does not appear to be associated with a Matrix ID on this Homeserver. +Guest users can't upload files. Please register to upload. +Some of your messages have not been sent. +This room is private or inaccessible to guests. You may be able to join if you register. +Tried to load a specific point in this room's timeline, but was unable to find it. +Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question. +This action cannot be performed by a guest user. Please register to be able to do this. +Tried to load a specific point in this room's timeline, but was unable to find it. +Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question. +You are trying to access %(roomName)s. +You will not be able to undo this change as you are promoting the user to have the same power level as yourself. EOT )]; } @@ -84,7 +94,7 @@ if ($_ =~ m/^(\s+)"(.*?)"(: *)"(.*?)"(,?)$/) { $sub = 1; } - if ($src eq $fixup && $dst !~ /\.$/) { + if ($ARGV !~ /(zh_Hans|zh_Hant|th)\.json$/ && $src eq $fixup && $dst !~ /\.$/) { print STDERR "fixing up dst: $dst\n"; $dst .= '.'; $sub = 1; diff --git a/scripts/generate-eslint-error-ignore-file b/scripts/generate-eslint-error-ignore-file new file mode 100755 index 0000000000..3a635f5a7d --- /dev/null +++ b/scripts/generate-eslint-error-ignore-file @@ -0,0 +1,21 @@ +#!/bin/sh +# +# generates .eslintignore.errorfiles to list the files which have errors in, +# so that they can be ignored in future automated linting. + +out=.eslintignore.errorfiles + +cd `dirname $0`/.. + +echo "generating $out" + +{ + cat < 0) | .filePath' | + sed -e 's/.*matrix-react-sdk\///'; +} > "$out" diff --git a/scripts/travis.sh b/scripts/travis.sh new file mode 100755 index 0000000000..f349b06ad5 --- /dev/null +++ b/scripts/travis.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +set -ex + +npm run test +./.travis-test-riot.sh + +# run the linter, but exclude any files known to have errors or warnings. +./node_modules/.bin/eslint --max-warnings 0 \ + --ignore-path .eslintignore.errorfiles \ + src test diff --git a/src/Analytics.js b/src/Analytics.js index c079011db7..92691da1ea 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -20,9 +20,9 @@ import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; function getRedactedUrl() { - const base = window.location.pathname.split('/').slice(-2).join('/'); const redactedHash = window.location.hash.replace(/#\/(room|user)\/(.+)/, "#/$1/"); - return base + redactedHash; + // hardcoded url to make piwik happy + return 'https://riot.im/app/' + redactedHash; } const customVariables = { @@ -30,6 +30,7 @@ const customVariables = { 'App Version': 2, 'User Type': 3, 'Chosen Language': 4, + 'Instance': 5, }; @@ -55,6 +56,7 @@ class Analytics { * but this is second best, Piwik should not pull anything implicitly. */ disable() { + this.trackEvent('Analytics', 'opt-out'); this.disabled = true; } @@ -86,6 +88,10 @@ class Analytics { this._setVisitVariable('Chosen Language', getCurrentLanguage()); + if (window.location.hostname === 'riot.im') { + this._setVisitVariable('Instance', window.location.pathname); + } + (function() { const g = document.createElement('script'); const s = document.getElementsByTagName('script')[0]; diff --git a/src/CallHandler.js b/src/CallHandler.js index b2ccf65df7..e3fbe9e5e3 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -51,13 +51,14 @@ limitations under the License. * } */ -var MatrixClientPeg = require('./MatrixClientPeg'); -var PlatformPeg = require("./PlatformPeg"); -var Modal = require('./Modal'); -var sdk = require('./index'); +import MatrixClientPeg from './MatrixClientPeg'; +import UserSettingsStore from './UserSettingsStore'; +import PlatformPeg from './PlatformPeg'; +import Modal from './Modal'; +import sdk from './index'; import { _t } from './languageHandler'; -var Matrix = require("matrix-js-sdk"); -var dis = require("./dispatcher"); +import Matrix from 'matrix-js-sdk'; +import dis from './dispatcher'; global.mxCalls = { //room_id: MatrixCall @@ -257,9 +258,9 @@ function _onAction(payload) { } else if (members.length === 2) { console.log("Place %s call in %s", payload.type, payload.room_id); - var call = Matrix.createNewMatrixCall( - MatrixClientPeg.get(), payload.room_id - ); + const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, { + forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false), + }); placeCall(call); } else { // > 2 diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 8af1894c79..aec32092ed 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -345,6 +345,7 @@ export function bodyToHtml(content, highlights, opts) { } safeBody = sanitizeHtml(body, sanitizeHtmlParams); safeBody = unicodeToImage(safeBody); + safeBody = addCodeCopyButton(safeBody); } finally { delete sanitizeHtmlParams.textFilter; @@ -363,6 +364,23 @@ export function bodyToHtml(content, highlights, opts) { return ; } +function addCodeCopyButton(safeBody) { + // Adds 'copy' buttons to pre blocks + // Note that this only manipulates the markup to add the buttons: + // we need to add the event handlers once the nodes are in the DOM + // since we can't save functions in the markup. + // This is done in TextualBody + const el = document.createElement("div"); + el.innerHTML = safeBody; + const codeBlocks = Array.from(el.getElementsByTagName("pre")); + codeBlocks.forEach(p => { + const button = document.createElement("span"); + button.className = "mx_EventTile_copyButton"; + p.appendChild(button); + }); + return el.innerHTML; +} + export function emojifyText(text) { return { __html: unicodeToImage(escape(text)), diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 54014a0166..3733ba1ea5 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -19,6 +19,7 @@ import q from 'q'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; +import createMatrixClient from './utils/createMatrixClient'; import Analytics from './Analytics'; import Notifier from './Notifier'; import UserActivity from './UserActivity'; @@ -34,29 +35,20 @@ import { _t } from './languageHandler'; * Called at startup, to attempt to build a logged-in Matrix session. It tries * a number of things: * - * 0. if it looks like we are in the middle of a registration process, it does - * nothing. * - * 1. if we have a loginToken in the (real) query params, it uses that to log - * in. - * - * 2. if we have a guest access token in the fragment query params, it uses + * 1. if we have a guest access token in the fragment query params, it uses * that. * - * 3. if an access token is stored in local storage (from a previous session), + * 2. if an access token is stored in local storage (from a previous session), * it uses that. * - * 4. it attempts to auto-register as a guest user. + * 3. it attempts to auto-register as a guest user. * - * If any of steps 1-4 are successful, it will call {setLoggedIn}, which in + * If any of steps 1-4 are successful, it will call {_doSetLoggedIn}, which in * turn will raise on_logged_in and will_start_client events. * * @param {object} opts * - * @param {object} opts.realQueryParams: string->string map of the - * query-parameters extracted from the real query-string of the starting - * URI. - * * @param {object} opts.fragmentQueryParams: string->string map of the * query-parameters extracted from the #-fragment of the starting URI. * @@ -70,54 +62,38 @@ import { _t } from './languageHandler'; * true; defines the IS to use. * * @returns {Promise} a promise which resolves when the above process completes. + * Resolves to `true` if we ended up starting a session, or `false` if we + * failed. */ export function loadSession(opts) { - const realQueryParams = opts.realQueryParams || {}; const fragmentQueryParams = opts.fragmentQueryParams || {}; let enableGuest = opts.enableGuest || false; const guestHsUrl = opts.guestHsUrl; const guestIsUrl = opts.guestIsUrl; const defaultDeviceDisplayName = opts.defaultDeviceDisplayName; - if (fragmentQueryParams.client_secret && fragmentQueryParams.sid) { - // this happens during email validation: the email contains a link to the - // IS, which in turn redirects back to vector. We let MatrixChat create a - // Registration component which completes the next stage of registration. - console.log("Not registering as guest: registration already in progress."); - return q(); - } - if (!guestHsUrl) { console.warn("Cannot enable guest access: can't determine HS URL to use"); enableGuest = false; } - if (realQueryParams.loginToken) { - if (!realQueryParams.homeserver) { - console.warn("Cannot log in with token: can't determine HS URL to use"); - } else { - return _loginWithToken(realQueryParams, defaultDeviceDisplayName); - } - } - if (enableGuest && fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_access_token ) { console.log("Using guest access credentials"); - setLoggedIn({ + return _doSetLoggedIn({ userId: fragmentQueryParams.guest_user_id, accessToken: fragmentQueryParams.guest_access_token, homeserverUrl: guestHsUrl, identityServerUrl: guestIsUrl, guest: true, - }); - return q(); + }, true).then(() => true); } return _restoreFromLocalStorage().then((success) => { if (success) { - return; + return true; } if (enableGuest) { @@ -125,10 +101,30 @@ export function loadSession(opts) { } // fall back to login screen + return false; }); } -function _loginWithToken(queryParams, defaultDeviceDisplayName) { +/** + * @param {Object} queryParams string->string map of the + * query-parameters extracted from the real query-string of the starting + * URI. + * + * @param {String} defaultDeviceDisplayName + * + * @returns {Promise} promise which resolves to true if we completed the token + * login, else false + */ +export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) { + if (!queryParams.loginToken) { + return q(false); + } + + if (!queryParams.homeserver) { + console.warn("Cannot log in with token: can't determine HS URL to use"); + return q(false); + } + // create a temporary MatrixClient to do the login const client = Matrix.createClient({ baseUrl: queryParams.homeserver, @@ -141,17 +137,21 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) { }, ).then(function(data) { console.log("Logged in with token"); - setLoggedIn({ - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - homeserverUrl: queryParams.homeserver, - identityServerUrl: queryParams.identityServer, - guest: false, + return _clearStorage().then(() => { + _persistCredentialsToLocalStorage({ + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + homeserverUrl: queryParams.homeserver, + identityServerUrl: queryParams.identityServer, + guest: false, + }); + return true; }); - }, (err) => { + }).catch((err) => { console.error("Failed to log in with login token: " + err + " " + err.data); + return false; }); } @@ -172,16 +172,17 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { }, }).then((creds) => { console.log("Registered as guest: %s", creds.user_id); - setLoggedIn({ + return _doSetLoggedIn({ userId: creds.user_id, deviceId: creds.device_id, accessToken: creds.access_token, homeserverUrl: hsUrl, identityServerUrl: isUrl, guest: true, - }); + }, true).then(() => true); }, (err) => { console.error("Failed to register as guest: " + err + " " + err.data); + return false; }); } @@ -216,15 +217,14 @@ function _restoreFromLocalStorage() { if (accessToken && userId && hsUrl) { console.log("Restoring session for %s", userId); try { - setLoggedIn({ + return _doSetLoggedIn({ userId: userId, deviceId: deviceId, accessToken: accessToken, homeserverUrl: hsUrl, identityServerUrl: isUrl, guest: isGuest, - }); - return q(true); + }, false).then(() => true); } catch (e) { return _handleRestoreFailure(e); } @@ -245,7 +245,7 @@ function _handleRestoreFailure(e) { + ' This is a once off; sorry for the inconvenience.', ); - _clearLocalStorage(); + _clearStorage(); return q.reject(new Error( _t('Unable to restore previous session') + ': ' + msg, @@ -266,7 +266,7 @@ function _handleRestoreFailure(e) { return def.promise.then((success) => { if (success) { // user clicked continue. - _clearLocalStorage(); + _clearStorage(); return false; } @@ -277,17 +277,40 @@ function _handleRestoreFailure(e) { let rtsClient = null; export function initRtsClient(url) { - rtsClient = new RtsClient(url); + if (url) { + rtsClient = new RtsClient(url); + } else { + rtsClient = null; + } } /** - * Transitions to a logged-in state using the given credentials + * Transitions to a logged-in state using the given credentials. + * + * Starts the matrix client and all other react-sdk services that + * listen for events while a session is logged in. + * + * Also stops the old MatrixClient and clears old credentials/etc out of + * storage before starting the new client. + * * @param {MatrixClientCreds} credentials The credentials to use */ export function setLoggedIn(credentials) { - credentials.guest = Boolean(credentials.guest); + stopMatrixClient(); + _doSetLoggedIn(credentials, true); +} - Analytics.setGuest(credentials.guest); +/** + * fires on_logging_in, optionally clears localstorage, persists new credentials + * to localstorage, starts the new client. + * + * @param {MatrixClientCreds} credentials + * @param {Boolean} clearStorage + * + * returns a Promise which resolves once the client has been started + */ +async function _doSetLoggedIn(credentials, clearStorage) { + credentials.guest = Boolean(credentials.guest); console.log( "setLoggedIn: mxid:", credentials.userId, @@ -295,32 +318,26 @@ export function setLoggedIn(credentials) { "guest:", credentials.guest, "hs:", credentials.homeserverUrl, ); + // This is dispatched to indicate that the user is still in the process of logging in // because `teamPromise` may take some time to resolve, breaking the assumption that // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms // later than MatrixChat might assume. dis.dispatch({action: 'on_logging_in'}); + if (clearStorage) { + await _clearStorage(); + } + + Analytics.setGuest(credentials.guest); + // Resolves by default let teamPromise = Promise.resolve(null); - // persist the session + if (localStorage) { try { - localStorage.setItem("mx_hs_url", credentials.homeserverUrl); - localStorage.setItem("mx_is_url", credentials.identityServerUrl); - localStorage.setItem("mx_user_id", credentials.userId); - localStorage.setItem("mx_access_token", credentials.accessToken); - localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest)); - - // if we didn't get a deviceId from the login, leave mx_device_id unset, - // rather than setting it to "undefined". - // - // (in this case MatrixClient doesn't bother with the crypto stuff - // - that's fine for us). - if (credentials.deviceId) { - localStorage.setItem("mx_device_id", credentials.deviceId); - } + _persistCredentialsToLocalStorage(credentials); // The user registered as a PWLU (PassWord-Less User), the generated password // is cached here such that the user can change it at a later time. @@ -331,8 +348,6 @@ export function setLoggedIn(credentials) { cachedPassword: credentials.password, }); } - - console.log("Session persisted for %s", credentials.userId); } catch (e) { console.warn("Error using local storage: can't persist session!", e); } @@ -349,9 +364,6 @@ export function setLoggedIn(credentials) { console.warn("No local storage available: can't persist session!"); } - // stop any running clients before we create a new one with these new credentials - stopMatrixClient(); - MatrixClientPeg.replaceUsingCreds(credentials); teamPromise.then((teamToken) => { @@ -364,6 +376,25 @@ export function setLoggedIn(credentials) { startMatrixClient(); } +function _persistCredentialsToLocalStorage(credentials) { + localStorage.setItem("mx_hs_url", credentials.homeserverUrl); + localStorage.setItem("mx_is_url", credentials.identityServerUrl); + localStorage.setItem("mx_user_id", credentials.userId); + localStorage.setItem("mx_access_token", credentials.accessToken); + localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest)); + + // if we didn't get a deviceId from the login, leave mx_device_id unset, + // rather than setting it to "undefined". + // + // (in this case MatrixClient doesn't bother with the crypto stuff + // - that's fine for us). + if (credentials.deviceId) { + localStorage.setItem("mx_device_id", credentials.deviceId); + } + + console.log("Session persisted for %s", credentials.userId); +} + /** * Logs the current session out and transitions to the logged-out state */ @@ -400,7 +431,7 @@ export function logout() { * Starts the matrix client and all other react-sdk services that * listen for events while a session is logged in. */ -export function startMatrixClient() { +function startMatrixClient() { // dispatch this before starting the matrix client: it's used // to add listeners for the 'sync' event so otherwise we'd have // a race condition (and we need to dispatch synchronously for this @@ -416,34 +447,44 @@ export function startMatrixClient() { } /* - * Stops a running client and all related services, used after - * a session has been logged out / ended. + * Stops a running client and all related services, and clears persistent + * storage. Used after a session has been logged out. */ export function onLoggedOut() { - _clearLocalStorage(); stopMatrixClient(); + _clearStorage().done(); dis.dispatch({action: 'on_logged_out'}); } -function _clearLocalStorage() { +/** + * @returns {Promise} promise which resolves once the stores have been cleared + */ +function _clearStorage() { Analytics.logout(); - if (!window.localStorage) { - return; - } - const hsUrl = window.localStorage.getItem("mx_hs_url"); - const isUrl = window.localStorage.getItem("mx_is_url"); - window.localStorage.clear(); - // preserve our HS & IS URLs for convenience - // N.B. we cache them in hsUrl/isUrl and can't really inline them - // as getCurrentHsUrl() may call through to localStorage. - // NB. We do clear the device ID (as well as all the settings) - if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl); - if (isUrl) window.localStorage.setItem("mx_is_url", isUrl); + if (window.localStorage) { + const hsUrl = window.localStorage.getItem("mx_hs_url"); + const isUrl = window.localStorage.getItem("mx_is_url"); + window.localStorage.clear(); + + // preserve our HS & IS URLs for convenience + // N.B. we cache them in hsUrl/isUrl and can't really inline them + // as getCurrentHsUrl() may call through to localStorage. + // NB. We do clear the device ID (as well as all the settings) + if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl); + if (isUrl) window.localStorage.setItem("mx_is_url", isUrl); + } + + // create a temporary client to clear out the persistent stores. + const cli = createMatrixClient({ + // we'll never make any requests, so can pass a bogus HS URL + baseUrl: "", + }); + return cli.clearStores(); } /** - * Stop all the background processes related to the current client + * Stop all the background processes related to the current client. */ export function stopMatrixClient() { Notifier.stop(); @@ -454,7 +495,6 @@ export function stopMatrixClient() { if (cli) { cli.stopClient(); cli.removeAllListeners(); - cli.store.deleteAllData(); MatrixClientPeg.unset(); } } diff --git a/src/Login.js b/src/Login.js index 87731744e9..8225509919 100644 --- a/src/Login.js +++ b/src/Login.js @@ -97,11 +97,6 @@ export default class Login { guest: true }; }, (error) => { - if (error.httpStatus === 403) { - error.friendlyText = _t("Guest access is disabled on this Home Server."); - } else { - error.friendlyText = _t("Failed to register as guest:") + ' ' + error.data; - } throw error; }); } @@ -157,15 +152,7 @@ export default class Login { accessToken: data.access_token }); }, function(error) { - if (error.httpStatus == 400 && loginParams.medium) { - error.friendlyText = ( - _t('This Home Server does not support login using email address.') - ); - } - else if (error.httpStatus === 403) { - error.friendlyText = ( - _t('Incorrect username and/or password.') - ); + if (error.httpStatus === 403) { if (self._fallbackHsUrl) { var fbClient = Matrix.createClient({ baseUrl: self._fallbackHsUrl, @@ -186,21 +173,23 @@ export default class Login { }); } } - else { - error.friendlyText = ( - _t("There was a problem logging in.") + ' (HTTP ' + error.httpStatus + ")" - ); - } throw error; }); } redirectToCas() { - var client = this._createTemporaryClient(); - var parsedUrl = url.parse(window.location.href, true); + const client = this._createTemporaryClient(); + const parsedUrl = url.parse(window.location.href, true); + + // XXX: at this point, the fragment will always be #/login, which is no + // use to anyone. Ideally, we would get the intended fragment from + // MatrixChat.screenAfterLogin so that you could follow #/room links etc + // through a CAS login. + parsedUrl.hash = ""; + parsedUrl.query["homeserver"] = client.getHomeserverUrl(); parsedUrl.query["identityServer"] = client.getIdentityServerUrl(); - var casUrl = client.getCasLoginUrl(url.format(parsedUrl)); + const casUrl = client.getCasLoginUrl(url.format(parsedUrl)); window.location.href = casUrl; } } diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 94e55a8d8a..47370e2142 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -16,12 +16,10 @@ limitations under the License. 'use strict'; -import Matrix from 'matrix-js-sdk'; import utils from 'matrix-js-sdk/lib/utils'; import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline'; import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set'; - -const localStorage = window.localStorage; +import createMatrixClient from './utils/createMatrixClient'; interface MatrixClientCreds { homeserverUrl: string, @@ -129,22 +127,7 @@ class MatrixClientPeg { timelineSupport: true, }; - if (localStorage) { - opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); - } - if (window.indexedDB && localStorage) { - // FIXME: bodge to remove old database. Remove this after a few weeks. - window.indexedDB.deleteDatabase("matrix-js-sdk:default"); - - opts.store = new Matrix.IndexedDBStore({ - indexedDB: window.indexedDB, - dbName: "riot-web-sync", - localStorage: localStorage, - workerScript: this.indexedDbWorkerScript, - }); - } - - this.matrixClient = Matrix.createClient(opts); + this.matrixClient = createMatrixClient(opts); // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. diff --git a/src/Modal.js b/src/Modal.js index 8d53b2da7d..e100105a88 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -64,7 +64,6 @@ const AsyncWrapper = React.createClass({ render: function() { const {loader, ...otherProps} = this.props; - if (this.state.component) { const Component = this.state.component; return ; @@ -199,4 +198,7 @@ class ModalManager { } } -export default new ModalManager(); +if (!global.singletonModalManager) { + global.singletonModalManager = new ModalManager(); +} +export default global.singletonModalManager; diff --git a/src/Rooms.js b/src/Rooms.js index 16b5ab9ee2..3ac7c68533 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -144,7 +144,18 @@ export function guessDMRoomTarget(room, me) { let oldestTs; let oldestUser; - // Pick the user who's been here longest (and isn't us) + // Pick the joined user who's been here longest (and isn't us), + for (const user of room.getJoinedMembers()) { + if (user.userId == me.userId) continue; + + if (oldestTs === undefined || user.events.member.getTs() < oldestTs) { + oldestUser = user; + oldestTs = user.events.member.getTs(); + } + } + if (oldestUser) return oldestUser; + + // if there are no joined members other than us, use the oldest member for (const user of room.currentState.getMembers()) { if (user.userId == me.userId) continue; diff --git a/src/RtsClient.js b/src/RtsClient.js index 8c3ce54b37..493b19599c 100644 --- a/src/RtsClient.js +++ b/src/RtsClient.js @@ -1,5 +1,7 @@ import 'whatwg-fetch'; +let fetchFunction = fetch; + function checkStatus(response) { if (!response.ok) { return response.text().then((text) => { @@ -31,7 +33,7 @@ const request = (url, opts) => { opts.body = JSON.stringify(opts.body); opts.headers['Content-Type'] = 'application/json'; } - return fetch(url, opts) + return fetchFunction(url, opts) .then(checkStatus) .then(parseJson); }; @@ -64,7 +66,7 @@ export default class RtsClient { client_secret: clientSecret, }, method: 'POST', - } + }, ); } @@ -74,7 +76,7 @@ export default class RtsClient { qs: { team_token: teamToken, }, - } + }, ); } @@ -91,7 +93,12 @@ export default class RtsClient { qs: { user_id: userId, }, - } + }, ); } + + // allow fetch to be replaced, for testing. + static setFetch(fn) { + fetchFunction = fn; + } } diff --git a/src/TextForEvent.js b/src/TextForEvent.js index fa78f9d61b..de12cec502 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -13,9 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ - -var MatrixClientPeg = require("./MatrixClientPeg"); -var CallHandler = require("./CallHandler"); +import MatrixClientPeg from "./MatrixClientPeg"; +import CallHandler from "./CallHandler"; import { _t } from './languageHandler'; import * as Roles from './Roles'; @@ -117,7 +116,7 @@ function textForTopicEvent(ev) { function textForRoomNameEvent(ev) { var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - + if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName: senderDisplayName}); } @@ -142,9 +141,21 @@ function textForCallAnswerEvent(event) { } function textForCallHangupEvent(event) { - var senderName = event.sender ? event.sender.name : _t('Someone'); - var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)'); - return _t('%(senderName)s ended the call.', {senderName: senderName}) + ' ' + supported; + const senderName = event.sender ? event.sender.name : _t('Someone'); + const eventContent = event.getContent(); + let reason = ""; + if(!MatrixClientPeg.get().supportsVoip()) { + reason = _t('(not supported by this browser)'); + } else if(eventContent.reason) { + if (eventContent.reason === "ice_failed") { + reason = _t('(could not connect media)'); + } else if (eventContent.reason === "invite_timeout") { + reason = _t('(no answer)'); + } else { + reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason}); + } + } + return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason; } function textForCallInviteEvent(event) { diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index d6f16a7105..8f113353d9 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -81,11 +81,13 @@ export default React.createClass({ FileSaver.saveAs(blob, 'riot-keys.txt'); this.props.onFinished(true); }).catch((e) => { + console.error("Error exporting e2e keys:", e); if (this._unmounted) { return; } + const msg = e.friendlyText || _t('Unknown error'); this.setState({ - errStr: e.message, + errStr: msg, phase: PHASE_EDIT, }); }); @@ -120,7 +122,7 @@ export default React.createClass({ 'you have received in encrypted rooms to a local file. You ' + 'will then be able to import the file into another Matrix ' + 'client in the future, so that client will also be able to ' + - 'decrypt these messages.' + 'decrypt these messages.', ) }

@@ -130,7 +132,7 @@ export default React.createClass({ 'careful to keep it secure. To help with this, you should enter ' + 'a passphrase below, which will be used to encrypt the exported ' + 'data. It will only be possible to import the data by using the ' + - 'same passphrase.' + 'same passphrase.', ) }

diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index 61d2aeec74..9eac7f78b2 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -89,11 +89,13 @@ export default React.createClass({ // TODO: it would probably be nice to give some feedback about what we've imported here. this.props.onFinished(true); }).catch((e) => { + console.error("Error importing e2e keys:", e); if (this._unmounted) { return; } + const msg = e.friendlyText || _t('Unknown error'); this.setState({ - errStr: e.message, + errStr: msg, phase: PHASE_EDIT, }); }); @@ -122,13 +124,13 @@ export default React.createClass({ 'This process allows you to import encryption keys ' + 'that you had previously exported from another Matrix ' + 'client. You will then be able to decrypt any ' + - 'messages that the other client could decrypt.' + 'messages that the other client could decrypt.', ) }

{ _t( 'The export file will be protected with a passphrase. ' + - 'You should enter the passphrase here, to decrypt the file.' + 'You should enter the passphrase here, to decrypt the file.', ) }

diff --git a/src/components/structures/CreateRoom.js b/src/components/structures/CreateRoom.js index 3e291dfd94..7ecc315ba7 100644 --- a/src/components/structures/CreateRoom.js +++ b/src/components/structures/CreateRoom.js @@ -231,7 +231,7 @@ module.exports = React.createClass({ if (curr_phase == this.phases.ERROR) { error_box = (
- {_t('An error occured: %(error_string)s', {error_string: this.state.error_string})} + {_t('An error occurred: %(error_string)s', {error_string: this.state.error_string})}
); } diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index a201a0bea7..8b0bcaad68 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -223,10 +223,8 @@ export default React.createClass({ ref='roomView' autoJoin={this.props.autoJoin} onRegistered={this.props.onRegistered} - eventId={this.props.initialEventId} thirdPartyInvite={this.props.thirdPartyInvite} oobData={this.props.roomOobData} - highlightedEventId={this.props.highlightedEventId} eventPixelOffset={this.props.initialEventPixelOffset} key={this.props.currentRoomId || 'roomview'} opacity={this.props.middleOpacity} @@ -241,7 +239,6 @@ export default React.createClass({ page_element = {}, + onTokenLoginCompleted: () => {}, }; }, @@ -192,7 +221,7 @@ module.exports = React.createClass({ componentWillMount: function() { SdkConfig.put(this.props.config); - RoomViewStore.addListener(this._onRoomViewStoreUpdated); + this._roomViewStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdated); this._onRoomViewStoreUpdated(); if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable(); @@ -269,30 +298,52 @@ module.exports = React.createClass({ window.addEventListener('resize', this.handleResize); this.handleResize(); - if (this.props.config.teamServerConfig && - this.props.config.teamServerConfig.teamServerURL - ) { - Lifecycle.initRtsClient(this.props.config.teamServerConfig.teamServerURL); - } + const teamServerConfig = this.props.config.teamServerConfig || {}; + Lifecycle.initRtsClient(teamServerConfig.teamServerURL); - // the extra q() ensures that synchronous exceptions hit the same codepath as - // asynchronous ones. - q().then(() => { - return Lifecycle.loadSession({ - realQueryParams: this.props.realQueryParams, - fragmentQueryParams: this.props.startingFragmentQueryParams, - enableGuest: this.props.enableGuest, - guestHsUrl: this.getCurrentHsUrl(), - guestIsUrl: this.getCurrentIsUrl(), - defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, + // the first thing to do is to try the token params in the query-string + Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => { + if(loggedIn) { + this.props.onTokenLoginCompleted(); + + // don't do anything else until the page reloads - just stay in + // the 'loading' state. + return; + } + + // if the user has followed a login or register link, don't reanimate + // the old creds, but rather go straight to the relevant page + const firstScreen = this.state.screenAfterLogin ? + this.state.screenAfterLogin.screen : null; + + if (firstScreen === 'login' || + firstScreen === 'register' || + firstScreen === 'forgot_password') { + this.setState({loading: false}); + this._showScreenAfterLogin(); + return; + } + + // the extra q() ensures that synchronous exceptions hit the same codepath as + // asynchronous ones. + return q().then(() => { + return Lifecycle.loadSession({ + fragmentQueryParams: this.props.startingFragmentQueryParams, + enableGuest: this.props.enableGuest, + guestHsUrl: this.getCurrentHsUrl(), + guestIsUrl: this.getCurrentIsUrl(), + defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, + }); + }).catch((e) => { + console.error("Unable to load session", e); + return false; + }).then((loadedSession) => { + if (!loadedSession) { + // fall back to showing the login screen + dis.dispatch({action: "start_login"}); + } }); - }).catch((e) => { - console.error("Unable to load session", e); - }).done(()=>{ - // stuff this through the dispatcher so that it happens - // after the on_logged_in action. - dis.dispatch({action: 'load_completed'}); - }); + }).done(); }, componentWillUnmount: function() { @@ -301,6 +352,7 @@ module.exports = React.createClass({ UDEHandler.stopListening(); window.removeEventListener("focus", this.onFocus); window.removeEventListener('resize', this.handleResize); + this._roomViewStoreToken.remove(); }, componentDidUpdate: function() { @@ -310,20 +362,19 @@ module.exports = React.createClass({ } }, - setStateForNewScreen: function(state) { + setStateForNewView: function(state) { + if (state.view === undefined) { + throw new Error("setStateForNewView with no view!"); + } const newState = { - screen: undefined, viewUserId: null, - loggedIn: false, - ready: false, - upgradeUsername: null, - guestAccessToken: null, }; Object.assign(newState, state); this.setState(newState); }, onAction: function(payload) { + // console.log(`MatrixClientPeg.onAction: ${payload.action}`); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -342,38 +393,19 @@ module.exports = React.createClass({ guestCreds: MatrixClientPeg.getCredentials(), }); } - this.setStateForNewScreen({ - screen: 'login', + this.setStateForNewView({ + view: VIEWS.LOGIN, }); this.notifyNewScreen('login'); break; case 'start_post_registration': - this.setState({ // don't clobber loggedIn status - screen: 'post_registration', + this.setState({ + view: VIEWS.POST_REGISTRATION, }); break; - case 'start_upgrade_registration': - // also stash our credentials, then if we restore the session, - // we can just do it the same way whether we started upgrade - // registration or explicitly logged out - this.setStateForNewScreen({ - guestCreds: MatrixClientPeg.getCredentials(), - screen: "register", - upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(), - guestAccessToken: MatrixClientPeg.get().getAccessToken(), - }); - - // stop the client: if we are syncing whilst the registration - // is completed in another browser, we'll be 401ed for using - // a guest access token for a non-guest account. - // It will be restarted in onReturnToGuestClick - Lifecycle.stopMatrixClient(); - - this.notifyNewScreen('register'); - break; case 'start_password_recovery': - this.setStateForNewScreen({ - screen: 'forgot_password', + this.setStateForNewView({ + view: VIEWS.FORGOT_PASSWORD, }); this.notifyNewScreen('forgot_password'); break; @@ -397,7 +429,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().leave(payload.room_id).done(() => { modal.close(); - if (this.currentRoomId === payload.room_id) { + if (this.state.currentRoomId === payload.room_id) { dis.dispatch({action: 'view_next_room'}); } }, (err) => { @@ -467,7 +499,7 @@ module.exports = React.createClass({ this.notifyNewScreen('home'); break; case 'view_set_mxid': - this._setMxId(); + this._setMxId(payload); break; case 'view_start_chat_or_reuse': this._chatCreateOrReuse(payload.user_id); @@ -517,7 +549,10 @@ module.exports = React.createClass({ // and also that we're not ready (we'll be marked as logged // in once the login completes, then ready once the sync // completes). - this.setState({loggingIn: true, ready: false}); + this.setStateForNewView({ + view: VIEWS.LOGGING_IN, + ready: false, + }); break; case 'on_logged_in': this._onLoggedIn(payload.teamToken); @@ -528,15 +563,15 @@ module.exports = React.createClass({ case 'will_start_client': this._onWillStartClient(); break; - case 'load_completed': - this._onLoadCompleted(); - break; case 'new_version': this.onVersion( payload.currentVersion, payload.newVersion, payload.releaseNotes, ); break; + case 'send_event': + this.onSendEvent(payload.room_id, payload.event); + break; } }, @@ -551,8 +586,8 @@ module.exports = React.createClass({ }, _startRegistration: function(params) { - this.setStateForNewScreen({ - screen: 'register', + this.setStateForNewView({ + view: VIEWS.REGISTER, // these params may be undefined, but if they are, // unset them from our state: we don't want to // resume a previous registration session if the @@ -571,6 +606,15 @@ module.exports = React.createClass({ const allRooms = RoomListSorter.mostRecentActivityFirst( MatrixClientPeg.get().getRooms(), ); + // If there are 0 rooms or 1 room, view the home page because otherwise + // if there are 0, we end up trying to index into an empty array, and + // if there is 1, we end up viewing the same room. + if (allRooms.length < 2) { + dis.dispatch({ + action: 'view_home_page', + }); + return; + } let roomIndex = -1; for (let i = 0; i < allRooms.length; ++i) { if (allRooms[i].roomId == this.state.currentRoomId) { @@ -608,6 +652,8 @@ module.exports = React.createClass({ // @param {boolean=} roomInfo.show_settings Makes RoomView show the room settings dialog. // @param {string=} roomInfo.event_id ID of the event in this room to show: this will cause a switch to the // context of that particular event. + // @param {boolean=} roomInfo.highlighted If true, add event_id to the hash of the URL + // and alter the EventTile to appear highlighted. // @param {Object=} roomInfo.third_party_invite Object containing data about the third party // we received to join the room, if any. // @param {string=} roomInfo.third_party_invite.inviteSignUrl 3pid invite sign URL @@ -619,30 +665,21 @@ module.exports = React.createClass({ this.focusComposer = true; const newState = { - initialEventId: roomInfo.event_id, - highlightedEventId: roomInfo.event_id, - initialEventPixelOffset: undefined, page_type: PageTypes.RoomView, thirdPartyInvite: roomInfo.third_party_invite, roomOobData: roomInfo.oob_data, - currentRoomAlias: roomInfo.room_alias, autoJoin: roomInfo.auto_join, }; - if (!roomInfo.room_alias) { - newState.currentRoomId = roomInfo.room_id; - } - - // if we aren't given an explicit event id, look for one in the - // scrollStateMap. - // - // TODO: do this in RoomView rather than here - if (!roomInfo.event_id && this.refs.loggedInView) { - const scrollState = this.refs.loggedInView.getScrollStateForRoom(roomInfo.room_id); - if (scrollState) { - newState.initialEventId = scrollState.focussedEvent; - newState.initialEventPixelOffset = scrollState.pixelOffset; - } + if (roomInfo.room_alias) { + console.log( + `Switching to room alias ${roomInfo.room_alias} at event ` + + roomInfo.event_id, + ); + } else { + console.log(`Switching to room id ${roomInfo.room_id} at event ` + + roomInfo.event_id, + ); } // Wait for the first sync to complete so that if a room does have an alias, @@ -670,7 +707,7 @@ module.exports = React.createClass({ } } - if (roomInfo.event_id) { + if (roomInfo.event_id && roomInfo.highlighted) { presentedId += "/" + roomInfo.event_id; } this.notifyNewScreen('room/' + presentedId); @@ -679,7 +716,7 @@ module.exports = React.createClass({ }); }, - _setMxId: function() { + _setMxId: function(payload) { const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); const close = Modal.createDialog(SetMxIdDialog, { homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(), @@ -688,6 +725,11 @@ module.exports = React.createClass({ dis.dispatch({ action: 'cancel_after_sync_prepared', }); + if (payload.go_home_on_cancel) { + dis.dispatch({ + action: 'view_home_page', + }); + } return; } this.onRegistered(credentials); @@ -739,8 +781,8 @@ module.exports = React.createClass({ title: _t('Create Room'), description: _t('Room name (optional)'), button: _t('Create Room'), - onFinished: (should_create, name) => { - if (should_create) { + onFinished: (shouldCreate, name) => { + if (shouldCreate) { const createOpts = {}; if (name) createOpts.name = name; createRoom({createOpts}).done(); @@ -768,6 +810,11 @@ module.exports = React.createClass({ } dis.dispatch({ action: 'view_set_mxid', + // If the set_mxid dialog is cancelled, view /home because if the browser + // was pointing at /user/@someone:domain?action=chat, the URL needs to be + // reset so that they can revisit /user/.. // (and trigger + // `_chatCreateOrReuse` again) + go_home_on_cancel: true, }); return; } @@ -848,22 +895,6 @@ module.exports = React.createClass({ }); }, - /** - * Called when the sessionloader has finished - */ - _onLoadCompleted: function() { - this.props.onLoadCompleted(); - this.setState({loading: false}); - - // Show screens (like 'register') that need to be shown without _onLoggedIn - // being called. 'register' needs to be routed here when the email confirmation - // link is clicked on. - if (this.state.screenAfterLogin && - ['register'].indexOf(this.state.screenAfterLogin.screen) !== -1) { - this._showScreenAfterLogin(); - } - }, - /** * Called whenever someone changes the theme * @@ -916,9 +947,8 @@ module.exports = React.createClass({ */ _onLoggedIn: function(teamToken) { this.setState({ + view: VIEWS.LOGGED_IN, guestCreds: null, - loggedIn: true, - loggingIn: false, }); if (teamToken) { @@ -960,6 +990,7 @@ module.exports = React.createClass({ this.state.screenAfterLogin.screen, this.state.screenAfterLogin.params, ); + // XXX: is this necessary? `showScreen` should do it for us. this.notifyNewScreen(this.state.screenAfterLogin.screen); this.setState({screenAfterLogin: null}); } else if (localStorage && localStorage.getItem('mx_last_room_id')) { @@ -978,8 +1009,8 @@ module.exports = React.createClass({ */ _onLoggedOut: function() { this.notifyNewScreen('login'); - this.setStateForNewScreen({ - loggedIn: false, + this.setStateForNewView({ + view: VIEWS.LOGIN, ready: false, collapse_lhs: false, collapse_rhs: false, @@ -1135,6 +1166,10 @@ module.exports = React.createClass({ const payload = { action: 'view_room', event_id: eventId, + // If an event ID is given in the URL hash, notify RoomViewStore to mark + // it as highlighted, which will propagate to RoomView and highlight the + // associated EventTile. + highlighted: Boolean(eventId), third_party_invite: thirdPartyInvite, oob_data: oobData, }; @@ -1146,7 +1181,7 @@ module.exports = React.createClass({ // we can't view a room unless we're logged in // (a guest account is fine) - if (this.state.loggedIn) { + if (this.state.view === VIEWS.LOGGED_IN) { dis.dispatch(payload); } } else if (screen.indexOf('user/') == 0) { @@ -1247,6 +1282,8 @@ module.exports = React.createClass({ onReturnToGuestClick: function() { // reanimate our guest login if (this.state.guestCreds) { + // TODO: this is probably a bit broken - we don't want to be + // clearing storage when we reanimate the guest creds. Lifecycle.setLoggedIn(this.state.guestCreds); this.setState({guestCreds: null}); } @@ -1264,7 +1301,7 @@ module.exports = React.createClass({ onFinishPostRegistration: function() { // Don't confuse this with "PageType" which is the middle window to show this.setState({ - screen: undefined, + view: VIEWS.LOGGED_IN, }); this.showScreen("settings"); }, @@ -1278,6 +1315,27 @@ module.exports = React.createClass({ }); }, + onSendEvent: function(roomId, event) { + const cli = MatrixClientPeg.get(); + if (!cli) { + dis.dispatch({action: 'message_send_failed'}); + return; + } + + cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { + dis.dispatch({action: 'message_sent'}); + }, (err) => { + if (err.name === 'UnknownDeviceError') { + dis.dispatch({ + action: 'unknown_device_error', + err: err, + room: cli.getRoom(roomId), + }); + } + dis.dispatch({action: 'message_send_failed'}); + }); + }, + updateStatusIndicator: function(state, prevState) { let notifCount = 0; @@ -1319,7 +1377,7 @@ module.exports = React.createClass({ }); } else { dis.dispatch({ - action: 'view_room_directory', + action: 'view_home_page', }); } }, @@ -1332,11 +1390,9 @@ module.exports = React.createClass({ }, render: function() { - // `loading` might be set to false before `loggedIn = true`, causing the default - // (``) to be visible for a few MS (say, whilst a request is in-flight to - // the RTS). So in the meantime, use `loggingIn`, which is true between - // actions `on_logging_in` and `on_logged_in`. - if (this.state.loading || this.state.loggingIn) { + // console.log(`Rendering MatrixChat with view ${this.state.view}`); + + if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN) { const Spinner = sdk.getComponent('elements.Spinner'); return (
@@ -1346,7 +1402,7 @@ module.exports = React.createClass({ } // needs to be before normal PageTypes as you are logged in technically - if (this.state.screen == 'post_registration') { + if (this.state.view === VIEWS.POST_REGISTRATION) { const PostRegistration = sdk.getComponent('structures.login.PostRegistration'); return ( - ); - } else if (this.state.loggedIn) { - // we think we are logged in, but are still waiting for the /sync to complete - const Spinner = sdk.getComponent('elements.Spinner'); - return ( - - ); - } else if (this.state.screen == 'register') { + if (this.state.view === VIEWS.LOGGED_IN) { + // `ready` and `view==LOGGED_IN` may be set before `page_type` (because the + // latter is set via the dispatcher). If we don't yet have a `page_type`, + // keep showing the spinner for now. + if (this.state.ready && this.state.page_type) { + /* for now, we stuff the entirety of our props and state into the LoggedInView. + * we should go through and figure out what we actually need to pass down, as well + * as using something like redux to avoid having a billion bits of state kicking around. + */ + const LoggedInView = sdk.getComponent('structures.LoggedInView'); + return ( + + ); + } else { + // we think we are logged in, but are still waiting for the /sync to complete + const Spinner = sdk.getComponent('elements.Spinner'); + return ( + + ); + } + } + + if (this.state.view === VIEWS.REGISTER) { const Registration = sdk.getComponent('structures.login.Registration'); return ( ); - } else if (this.state.screen == 'forgot_password') { + } + + + if (this.state.view === VIEWS.FORGOT_PASSWORD) { const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); return ( ); - } else { + } + + if (this.state.view === VIEWS.LOGIN) { const Login = sdk.getComponent('structures.login.Login'); return ( ); } + + console.error(`Unknown view ${this.state.view}`); }, }); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index df21715d75..b29b3579f0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -83,36 +83,8 @@ module.exports = React.createClass({ // * invited us tovthe room oobData: React.PropTypes.object, - // id of an event to jump to. If not given, will go to the end of the - // live timeline. - eventId: React.PropTypes.string, - - // where to position the event given by eventId, in pixels from the - // bottom of the viewport. If not given, will try to put the event - // 1/3 of the way down the viewport. - eventPixelOffset: React.PropTypes.number, - - // ID of an event to highlight. If undefined, no event will be highlighted. - // Typically this will either be the same as 'eventId', or undefined. - highlightedEventId: React.PropTypes.string, - // is the RightPanel collapsed? collapsedRhs: React.PropTypes.bool, - - // a map from room id to scroll state, which will be updated on unmount. - // - // If there is no special scroll state (ie, we are following the live - // timeline), the scroll state is null. Otherwise, it is an object with - // the following properties: - // - // focussedEvent: the ID of the 'focussed' event. Typically this is - // the last event fully visible in the viewport, though if we - // have done an explicit scroll to an explicit event, it will be - // that event. - // - // pixelOffset: the number of pixels the window is scrolled down - // from the focussedEvent. - scrollStateMap: React.PropTypes.object, }, getInitialState: function() { @@ -121,6 +93,14 @@ module.exports = React.createClass({ roomId: null, roomLoading: true, peekLoading: false, + shouldPeek: true, + + // The event to be scrolled to initially + initialEventId: null, + // The offset in pixels from the event with which to scroll vertically + initialEventPixelOffset: null, + // Whether to highlight the event scrolled to + isInitialEventHighlighted: null, forwardingEvent: null, editingRoomSettings: false, @@ -180,15 +160,59 @@ module.exports = React.createClass({ if (this.unmounted) { return; } - this.setState({ + const newState = { roomId: RoomViewStore.getRoomId(), roomAlias: RoomViewStore.getRoomAlias(), roomLoading: RoomViewStore.isRoomLoading(), roomLoadError: RoomViewStore.getRoomLoadError(), joining: RoomViewStore.isJoining(), - }, () => { - this._onHaveRoom(); - this.onRoom(MatrixClientPeg.get().getRoom(this.state.roomId)); + initialEventId: RoomViewStore.getInitialEventId(), + initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(), + isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), + forwardingEvent: RoomViewStore.getForwardingEvent(), + shouldPeek: RoomViewStore.shouldPeek(), + }; + + // finished joining, start waiting for a room and show a spinner. See onRoom. + newState.waitingForRoom = this.state.joining && !newState.joining && + !RoomViewStore.getJoinError(); + + // Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307 + console.log( + 'RVS update:', + newState.roomId, + newState.roomAlias, + 'loading?', newState.roomLoading, + 'joining?', newState.joining, + 'initial?', initial, + 'waiting?', newState.waitingForRoom, + 'shouldPeek?', newState.shouldPeek, + ); + + // NB: This does assume that the roomID will not change for the lifetime of + // the RoomView instance + if (initial) { + newState.room = MatrixClientPeg.get().getRoom(newState.roomId); + } + + // Clear the search results when clicking a search result (which changes the + // currently scrolled to event, this.state.initialEventId). + if (this.state.initialEventId !== newState.initialEventId) { + newState.searchResults = null; + } + + // Store the scroll state for the previous room so that we can return to this + // position when viewing this room in future. + if (this.state.roomId !== newState.roomId) { + this._updateScrollMap(this.state.roomId); + } + + this.setState(newState, () => { + // At this point, this.state.roomId could be null (e.g. the alias might not + // have been resolved yet) so anything called here must handle this case. + if (initial) { + this._onHaveRoom(); + } }); }, @@ -204,25 +228,24 @@ module.exports = React.createClass({ // which must be by alias or invite wherever possible (peeking currently does // not work over federation). - // NB. We peek if we are not in the room, although if we try to peek into - // a room in which we have a member event (ie. we've left) synapse will just - // send us the same data as we get in the sync (ie. the last events we saw). - const room = MatrixClientPeg.get().getRoom(this.state.roomId); - let isUserJoined = null; + // NB. We peek if we have never seen the room before (i.e. js-sdk does not know + // about it). We don't peek in the historical case where we were joined but are + // now not joined because the js-sdk peeking API will clobber our historical room, + // making it impossible to indicate a newly joined room. + const room = this.state.room; if (room) { - isUserJoined = room.hasMembershipState( - MatrixClientPeg.get().credentials.userId, 'join', - ); - this._updateAutoComplete(room); this.tabComplete.loadEntries(room); + this.setState({ + unsentMessageError: this._getUnsentMessageError(room), + }); + this._onRoomLoaded(room); } - if (!isUserJoined && !this.state.joining && this.state.roomId) { + if (!this.state.joining && this.state.roomId) { if (this.props.autoJoin) { this.onJoinButtonClicked(); - } else if (this.state.roomId) { + } else if (!room && this.state.shouldPeek) { console.log("Attempting to peek into room %s", this.state.roomId); - this.setState({ peekLoading: true, }); @@ -246,12 +269,9 @@ module.exports = React.createClass({ } }).done(); } - } else if (isUserJoined) { + } else if (room) { + // Stop peeking because we have joined this room previously MatrixClientPeg.get().stopPeeking(); - this.setState({ - unsentMessageError: this._getUnsentMessageError(room), - }); - this._onRoomLoaded(room); } }, @@ -287,13 +307,6 @@ module.exports = React.createClass({ } }, - componentWillReceiveProps: function(newProps) { - if (newProps.eventId != this.props.eventId) { - // when we change focussed event id, hide the search results. - this.setState({searchResults: null}); - } - }, - shouldComponentUpdate: function(nextProps, nextState) { return (!ObjectUtils.shallowEqual(this.props, nextProps) || !ObjectUtils.shallowEqual(this.state, nextState)); @@ -319,7 +332,7 @@ module.exports = React.createClass({ this.unmounted = true; // update the scroll map before we get unmounted - this._updateScrollMap(); + this._updateScrollMap(this.state.roomId); if (this.refs.roomView) { // disconnect the D&D event listeners from the room view. This @@ -445,11 +458,6 @@ module.exports = React.createClass({ callState: callState }); - break; - case 'forward_event': - this.setState({ - forwardingEvent: payload.content, - }); break; } }, @@ -598,12 +606,25 @@ module.exports = React.createClass({ }); }, + _updateScrollMap(roomId) { + // No point updating scroll state if the room ID hasn't been resolved yet + if (!roomId) { + return; + } + dis.dispatch({ + action: 'update_scroll_state', + room_id: roomId, + scroll_state: this._getScrollState(), + }); + }, + onRoom: function(room) { if (!room || room.roomId !== this.state.roomId) { return; } this.setState({ room: room, + waitingForRoom: false, }, () => { this._onRoomLoaded(room); }); @@ -659,7 +680,14 @@ module.exports = React.createClass({ onRoomMemberMembership: function(ev, member, oldMembership) { if (member.userId == MatrixClientPeg.get().credentials.userId) { - this.forceUpdate(); + + if (member.membership === 'join') { + this.setState({ + waitingForRoom: false, + }); + } else { + this.forceUpdate(); + } } }, @@ -1137,8 +1165,13 @@ module.exports = React.createClass({ this.updateTint(); this.setState({ editingRoomSettings: false, - forwardingEvent: null, }); + if (this.state.forwardingEvent) { + dis.dispatch({ + action: 'forward_event', + event: null, + }); + } dis.dispatch({action: 'focus_composer'}); }, @@ -1240,21 +1273,6 @@ module.exports = React.createClass({ } }, - // update scrollStateMap on unmount - _updateScrollMap: function() { - if (!this.state.room) { - // we were instantiated on a room alias and haven't yet joined the room. - return; - } - if (!this.props.scrollStateMap) return; - - var roomId = this.state.room.roomId; - - var state = this._getScrollState(); - this.props.scrollStateMap[roomId] = state; - }, - - // get the current scroll position of the room, so that it can be // restored when we switch back to it. // @@ -1428,6 +1446,10 @@ module.exports = React.createClass({ const Loader = sdk.getComponent("elements.Spinner"); const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); + // Whether the preview bar spinner should be shown. We do this when joining or + // when waiting for a room to be returned by js-sdk when joining + const previewBarSpinner = this.state.joining || this.state.waitingForRoom; + if (!this.state.room) { if (this.state.roomLoading || this.state.peekLoading) { return ( @@ -1447,7 +1469,7 @@ module.exports = React.createClass({ // We have no room object for this room, only the ID. // We've got to this room by following a link, possibly a third party invite. - var room_alias = this.state.room_alias; + const roomAlias = this.state.roomAlias; return (
@@ -1560,7 +1582,7 @@ module.exports = React.createClass({ } else if (this.state.uploadingRoomSettings) { aux = ; } else if (this.state.forwardingEvent !== null) { - aux = ; + aux = ; } else if (this.state.searching) { hideCancel = true; // has own cancel aux = ; @@ -1580,7 +1602,7 @@ module.exports = React.createClass({