Merge branch 'develop' into rte-fixes2

Conflicts:
	package.json
	src/autocomplete/CommandProvider.js
	src/autocomplete/UserProvider.js
	src/components/structures/RoomView.js
	src/components/structures/UserSettings.js
	src/components/views/rooms/MessageComposerInput.js
This commit is contained in:
Luke Barnard 2017-06-23 15:30:06 +01:00
commit 87609582c6
216 changed files with 21940 additions and 3614 deletions

184
.eslintignore.errorfiles Normal file
View file

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

5
.gitignore vendored
View file

@ -9,3 +9,8 @@ npm-debug.log
# test reports created by karma # test reports created by karma
/karma-reports /karma-reports
/.idea
/src/component-index.js
.DS_Store

View file

@ -9,11 +9,16 @@ set -ev
RIOT_WEB_DIR=riot-web RIOT_WEB_DIR=riot-web
REACT_SDK_DIR=`pwd` REACT_SDK_DIR=`pwd`
git clone --depth=1 --branch develop https://github.com/vector-im/riot-web.git \ curbranch="${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}"
echo "Determined branch to be $curbranch"
git clone https://github.com/vector-im/riot-web.git \
"$RIOT_WEB_DIR" "$RIOT_WEB_DIR"
cd "$RIOT_WEB_DIR" cd "$RIOT_WEB_DIR"
git checkout "$curbranch" || git checkout develop
mkdir node_modules mkdir node_modules
npm install npm install

View file

@ -5,5 +5,4 @@ install:
- npm install - npm install
- (cd node_modules/matrix-js-sdk && npm install) - (cd node_modules/matrix-js-sdk && npm install)
script: script:
- npm run test ./scripts/travis.sh
- ./.travis-test-riot.sh

View file

@ -1,3 +1,624 @@
Changes in [0.9.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.7) (2017-06-22)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.6...v0.9.7)
* Fix ability to invite users with caps in their user IDs
[\#1128](https://github.com/matrix-org/matrix-react-sdk/pull/1128)
* Fix another race with first-sync
[\#1131](https://github.com/matrix-org/matrix-react-sdk/pull/1131)
* Make the indexeddb worker script work again
[\#1132](https://github.com/matrix-org/matrix-react-sdk/pull/1132)
* Use the web worker when clearing js-sdk stores
[\#1133](https://github.com/matrix-org/matrix-react-sdk/pull/1133)
Changes in [0.9.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.6) (2017-06-20)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.5...v0.9.6)
* Fix infinite spinner on email registration
[\#1120](https://github.com/matrix-org/matrix-react-sdk/pull/1120)
* Translate help promots in room list
[\#1121](https://github.com/matrix-org/matrix-react-sdk/pull/1121)
* Internationalise the drop targets
[\#1122](https://github.com/matrix-org/matrix-react-sdk/pull/1122)
* Fix another infinite spin on register
[\#1124](https://github.com/matrix-org/matrix-react-sdk/pull/1124)
Changes in [0.9.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.5) (2017-06-19)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.5-rc.2...v0.9.5)
* Don't peek when creating a room
[\#1113](https://github.com/matrix-org/matrix-react-sdk/pull/1113)
* More translations & translation fixes
Changes in [0.9.5-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.5-rc.2) (2017-06-16)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.5-rc.1...v0.9.5-rc.2)
* Avoid getting stuck in a loop in CAS login
[\#1109](https://github.com/matrix-org/matrix-react-sdk/pull/1109)
* Update from Weblate.
[\#1101](https://github.com/matrix-org/matrix-react-sdk/pull/1101)
* Correctly inspect state when rejecting invite
[\#1108](https://github.com/matrix-org/matrix-react-sdk/pull/1108)
* Make sure to pass the roomAlias to the preview header if we have it
[\#1107](https://github.com/matrix-org/matrix-react-sdk/pull/1107)
* Make sure captcha disappears when container does
[\#1106](https://github.com/matrix-org/matrix-react-sdk/pull/1106)
* Fix URL previews
[\#1105](https://github.com/matrix-org/matrix-react-sdk/pull/1105)
Changes in [0.9.5-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.5-rc.1) (2017-06-15)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.4...v0.9.5-rc.1)
* Groundwork for tests including a teamserver login
[\#1098](https://github.com/matrix-org/matrix-react-sdk/pull/1098)
* Show a spinner when accepting an invite and waitingForRoom
[\#1100](https://github.com/matrix-org/matrix-react-sdk/pull/1100)
* Display a spinner until new room object after join success
[\#1099](https://github.com/matrix-org/matrix-react-sdk/pull/1099)
* Luke/attempt fix peeking regression
[\#1097](https://github.com/matrix-org/matrix-react-sdk/pull/1097)
* Show correct text in set email password dialog (2)
[\#1096](https://github.com/matrix-org/matrix-react-sdk/pull/1096)
* Don't create a guest login if user went to /login
[\#1092](https://github.com/matrix-org/matrix-react-sdk/pull/1092)
* Give password confirmation correct title, description
[\#1095](https://github.com/matrix-org/matrix-react-sdk/pull/1095)
* Make enter submit change password form
[\#1094](https://github.com/matrix-org/matrix-react-sdk/pull/1094)
* When not specified, remove roomAlias state in RoomViewStore
[\#1093](https://github.com/matrix-org/matrix-react-sdk/pull/1093)
* Update from Weblate.
[\#1091](https://github.com/matrix-org/matrix-react-sdk/pull/1091)
* Fixed pagination infinite loop caused by long messages
[\#1045](https://github.com/matrix-org/matrix-react-sdk/pull/1045)
* Clear persistent storage on login and logout
[\#1085](https://github.com/matrix-org/matrix-react-sdk/pull/1085)
* DM guessing: prefer oldest joined member
[\#1087](https://github.com/matrix-org/matrix-react-sdk/pull/1087)
* Ask for email address after setting password for the first time
[\#1090](https://github.com/matrix-org/matrix-react-sdk/pull/1090)
* i18n for setting password flow
[\#1089](https://github.com/matrix-org/matrix-react-sdk/pull/1089)
* remove mx_filterFlipColor from verified e2e icon so its not purple :/
[\#1088](https://github.com/matrix-org/matrix-react-sdk/pull/1088)
* width and height must be int otherwise synapse cries
[\#1083](https://github.com/matrix-org/matrix-react-sdk/pull/1083)
* remove RoomViewStore listener from MatrixChat on unmount
[\#1084](https://github.com/matrix-org/matrix-react-sdk/pull/1084)
* Add script to copy translations between files
[\#1082](https://github.com/matrix-org/matrix-react-sdk/pull/1082)
* Only process user_directory response if it's for the current query
[\#1081](https://github.com/matrix-org/matrix-react-sdk/pull/1081)
* Fix regressions with starting a 1-1.
[\#1080](https://github.com/matrix-org/matrix-react-sdk/pull/1080)
* allow forcing of TURN
[\#1079](https://github.com/matrix-org/matrix-react-sdk/pull/1079)
* Remove a bunch of dead code from react-sdk
[\#1077](https://github.com/matrix-org/matrix-react-sdk/pull/1077)
* Improve error logging/reporting in megolm import/export
[\#1061](https://github.com/matrix-org/matrix-react-sdk/pull/1061)
* Delinting
[\#1064](https://github.com/matrix-org/matrix-react-sdk/pull/1064)
* Show reason for a call hanging up unexpectedly.
[\#1071](https://github.com/matrix-org/matrix-react-sdk/pull/1071)
* Add reason for ban in room settings
[\#1072](https://github.com/matrix-org/matrix-react-sdk/pull/1072)
* adds mx_filterFlipColor so that the dark theme will invert this image
[\#1070](https://github.com/matrix-org/matrix-react-sdk/pull/1070)
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)
* Hotfix: Allow password reset when logged in
[\#1044](https://github.com/matrix-org/matrix-react-sdk/pull/1044)
Changes in [0.9.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.1) (2017-06-02)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.0...v0.9.1)
* Update from Weblate.
[\#1012](https://github.com/matrix-org/matrix-react-sdk/pull/1012)
* typo, missing import and mis-casing
[\#1014](https://github.com/matrix-org/matrix-react-sdk/pull/1014)
* Update from Weblate.
[\#1010](https://github.com/matrix-org/matrix-react-sdk/pull/1010)
Changes in [0.9.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.0) (2017-06-02)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.0-rc.2...v0.9.0)
* sync pt with pt_BR
[\#1009](https://github.com/matrix-org/matrix-react-sdk/pull/1009)
* Update from Weblate.
[\#1008](https://github.com/matrix-org/matrix-react-sdk/pull/1008)
* Update from Weblate.
[\#1003](https://github.com/matrix-org/matrix-react-sdk/pull/1003)
* allow hiding redactions, restoring old behaviour
[\#1004](https://github.com/matrix-org/matrix-react-sdk/pull/1004)
* Add missing translations
[\#1005](https://github.com/matrix-org/matrix-react-sdk/pull/1005)
Changes in [0.9.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.0-rc.2) (2017-06-02)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.0-rc.1...v0.9.0-rc.2)
* Update from Weblate.
[\#1002](https://github.com/matrix-org/matrix-react-sdk/pull/1002)
* webrtc config electron
[\#850](https://github.com/matrix-org/matrix-react-sdk/pull/850)
* enable useCompactLayout user setting an add a class when it's enabled
[\#986](https://github.com/matrix-org/matrix-react-sdk/pull/986)
* Update from Weblate.
[\#987](https://github.com/matrix-org/matrix-react-sdk/pull/987)
* Translation fixes for everything but src/components
[\#990](https://github.com/matrix-org/matrix-react-sdk/pull/990)
* Fix tests
[\#1001](https://github.com/matrix-org/matrix-react-sdk/pull/1001)
* Fix tests for PR #989
[\#999](https://github.com/matrix-org/matrix-react-sdk/pull/999)
* Revert "Revert "add labels to language picker""
[\#1000](https://github.com/matrix-org/matrix-react-sdk/pull/1000)
* maybe fixxy [Electron] external thing?
[\#997](https://github.com/matrix-org/matrix-react-sdk/pull/997)
* travisci: Don't run the riot-web tests if the react-sdk tests fail
[\#992](https://github.com/matrix-org/matrix-react-sdk/pull/992)
* Support 12hr time on DateSeparator
[\#991](https://github.com/matrix-org/matrix-react-sdk/pull/991)
* Revert "add labels to language picker"
[\#994](https://github.com/matrix-org/matrix-react-sdk/pull/994)
* Call MatrixClient.clearStores on logout
[\#983](https://github.com/matrix-org/matrix-react-sdk/pull/983)
* Matthew/room avatar event
[\#988](https://github.com/matrix-org/matrix-react-sdk/pull/988)
* add labels to language picker
[\#989](https://github.com/matrix-org/matrix-react-sdk/pull/989)
* Update from Weblate.
[\#981](https://github.com/matrix-org/matrix-react-sdk/pull/981)
Changes in [0.9.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.0-rc.1) (2017-06-01)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.9...v0.9.0-rc.1)
* Fix rare case where presence duration is undefined
[\#982](https://github.com/matrix-org/matrix-react-sdk/pull/982)
* add concept of platform handling loudNotifications (bings/pings/whatHaveYou)
[\#985](https://github.com/matrix-org/matrix-react-sdk/pull/985)
* Fixes to i18n code
[\#984](https://github.com/matrix-org/matrix-react-sdk/pull/984)
* Update from Weblate.
[\#978](https://github.com/matrix-org/matrix-react-sdk/pull/978)
* Add partial support for RTL languages
[\#955](https://github.com/matrix-org/matrix-react-sdk/pull/955)
* Added two strings to translate
[\#975](https://github.com/matrix-org/matrix-react-sdk/pull/975)
* Update from Weblate.
[\#976](https://github.com/matrix-org/matrix-react-sdk/pull/976)
* Update from Weblate.
[\#974](https://github.com/matrix-org/matrix-react-sdk/pull/974)
* Initial Electron Settings - for Auto Launch
[\#920](https://github.com/matrix-org/matrix-react-sdk/pull/920)
* Fix missing string in the room settings
[\#973](https://github.com/matrix-org/matrix-react-sdk/pull/973)
* fix error in i18n string
[\#972](https://github.com/matrix-org/matrix-react-sdk/pull/972)
* Update from Weblate.
[\#970](https://github.com/matrix-org/matrix-react-sdk/pull/970)
* Support 12hr time in full date
[\#971](https://github.com/matrix-org/matrix-react-sdk/pull/971)
* Add _tJsx()
[\#968](https://github.com/matrix-org/matrix-react-sdk/pull/968)
* Update from Weblate.
[\#966](https://github.com/matrix-org/matrix-react-sdk/pull/966)
* Remove space between time and AM/PM
[\#969](https://github.com/matrix-org/matrix-react-sdk/pull/969)
* Piwik Analytics
[\#948](https://github.com/matrix-org/matrix-react-sdk/pull/948)
* Update from Weblate.
[\#965](https://github.com/matrix-org/matrix-react-sdk/pull/965)
* Improve ChatInviteDialog perf by ditching fuse, using indexOf and
lastActiveTs()
[\#960](https://github.com/matrix-org/matrix-react-sdk/pull/960)
* Say "X removed the room name" instead of showing nothing
[\#958](https://github.com/matrix-org/matrix-react-sdk/pull/958)
* roomview/roomheader fixes
[\#959](https://github.com/matrix-org/matrix-react-sdk/pull/959)
* Update from Weblate.
[\#953](https://github.com/matrix-org/matrix-react-sdk/pull/953)
* fix i18n in a situation where navigator.languages=[]
[\#956](https://github.com/matrix-org/matrix-react-sdk/pull/956)
* `t_` -> `_t` fix typo
[\#957](https://github.com/matrix-org/matrix-react-sdk/pull/957)
* Change redact -> remove for clarity
[\#831](https://github.com/matrix-org/matrix-react-sdk/pull/831)
* Update from Weblate.
[\#950](https://github.com/matrix-org/matrix-react-sdk/pull/950)
* fix mis-linting - missed it in code review :(
[\#952](https://github.com/matrix-org/matrix-react-sdk/pull/952)
* i18n fixes
[\#951](https://github.com/matrix-org/matrix-react-sdk/pull/951)
* Message Forwarding
[\#812](https://github.com/matrix-org/matrix-react-sdk/pull/812)
* don't focus_composer on window focus
[\#944](https://github.com/matrix-org/matrix-react-sdk/pull/944)
* Fix vector-im/riot-web#4042
[\#947](https://github.com/matrix-org/matrix-react-sdk/pull/947)
* import _t, drop two unused imports
[\#946](https://github.com/matrix-org/matrix-react-sdk/pull/946)
* Fix punctuation in TextForEvent to be i18n'd consistently
[\#945](https://github.com/matrix-org/matrix-react-sdk/pull/945)
* actually wire up alwaysShowTimestamps
[\#940](https://github.com/matrix-org/matrix-react-sdk/pull/940)
* Update from Weblate.
[\#943](https://github.com/matrix-org/matrix-react-sdk/pull/943)
* Update from Weblate.
[\#942](https://github.com/matrix-org/matrix-react-sdk/pull/942)
* Update from Weblate.
[\#941](https://github.com/matrix-org/matrix-react-sdk/pull/941)
* Update from Weblate.
[\#938](https://github.com/matrix-org/matrix-react-sdk/pull/938)
* Fix PM being AM
[\#939](https://github.com/matrix-org/matrix-react-sdk/pull/939)
* pass call state through dispatcher, for poor electron
[\#918](https://github.com/matrix-org/matrix-react-sdk/pull/918)
* Translations!
[\#934](https://github.com/matrix-org/matrix-react-sdk/pull/934)
* Remove suffix and prefix from login input username
[\#906](https://github.com/matrix-org/matrix-react-sdk/pull/906)
* Kierangould/12hourtimestamp
[\#903](https://github.com/matrix-org/matrix-react-sdk/pull/903)
* Don't include src in the test resolve root
[\#931](https://github.com/matrix-org/matrix-react-sdk/pull/931)
* Make the linked versions open a new tab, turt2live complained :P
[\#910](https://github.com/matrix-org/matrix-react-sdk/pull/910)
* Fix lint errors in SlashCommands
[\#919](https://github.com/matrix-org/matrix-react-sdk/pull/919)
* autoFocus input box
[\#911](https://github.com/matrix-org/matrix-react-sdk/pull/911)
* Make travis test against riot-web new-guest-access
[\#917](https://github.com/matrix-org/matrix-react-sdk/pull/917)
* Add right-branch logic to travis test script
[\#916](https://github.com/matrix-org/matrix-react-sdk/pull/916)
* Group e2e keys into blocks of 4 characters
[\#914](https://github.com/matrix-org/matrix-react-sdk/pull/914)
* Factor out DeviceVerifyDialog
[\#913](https://github.com/matrix-org/matrix-react-sdk/pull/913)
* Fix 'missing page_type' error
[\#909](https://github.com/matrix-org/matrix-react-sdk/pull/909)
* code style update
[\#904](https://github.com/matrix-org/matrix-react-sdk/pull/904)
Changes in [0.8.9](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9) (2017-05-22)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.9-rc.1...v0.8.9)
* No changes
Changes in [0.8.9-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9-rc.1) (2017-05-19)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8...v0.8.9-rc.1)
* Prevent an exception getting scroll node
[\#902](https://github.com/matrix-org/matrix-react-sdk/pull/902)
* Fix a few remaining snags with country dd
[\#901](https://github.com/matrix-org/matrix-react-sdk/pull/901)
* Add left_aligned class to CountryDropdown
[\#900](https://github.com/matrix-org/matrix-react-sdk/pull/900)
* Swap to new flag files (which are stored as GB.png)
[\#899](https://github.com/matrix-org/matrix-react-sdk/pull/899)
* Improve phone number country dropdown for registration and login (Act. 2,
Return of the Prefix)
[\#897](https://github.com/matrix-org/matrix-react-sdk/pull/897)
* Support for pasting files into normal composer
[\#892](https://github.com/matrix-org/matrix-react-sdk/pull/892)
* tell guests they can't use filepanel until they register
[\#887](https://github.com/matrix-org/matrix-react-sdk/pull/887)
* Prevent reskindex -w from running when file names have not changed
[\#888](https://github.com/matrix-org/matrix-react-sdk/pull/888)
* I broke UserSettings for webpack-dev-server
[\#884](https://github.com/matrix-org/matrix-react-sdk/pull/884)
* various fixes to RoomHeader
[\#880](https://github.com/matrix-org/matrix-react-sdk/pull/880)
* remove /me whether or not it has a space after it
[\#885](https://github.com/matrix-org/matrix-react-sdk/pull/885)
* show error if we can't set a filter because no room
[\#883](https://github.com/matrix-org/matrix-react-sdk/pull/883)
* Fix RM not updating if RR event unpaginated
[\#874](https://github.com/matrix-org/matrix-react-sdk/pull/874)
* change roomsettings wording
[\#878](https://github.com/matrix-org/matrix-react-sdk/pull/878)
* make reskindex windows friendly
[\#875](https://github.com/matrix-org/matrix-react-sdk/pull/875)
* Fixes 2 issues with Dialog closing
[\#867](https://github.com/matrix-org/matrix-react-sdk/pull/867)
* Automatic Reskindex
[\#871](https://github.com/matrix-org/matrix-react-sdk/pull/871)
* Put room name in 'leave room' confirmation dialog
[\#873](https://github.com/matrix-org/matrix-react-sdk/pull/873)
* Fix this/self fail in LeftPanel
[\#872](https://github.com/matrix-org/matrix-react-sdk/pull/872)
* Don't show null URL previews
[\#870](https://github.com/matrix-org/matrix-react-sdk/pull/870)
* Fix keys for AddressSelector
[\#869](https://github.com/matrix-org/matrix-react-sdk/pull/869)
* Make left panel better for new users (mk II)
[\#859](https://github.com/matrix-org/matrix-react-sdk/pull/859)
* Explicitly save composer content onUnload
[\#866](https://github.com/matrix-org/matrix-react-sdk/pull/866)
* Warn on unload
[\#851](https://github.com/matrix-org/matrix-react-sdk/pull/851)
* Log deviceid at login
[\#862](https://github.com/matrix-org/matrix-react-sdk/pull/862)
* Guests can't send RR so no point trying
[\#860](https://github.com/matrix-org/matrix-react-sdk/pull/860)
* Remove babelcheck
[\#861](https://github.com/matrix-org/matrix-react-sdk/pull/861)
* T3chguy/settings versions improvements
[\#857](https://github.com/matrix-org/matrix-react-sdk/pull/857)
* Change max-len 90->120
[\#852](https://github.com/matrix-org/matrix-react-sdk/pull/852)
* Remove DM-guessing code
[\#829](https://github.com/matrix-org/matrix-react-sdk/pull/829)
* Fix jumping to an unread event when in MELS
[\#855](https://github.com/matrix-org/matrix-react-sdk/pull/855)
* Validate phone number on login
[\#856](https://github.com/matrix-org/matrix-react-sdk/pull/856)
* Failed to enable HTML5 Notifications Error Dialogs
[\#827](https://github.com/matrix-org/matrix-react-sdk/pull/827)
* Pin filesize ver to fix break upstream
[\#854](https://github.com/matrix-org/matrix-react-sdk/pull/854)
* Improve RoomDirectory Look & Feel
[\#848](https://github.com/matrix-org/matrix-react-sdk/pull/848)
* Only show jumpToReadMarker bar when RM !== RR
[\#845](https://github.com/matrix-org/matrix-react-sdk/pull/845)
* Allow MELS to have its own RM
[\#846](https://github.com/matrix-org/matrix-react-sdk/pull/846)
* Use document.onkeydown instead of onkeypress
[\#844](https://github.com/matrix-org/matrix-react-sdk/pull/844)
* (Room)?Avatar: Request 96x96 avatars on high DPI screens
[\#808](https://github.com/matrix-org/matrix-react-sdk/pull/808)
* Add mx_EventTile_emote class
[\#842](https://github.com/matrix-org/matrix-react-sdk/pull/842)
* Fix dialog reappearing after hitting Enter
[\#841](https://github.com/matrix-org/matrix-react-sdk/pull/841)
* Fix spinner that shows until the first sync
[\#840](https://github.com/matrix-org/matrix-react-sdk/pull/840)
* Show spinner until first sync has completed
[\#839](https://github.com/matrix-org/matrix-react-sdk/pull/839)
* Style fixes for LoggedInView
[\#838](https://github.com/matrix-org/matrix-react-sdk/pull/838)
* Fix specifying custom server for registration
[\#834](https://github.com/matrix-org/matrix-react-sdk/pull/834)
* Improve country dropdown UX and expose +prefix
[\#833](https://github.com/matrix-org/matrix-react-sdk/pull/833)
* Fix user settings store
[\#836](https://github.com/matrix-org/matrix-react-sdk/pull/836)
* show the room name in the UDE Dialog
[\#832](https://github.com/matrix-org/matrix-react-sdk/pull/832)
* summarise profile changes in MELS
[\#826](https://github.com/matrix-org/matrix-react-sdk/pull/826)
* Transform h1 and h2 tags to h3 tags
[\#820](https://github.com/matrix-org/matrix-react-sdk/pull/820)
* limit our keyboard shortcut modifiers correctly
[\#825](https://github.com/matrix-org/matrix-react-sdk/pull/825)
* Specify cross platform regexes and add olm to noParse
[\#823](https://github.com/matrix-org/matrix-react-sdk/pull/823)
* Remember element that was in focus before rendering dialog
[\#822](https://github.com/matrix-org/matrix-react-sdk/pull/822)
* move user settings outward and use built in read receipts disabling
[\#824](https://github.com/matrix-org/matrix-react-sdk/pull/824)
* File Download Consistency
[\#802](https://github.com/matrix-org/matrix-react-sdk/pull/802)
* Show Access Token under Advanced in Settings
[\#806](https://github.com/matrix-org/matrix-react-sdk/pull/806)
* Link tags/commit hashes in the UserSettings version section
[\#810](https://github.com/matrix-org/matrix-react-sdk/pull/810)
* On return to RoomView from auxPanel, send focus back to Composer
[\#813](https://github.com/matrix-org/matrix-react-sdk/pull/813)
* Change presence status labels to 'for' instead of 'ago'
[\#817](https://github.com/matrix-org/matrix-react-sdk/pull/817)
* Disable Scalar Integrations if urls passed to it are falsey
[\#816](https://github.com/matrix-org/matrix-react-sdk/pull/816)
* Add option to hide other people's read receipts.
[\#818](https://github.com/matrix-org/matrix-react-sdk/pull/818)
* Add option to not send typing notifications
[\#819](https://github.com/matrix-org/matrix-react-sdk/pull/819)
* Sync RM across instances of Riot
[\#805](https://github.com/matrix-org/matrix-react-sdk/pull/805)
* First iteration on improving login UI
[\#811](https://github.com/matrix-org/matrix-react-sdk/pull/811)
* focus on composer after jumping to bottom
[\#809](https://github.com/matrix-org/matrix-react-sdk/pull/809)
* Improve RoomList performance via side-stepping React
[\#807](https://github.com/matrix-org/matrix-react-sdk/pull/807)
* Don't show link preview when link is inside of a quote
[\#762](https://github.com/matrix-org/matrix-react-sdk/pull/762)
* Escape closes UserSettings
[\#765](https://github.com/matrix-org/matrix-react-sdk/pull/765)
* Implement user power-level changes in timeline
[\#794](https://github.com/matrix-org/matrix-react-sdk/pull/794)
Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25) Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25)
=================================================================================================== ===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8)

View file

@ -24,6 +24,10 @@ In the interim, `vector-im/riot-web` and `matrix-org/matrix-react-sdk` should
be considered as a single project (for instance, matrix-react-sdk bugs be considered as a single project (for instance, matrix-react-sdk bugs
are currently filed against vector-im/riot-web rather than this project). are currently filed against vector-im/riot-web rather than this project).
Translation Status
==================
[![translationsstatus](https://translate.nordgedanken.de/widgets/riot-web/-/multi-auto.svg)](https://translate.nordgedanken.de/engage/riot-web/?utm_source=widget)
Developer Guide Developer Guide
=============== ===============
@ -190,4 +194,3 @@ Alternative instructions:
* Create an index.html file pulling in your compiled javascript and the * Create an index.html file pulling in your compiled javascript and the
CSS bundle from the skin you use. For now, you'll also need to manually CSS bundle from the skin you use. For now, you'll also need to manually
import CSS from any skins that your skin inherts from. import CSS from any skins that your skin inherts from.

View file

@ -69,25 +69,41 @@ General Style
console.log("I am a fish"); // Bad console.log("I am a fish"); // Bad
} }
``` ```
- No new line before else, catch, finally, etc:
```javascript
if (x) {
console.log("I am a fish");
} else {
console.log("I am a chimp"); // Good
}
if (x) {
console.log("I am a fish");
}
else {
console.log("I am a chimp"); // Bad
}
```
- Declare one variable per var statement (consistent with Node). Unless they - Declare one variable per var statement (consistent with Node). Unless they
are simple and closely related. If you put the next declaration on a new line, are simple and closely related. If you put the next declaration on a new line,
treat yourself to another `var`: treat yourself to another `var`:
```javascript ```javascript
var key = "foo", const key = "foo",
comparator = function(x, y) { comparator = function(x, y) {
return x - y; return x - y;
}; // Bad }; // Bad
var key = "foo"; const key = "foo";
var comparator = function(x, y) { const comparator = function(x, y) {
return x - y; return x - y;
}; // Good }; // Good
var x = 0, y = 0; // Fine let x = 0, y = 0; // Fine
var x = 0; let x = 0;
var y = 0; // Also fine let y = 0; // Also fine
``` ```
- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.: - A single line `if` is fine, all others have braces. This prevents errors when adding to the code.:

1
header
View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -21,6 +21,11 @@ npm run test
# run eslint # run eslint
npm run lintall -- -f checkstyle -o eslint.xml || true 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 # delete the old tarball, if it exists
rm -f matrix-react-sdk-*.tgz rm -f matrix-react-sdk-*.tgz

View file

@ -55,11 +55,18 @@ module.exports = function (config) {
// some images to reduce noise from the tests // some images to reduce noise from the tests
{pattern: 'test/img/*', watched: false, included: false, {pattern: 'test/img/*', watched: false, included: false,
served: true, nocache: false}, served: true, nocache: false},
// translation files
{pattern: 'src/i18n/strings/*', watcheed: false, included: false, served: true},
{pattern: 'test/i18n/*', watched: false, included: false, served: true},
], ],
// redirect img links to the karma server
proxies: { proxies: {
// redirect img links to the karma server
"/img/": "/base/test/img/", "/img/": "/base/test/img/",
// special languages.json file for the tests
"/i18n/languages.json": "/base/test/i18n/languages.json",
// and redirect i18n requests
"/i18n/": "/base/src/i18n/strings/",
}, },
// list of files to exclude // list of files to exclude
@ -166,11 +173,15 @@ module.exports = function (config) {
'sinon': 'sinon/pkg/sinon.js', 'sinon': 'sinon/pkg/sinon.js',
}, },
root: [ root: [
path.resolve('./src'),
path.resolve('./test'), path.resolve('./test'),
], ],
}, },
devtool: 'inline-source-map', devtool: 'inline-source-map',
externals: {
// Don't try to bundle electron: leave it as a commonjs dependency
// (the 'commonjs' here means it will output a 'require')
"electron": "commonjs electron",
},
}, },
webpackMiddleware: { webpackMiddleware: {

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "0.8.8", "version": "0.9.7",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -31,9 +31,11 @@
"reskindex": "scripts/reskindex.js" "reskindex": "scripts/reskindex.js"
}, },
"scripts": { "scripts": {
"reskindex": "scripts/reskindex.js -h header", "reskindex": "node scripts/reskindex.js -h header",
"build": "babel src -d lib --source-maps", "reskindex:watch": "node scripts/reskindex.js -h header -w",
"start": "babel src -w -d lib --source-maps", "build": "npm run reskindex && babel src -d lib --source-maps",
"build:watch": "babel src -w -d lib --source-maps",
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
"lint": "eslint src/", "lint": "eslint src/",
"lintall": "eslint src/ test/", "lintall": "eslint src/ test/",
"clean": "rimraf lib", "clean": "rimraf lib",
@ -48,13 +50,15 @@
"browser-request": "^0.3.3", "browser-request": "^0.3.3",
"classnames": "^2.1.2", "classnames": "^2.1.2",
"commonmark": "^0.27.0", "commonmark": "^0.27.0",
"counterpart": "^0.18.0",
"draft-js": "^0.9.1", "draft-js": "^0.9.1",
"draft-js-export-html": "^0.5.0", "draft-js-export-html": "^0.5.0",
"draft-js-export-markdown": "^0.2.0", "draft-js-export-markdown": "^0.2.0",
"emojione": "2.2.3", "emojione": "2.2.3",
"file-saver": "^1.3.3", "file-saver": "^1.3.3",
"filesize": "3.5.6", "filesize": "3.5.6",
"flux": "^2.0.3", "flux": "2.1.1",
"fuse.js": "^2.2.0",
"glob": "^5.0.14", "glob": "^5.0.14",
"highlight.js": "^8.9.1", "highlight.js": "^8.9.1",
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
@ -68,7 +72,7 @@
"react": "^15.4.0", "react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2", "react-addons-css-transition-group": "15.3.2",
"react-dom": "^15.4.0", "react-dom": "^15.4.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#39d858c", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"sanitize-html": "^1.11.1", "sanitize-html": "^1.11.1",
"text-encoding-utf-8": "^1.0.1", "text-encoding-utf-8": "^1.0.1",
"velocity-vector": "vector-im/velocity#059e3b2", "velocity-vector": "vector-im/velocity#059e3b2",
@ -89,6 +93,7 @@
"babel-preset-es2016": "^6.11.3", "babel-preset-es2016": "^6.11.3",
"babel-preset-es2017": "^6.14.0", "babel-preset-es2017": "^6.14.0",
"babel-preset-react": "^6.11.1", "babel-preset-react": "^6.11.1",
"chokidar": "^1.6.1",
"eslint": "^3.13.1", "eslint": "^3.13.1",
"eslint-config-google": "^0.7.1", "eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^4.0.1", "eslint-plugin-babel": "^4.0.1",
@ -105,6 +110,7 @@
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^1.7.0", "karma-webpack": "^1.7.0",
"mocha": "^2.4.5", "mocha": "^2.4.5",
"parallelshell": "^1.2.0",
"phantomjs-prebuilt": "^2.1.7", "phantomjs-prebuilt": "^2.1.7",
"react-addons-test-utils": "^15.4.0", "react-addons-test-utils": "^15.4.0",
"require-json": "0.0.1", "require-json": "0.0.1",

192
scripts/check-i18n.pl Executable file
View file

@ -0,0 +1,192 @@
#!/usr/bin/perl
use strict;
use warnings;
use Cwd 'abs_path';
# script which checks how out of sync the i18ns are drifting
# example i18n format:
# "%(oneUser)sleft": "%(oneUser)sleft",
$|=1;
$0 =~ /^(.*\/)/;
my $i18ndir = abs_path($1."/../src/i18n/strings");
my $srcdir = abs_path($1."/../src");
my $en = read_i18n($i18ndir."/en_EN.json");
my $src_strings = read_src_strings($srcdir);
my $src = {};
print "Checking strings in src\n";
foreach my $tuple (@$src_strings) {
my ($s, $file) = (@$tuple);
$src->{$s} = $file;
if (!$en->{$s}) {
if ($en->{$s . '.'}) {
printf ("%50s %24s\t%s\n", $file, "en_EN has fullstop!", $s);
}
else {
$s =~ /^(.*)\.?$/;
if ($en->{$1}) {
printf ("%50s %24s\t%s\n", $file, "en_EN lacks fullstop!", $s);
}
else {
printf ("%50s %24s\t%s\n", $file, "Translation missing!", $s);
}
}
}
}
print "\nChecking en_EN\n";
my $count = 0;
my $remaining_src = {};
foreach (keys %$src) { $remaining_src->{$_}++ };
foreach my $k (sort keys %$en) {
# crappy heuristic to ignore country codes for now...
next if ($k =~ /^(..|..-..)$/);
if ($en->{$k} ne $k) {
printf ("%50s %24s\t%s\n", "en_EN", "en_EN is not symmetrical", $k);
}
if (!$src->{$k}) {
if ($src->{$k. '.'}) {
printf ("%50s %24s\t%s\n", $src->{$k. '.'}, "src has fullstop!", $k);
}
else {
$k =~ /^(.*)\.?$/;
if ($src->{$1}) {
printf ("%50s %24s\t%s\n", $src->{$1}, "src lacks fullstop!", $k);
}
else {
printf ("%50s %24s\t%s\n", '???', "Not present in src?", $k);
}
}
}
else {
$count++;
delete $remaining_src->{$k};
}
}
printf ("$count/" . (scalar keys %$src) . " strings found in src are present in en_EN\n");
foreach (keys %$remaining_src) {
print "missing: $_\n";
}
opendir(DIR, $i18ndir) || die $!;
my @files = readdir(DIR);
closedir(DIR);
foreach my $lang (grep { -f "$i18ndir/$_" && !/(basefile|en_EN)\.json/ } @files) {
print "\nChecking $lang\n";
my $map = read_i18n($i18ndir."/".$lang);
my $count = 0;
my $remaining_en = {};
foreach (keys %$en) { $remaining_en->{$_}++ };
foreach my $k (sort keys %$map) {
{
no warnings 'uninitialized';
my $vars = {};
while ($k =~ /%\((.*?)\)s/g) {
$vars->{$1}++;
}
while ($map->{$k} =~ /%\((.*?)\)s/g) {
$vars->{$1}--;
}
foreach my $var (keys %$vars) {
if ($vars->{$var} != 0) {
printf ("%10s %24s\t%s\n", $lang, "Broken var ($var)s", $k);
}
}
}
if ($en->{$k}) {
if ($map->{$k} eq $k) {
printf ("%10s %24s\t%s\n", $lang, "Untranslated string?", $k);
}
$count++;
delete $remaining_en->{$k};
}
else {
if ($en->{$k . "."}) {
printf ("%10s %24s\t%s\n", $lang, "en_EN has fullstop!", $k);
next;
}
$k =~ /^(.*)\.?$/;
if ($en->{$1}) {
printf ("%10s %24s\t%s\n", $lang, "en_EN lacks fullstop!", $k);
next;
}
printf ("%10s %24s\t%s\n", $lang, "Not present in en_EN", $k);
}
}
if (scalar keys %$remaining_en < 100) {
foreach (keys %$remaining_en) {
printf ("%10s %24s\t%s\n", $lang, "Not yet translated", $_);
}
}
printf ("$count/" . (scalar keys %$en) . " strings translated\n");
}
sub read_i18n {
my $path = shift;
my $map = {};
$path =~ /.*\/(.*)$/;
my $lang = $1;
open(FILE, "<", $path) || die $!;
while(<FILE>) {
if ($_ =~ m/^(\s+)"(.*?)"(: *)"(.*?)"(,?)$/) {
my ($indent, $src, $colon, $dst, $comma) = ($1, $2, $3, $4, $5);
$src =~ s/\\"/"/g;
$dst =~ s/\\"/"/g;
if ($map->{$src}) {
printf ("%10s %24s\t%s\n", $lang, "Duplicate translation!", $src);
}
$map->{$src} = $dst;
}
}
close(FILE);
return $map;
}
sub read_src_strings {
my $path = shift;
use File::Find;
use File::Slurp;
my $strings = [];
my @files;
find( sub { push @files, $File::Find::name if (-f $_ && /\.jsx?$/) }, $path );
foreach my $file (@files) {
my $src = read_file($file);
$src =~ s/'\s*\+\s*'//g;
$src =~ s/"\s*\+\s*"//g;
$file =~ s/^.*\/src/src/;
while ($src =~ /_t(?:Jsx)?\(\s*'(.*?[^\\])'/sg) {
my $s = $1;
$s =~ s/\\'/'/g;
push @$strings, [$s, $file];
}
while ($src =~ /_t(?:Jsx)?\(\s*"(.*?[^\\])"/sg) {
push @$strings, [$1, $file];
}
}
return $strings;
}

47
scripts/copy-i18n.py Executable file
View file

@ -0,0 +1,47 @@
#!/usr/bin/env python
import json
import sys
import os
if len(sys.argv) < 3:
print "Usage: %s <source> <dest>" % (sys.argv[0],)
print "eg. %s pt_BR.json pt.json" % (sys.argv[0],)
print
print "Adds any translations to <dest> that exist in <source> but not <dest>"
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)

114
scripts/fix-i18n.pl Executable file
View file

@ -0,0 +1,114 @@
#!/usr/bin/perl -ni
use strict;
use warnings;
# script which synchronises i18n strings to include punctuation.
# i've cherry-picked ones which seem to have diverged between the different translations
# from TextForEvent, causing missing events all over the place
BEGIN {
$::fixups = [split(/\n/, <<EOT
%(targetName)s accepted the invitation for %(displayName)s.
%(targetName)s accepted an invitation.
%(senderName)s requested a VoIP conference.
%(senderName)s invited %(targetName)s.
%(senderName)s banned %(targetName)s.
%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.
%(senderName)s set their display name to %(displayName)s.
%(senderName)s removed their display name (%(oldDisplayName)s).
%(senderName)s removed their profile picture.
%(senderName)s changed their profile picture.
%(senderName)s set a profile picture.
VoIP conference started.
%(targetName)s joined the room.
VoIP conference finished.
%(targetName)s rejected the invitation.
%(targetName)s left the room.
%(senderName)s unbanned %(targetName)s.
%(senderName)s kicked %(targetName)s.
%(senderName)s withdrew %(targetName)s's inivitation.
%(targetName)s left the room.
%(senderDisplayName)s changed the topic to "%(topic)s".
%(senderDisplayName)s changed the room name to %(roomName)s.
%(senderDisplayName)s sent an image.
%(senderName)s answered the call.
%(senderName)s ended the call.
%(senderName)s placed a %(callType)s call.
%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.
%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).
%(senderName)s changed the power level of %(powerLevelDiffText)s.
For security, this session has been signed out. Please sign in again.
You need to log back in to generate end-to-end encryption keys for this device and submit the public key to your homeserver. This is a once off; sorry for the inconvenience.
A new password must be entered.
Guests can't set avatars. Please register.
Failed to set avatar.
Unable to verify email address.
Guests can't use labs features. Please register.
A new password must be entered.
Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.
Guests cannot join this room even if explicitly invited.
Guest users can't invite users. Please register to invite.
This room is inaccessible to guests. You may be able to join if you register.
delete the alias.
remove %(name)s from the directory.
Conference call failed.
Conference calling is in development and may not be reliable.
Guest users can't create new rooms. Please register to create room and start a chat.
Server may be unavailable, overloaded, or you hit a bug.
Server unavailable, overloaded, or something else went wrong.
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
)];
}
# example i18n format:
# "%(oneUser)sleft": "%(oneUser)sleft",
# script called with the line of the file to be checked
my $sub = 0;
if ($_ =~ m/^(\s+)"(.*?)"(: *)"(.*?)"(,?)$/) {
my ($indent, $src, $colon, $dst, $comma) = ($1, $2, $3, $4, $5);
$src =~ s/\\"/"/g;
$dst =~ s/\\"/"/g;
foreach my $fixup (@{$::fixups}) {
my $dotless_fixup = substr($fixup, 0, -1);
if ($src eq $dotless_fixup) {
print STDERR "fixing up src: $src\n";
$src .= '.';
$sub = 1;
}
if ($ARGV !~ /(zh_Hans|zh_Hant|th)\.json$/ && $src eq $fixup && $dst !~ /\.$/) {
print STDERR "fixing up dst: $dst\n";
$dst .= '.';
$sub = 1;
}
if ($sub) {
$src =~ s/"/\\"/g;
$dst =~ s/"/\\"/g;
print qq($indent"$src"$colon"$dst"$comma\n);
last;
}
}
}
if (!$sub) {
print $_;
}

View 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 <<EOF
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
EOF
./node_modules/.bin/eslint --no-ignore -f json src test |
jq -r '.[] | select((.errorCount + .warningCount) > 0) | .filePath' |
sed -e 's/.*matrix-react-sdk\///';
} > "$out"

View file

@ -1,20 +1,27 @@
#!/usr/bin/env node #!/usr/bin/env node
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var glob = require('glob'); var glob = require('glob');
var args = require('optimist').argv; var args = require('optimist').argv;
var chokidar = require('chokidar');
var header = args.h || args.header;
var componentsDir = path.join('src', 'components');
var componentIndex = path.join('src', 'component-index.js'); var componentIndex = path.join('src', 'component-index.js');
var componentIndexTmp = componentIndex+".tmp";
var componentsDir = path.join('src', 'components');
var componentGlob = '**/*.js';
var prevFiles = [];
function reskindex() {
var files = glob.sync(componentGlob, {cwd: componentsDir}).sort();
if (!filesHaveChanged(files, prevFiles)) {
return;
}
prevFiles = files;
var header = args.h || args.header;
var packageJson = JSON.parse(fs.readFileSync('./package.json')); var packageJson = JSON.parse(fs.readFileSync('./package.json'));
var strm = fs.createWriteStream(componentIndex); var strm = fs.createWriteStream(componentIndexTmp);
if (header) { if (header) {
strm.write(fs.readFileSync(header)); strm.write(fs.readFileSync(header));
@ -26,18 +33,21 @@ strm.write(" * THIS FILE IS AUTO-GENERATED\n");
strm.write(" * You can edit it you like, but your changes will be overwritten,\n"); strm.write(" * You can edit it you like, but your changes will be overwritten,\n");
strm.write(" * so you'd just be trying to swim upstream like a salmon.\n"); strm.write(" * so you'd just be trying to swim upstream like a salmon.\n");
strm.write(" * You are not a salmon.\n"); strm.write(" * You are not a salmon.\n");
strm.write(" *\n");
strm.write(" * To update it, run:\n");
strm.write(" * ./reskindex.js -h header\n");
strm.write(" */\n\n"); strm.write(" */\n\n");
if (packageJson['matrix-react-parent']) { if (packageJson['matrix-react-parent']) {
strm.write("module.exports.components = require('"+packageJson['matrix-react-parent']+"/lib/component-index').components;\n\n"); const parentIndex = packageJson['matrix-react-parent'] +
'/lib/component-index';
strm.write(
`let components = require('${parentIndex}').components;
if (!components) {
throw new Error("'${parentIndex}' didn't export components");
}
`);
} else { } else {
strm.write("module.exports.components = {};\n"); strm.write("let components = {};\n");
} }
var files = glob.sync('**/*.js', {cwd: componentsDir}).sort();
for (var i = 0; i < files.length; ++i) { for (var i = 0; i < files.length; ++i) {
var file = files[i].replace('.js', ''); var file = files[i].replace('.js', '');
@ -45,9 +55,45 @@ for (var i = 0; i < files.length; ++i) {
var importName = moduleName.replace(/\./g, "$"); var importName = moduleName.replace(/\./g, "$");
strm.write("import " + importName + " from './components/" + file + "';\n"); strm.write("import " + importName + " from './components/" + file + "';\n");
strm.write(importName + " && (module.exports.components['"+moduleName+"'] = " + importName + ");"); strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");");
strm.write('\n'); strm.write('\n');
strm.uncork(); strm.uncork();
} }
strm.write("export {components};\n");
strm.end(); strm.end();
fs.rename(componentIndexTmp, componentIndex, function(err) {
if(err) {
console.error("Error moving new index into place: " + err);
} else {
console.log('Reskindex: completed');
}
});
}
// Expects both arrays of file names to be sorted
function filesHaveChanged(files, prevFiles) {
if (files.length !== prevFiles.length) {
return true;
}
// Check for name changes
for (var i = 0; i < files.length; i++) {
if (prevFiles[i] !== files[i]) {
return true;
}
}
return false;
}
// -w indicates watch mode where any FS events will trigger reskindex
if (!args.w) {
reskindex();
return;
}
var watchDebouncer = null;
chokidar.watch(path.join(componentsDir, componentGlob)).on('all', (event, path) => {
if (path === componentIndex) return;
if (watchDebouncer) clearTimeout(watchDebouncer);
watchDebouncer = setTimeout(reskindex, 1000);
});

11
scripts/travis.sh Executable file
View file

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

View file

@ -16,6 +16,7 @@ limitations under the License.
*/ */
var MatrixClientPeg = require("./MatrixClientPeg"); var MatrixClientPeg = require("./MatrixClientPeg");
import { _t } from './languageHandler';
/** /**
* Allows a user to add a third party identifier to their Home Server and, * Allows a user to add a third party identifier to their Home Server and,
@ -44,7 +45,7 @@ class AddThreepid {
return res; return res;
}, function(err) { }, function(err) {
if (err.errcode == 'M_THREEPID_IN_USE') { if (err.errcode == 'M_THREEPID_IN_USE') {
err.message = "This email address is already in use"; err.message = _t('This email address is already in use');
} else if (err.httpStatus) { } else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`; err.message = err.message + ` (Status ${err.httpStatus})`;
} }
@ -69,7 +70,7 @@ class AddThreepid {
return res; return res;
}, function(err) { }, function(err) {
if (err.errcode == 'M_THREEPID_IN_USE') { if (err.errcode == 'M_THREEPID_IN_USE') {
err.message = "This phone number is already in use"; err.message = _t('This phone number is already in use');
} else if (err.httpStatus) { } else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`; err.message = err.message + ` (Status ${err.httpStatus})`;
} }
@ -91,7 +92,7 @@ class AddThreepid {
id_server: identityServerDomain id_server: identityServerDomain
}, this.bind).catch(function(err) { }, this.bind).catch(function(err) {
if (err.httpStatus === 401) { if (err.httpStatus === 401) {
err.message = "Failed to verify email address: make sure you clicked the link in the email"; err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
} }
else if (err.httpStatus) { else if (err.httpStatus) {
err.message += ` (Status ${err.httpStatus})`; err.message += ` (Status ${err.httpStatus})`;

153
src/Analytics.js Normal file
View file

@ -0,0 +1,153 @@
/*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import { getCurrentLanguage } from './languageHandler';
import MatrixClientPeg from './MatrixClientPeg';
import PlatformPeg from './PlatformPeg';
import SdkConfig from './SdkConfig';
function getRedactedUrl() {
const redactedHash = window.location.hash.replace(/#\/(room|user)\/(.+)/, "#/$1/<redacted>");
// hardcoded url to make piwik happy
return 'https://riot.im/app/' + redactedHash;
}
const customVariables = {
'App Platform': 1,
'App Version': 2,
'User Type': 3,
'Chosen Language': 4,
'Instance': 5,
};
class Analytics {
constructor() {
this._paq = null;
this.disabled = true;
this.firstPage = true;
}
/**
* Enable Analytics if initialized but disabled
* otherwise try and initalize, no-op if piwik config missing
*/
enable() {
if (this._paq || this._init()) {
this.disabled = false;
}
}
/**
* Disable Analytics calls, will not fully unload Piwik until a refresh,
* but this is second best, Piwik should not pull anything implicitly.
*/
disable() {
this.trackEvent('Analytics', 'opt-out');
this.disabled = true;
}
_init() {
const config = SdkConfig.get();
if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return;
const url = config.piwik.url;
const siteId = config.piwik.siteId;
const self = this;
window._paq = this._paq = window._paq || [];
this._paq.push(['setTrackerUrl', url+'piwik.php']);
this._paq.push(['setSiteId', siteId]);
this._paq.push(['trackAllContentImpressions']);
this._paq.push(['discardHashTag', false]);
this._paq.push(['enableHeartBeatTimer']);
this._paq.push(['enableLinkTracking', true]);
const platform = PlatformPeg.get();
this._setVisitVariable('App Platform', platform.getHumanReadableName());
platform.getAppVersion().then((version) => {
this._setVisitVariable('App Version', version);
}).catch(() => {
this._setVisitVariable('App Version', 'unknown');
});
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];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=url+'piwik.js';
g.onload = function() {
console.log('Initialised anonymous analytics');
self._paq = window._paq;
};
s.parentNode.insertBefore(g, s);
})();
return true;
}
trackPageChange() {
if (this.disabled) return;
if (this.firstPage) {
// De-duplicate first page
// router seems to hit the fn twice
this.firstPage = false;
return;
}
this._paq.push(['setCustomUrl', getRedactedUrl()]);
this._paq.push(['trackPageView']);
}
trackEvent(category, action, name) {
if (this.disabled) return;
this._paq.push(['trackEvent', category, action, name]);
}
logout() {
if (this.disabled) return;
this._paq.push(['deleteCookies']);
}
login() { // not used currently
const cli = MatrixClientPeg.get();
if (this.disabled || !cli) return;
this._paq.push(['setUserId', `@${cli.getUserIdLocalpart()}:${cli.getDomain()}`]);
}
_setVisitVariable(key, value) {
this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']);
}
setGuest(guest) {
if (this.disabled) return;
this._setVisitVariable('User Type', guest ? 'Guest' : 'Logged In');
}
}
if (!global.mxAnalytics) {
global.mxAnalytics = new Analytics();
}
module.exports = global.mxAnalytics;

View file

@ -17,6 +17,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import dis from './dispatcher';
/** /**
* Base class for classes that provide platform-specific functionality * Base class for classes that provide platform-specific functionality
* eg. Setting an application badge or displaying notifications * eg. Setting an application badge or displaying notifications
@ -27,6 +29,21 @@ export default class BasePlatform {
constructor() { constructor() {
this.notificationCount = 0; this.notificationCount = 0;
this.errorDidOccur = false; this.errorDidOccur = false;
dis.register(this._onAction.bind(this));
}
_onAction(payload: Object) {
switch (payload.action) {
case 'on_logged_out':
this.setNotificationCount(0);
break;
}
}
// Used primarily for Analytics
getHumanReadableName(): string {
return 'Base Platform';
} }
setNotificationCount(count: number) { setNotificationCount(count: number) {
@ -66,11 +83,14 @@ export default class BasePlatform {
displayNotification(title: string, msg: string, avatarUrl: string, room: Object) { displayNotification(title: string, msg: string, avatarUrl: string, room: Object) {
} }
loudNotification(ev: Event, room: Object) {
}
/** /**
* Returns a promise that resolves to a string representing * Returns a promise that resolves to a string representing
* the current version of the application. * the current version of the application.
*/ */
getAppVersion() { getAppVersion(): Promise<string> {
throw new Error("getAppVersion not implemented!"); throw new Error("getAppVersion not implemented!");
} }
@ -79,10 +99,12 @@ export default class BasePlatform {
* with getUserMedia, return a string explaining why not. * with getUserMedia, return a string explaining why not.
* Otherwise, return null. * Otherwise, return null.
*/ */
screenCaptureErrorString() { screenCaptureErrorString(): string {
return "Not implemented"; return "Not implemented";
} }
isElectron(): boolean { return false; }
/** /**
* Restarts the application, without neccessarily reloading * Restarts the application, without neccessarily reloading
* any application code * any application code

View file

@ -51,12 +51,14 @@ limitations under the License.
* } * }
*/ */
var MatrixClientPeg = require('./MatrixClientPeg'); import MatrixClientPeg from './MatrixClientPeg';
var PlatformPeg = require("./PlatformPeg"); import UserSettingsStore from './UserSettingsStore';
var Modal = require('./Modal'); import PlatformPeg from './PlatformPeg';
var sdk = require('./index'); import Modal from './Modal';
var Matrix = require("matrix-js-sdk"); import sdk from './index';
var dis = require("./dispatcher"); import { _t } from './languageHandler';
import Matrix from 'matrix-js-sdk';
import dis from './dispatcher';
global.mxCalls = { global.mxCalls = {
//room_id: MatrixCall //room_id: MatrixCall
@ -142,8 +144,8 @@ function _setCallListeners(call) {
play("busyAudio"); play("busyAudio");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Call Timeout", title: _t('Call Timeout'),
description: "The remote side failed to pick up." description: _t('The remote side failed to pick up') + '.',
}); });
} }
else if (oldState === "invite_sent") { else if (oldState === "invite_sent") {
@ -179,7 +181,8 @@ function _setCallState(call, roomId, status) {
} }
dis.dispatch({ dis.dispatch({
action: 'call_state', action: 'call_state',
room_id: roomId room_id: roomId,
state: status,
}); });
} }
@ -203,8 +206,8 @@ function _onAction(payload) {
console.log("Can't capture screen: " + screenCapErrorString); console.log("Can't capture screen: " + screenCapErrorString);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Unable to capture screen", title: _t('Unable to capture screen'),
description: screenCapErrorString description: screenCapErrorString,
}); });
return; return;
} }
@ -223,8 +226,8 @@ function _onAction(payload) {
if (module.exports.getAnyActiveCall()) { if (module.exports.getAnyActiveCall()) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Existing Call", title: _t('Existing Call'),
description: "You are already in a call." description: _t('You are already in a call.'),
}); });
return; // don't allow >1 call to be placed. return; // don't allow >1 call to be placed.
} }
@ -233,8 +236,8 @@ function _onAction(payload) {
if (!MatrixClientPeg.get().supportsVoip()) { if (!MatrixClientPeg.get().supportsVoip()) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "VoIP is unsupported", title: _t('VoIP is unsupported'),
description: "You cannot place VoIP calls in this browser." description: _t('You cannot place VoIP calls in this browser.'),
}); });
return; return;
} }
@ -249,15 +252,15 @@ function _onAction(payload) {
if (members.length <= 1) { if (members.length <= 1) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
description: "You cannot place a call with yourself." description: _t('You cannot place a call with yourself.'),
}); });
return; return;
} }
else if (members.length === 2) { else if (members.length === 2) {
console.log("Place %s call in %s", payload.type, payload.room_id); console.log("Place %s call in %s", payload.type, payload.room_id);
var call = Matrix.createNewMatrixCall( const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, {
MatrixClientPeg.get(), payload.room_id forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false),
); });
placeCall(call); placeCall(call);
} }
else { // > 2 else { // > 2
@ -275,14 +278,14 @@ function _onAction(payload) {
if (!ConferenceHandler) { if (!ConferenceHandler) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
description: "Conference calls are not supported in this client" description: _t('Conference calls are not supported in this client'),
}); });
} }
else if (!MatrixClientPeg.get().supportsVoip()) { else if (!MatrixClientPeg.get().supportsVoip()) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "VoIP is unsupported", title: _t('VoIP is unsupported'),
description: "You cannot place VoIP calls in this browser." description: _t('You cannot place VoIP calls in this browser.'),
}); });
} }
else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) { else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) {
@ -294,14 +297,14 @@ function _onAction(payload) {
// Therefore we disable conference calling in E2E rooms. // Therefore we disable conference calling in E2E rooms.
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
description: "Conference calls are not supported in encrypted rooms", description: _t('Conference calls are not supported in encrypted rooms'),
}); });
} }
else { else {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Warning!", title: _t('Warning!'),
description: "Conference calling is in development and may not be reliable.", description: _t('Conference calling is in development and may not be reliable.'),
onFinished: confirm=>{ onFinished: confirm=>{
if (confirm) { if (confirm) {
ConferenceHandler.createNewMatrixCall( ConferenceHandler.createNewMatrixCall(
@ -312,8 +315,8 @@ function _onAction(payload) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Conference call failed: " + err); console.error("Conference call failed: " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to set up conference call", title: _t('Failed to set up conference call'),
description: "Conference call failed. " + ((err && err.message) ? err.message : ""), description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''),
}); });
}); });
} }

64
src/CallMediaHandler.js Normal file
View file

@ -0,0 +1,64 @@
/*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import UserSettingsStore from './UserSettingsStore';
import * as Matrix from 'matrix-js-sdk';
export default {
getDevices: function() {
// Only needed for Electron atm, though should work in modern browsers
// once permission has been granted to the webapp
return navigator.mediaDevices.enumerateDevices().then(function(devices) {
const audioIn = [];
const videoIn = [];
if (devices.some((device) => !device.label)) return false;
devices.forEach((device) => {
switch (device.kind) {
case 'audioinput': audioIn.push(device); break;
case 'videoinput': videoIn.push(device); break;
}
});
// console.log("Loaded WebRTC Devices", mediaDevices);
return {
audioinput: audioIn,
videoinput: videoIn,
};
}, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); });
},
loadDevices: function() {
// this.getDevices().then((devices) => {
const localSettings = UserSettingsStore.getLocalSettings();
// // if deviceId is not found, automatic fallback is in spec
// // recall previously stored inputs if any
Matrix.setMatrixCallAudioInput(localSettings['webrtc_audioinput']);
Matrix.setMatrixCallVideoInput(localSettings['webrtc_videoinput']);
// });
},
setAudioInput: function(deviceId) {
UserSettingsStore.setLocalSetting('webrtc_audioinput', deviceId);
Matrix.setMatrixCallAudioInput(deviceId);
},
setVideoInput: function(deviceId) {
UserSettingsStore.setLocalSetting('webrtc_videoinput', deviceId);
Matrix.setMatrixCallVideoInput(deviceId);
},
};

View file

@ -1,62 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
// singleton which dispatches invocations of a given type & argument
// rather than just a type (as per EventEmitter and Flux's dispatcher etc)
//
// This means you can have a single point which listens for an EventEmitter event
// and then dispatches out to one of thousands of RoomTiles (for instance) rather than
// having each RoomTile register for the EventEmitter event and having to
// iterate over all of them.
class ConstantTimeDispatcher {
constructor() {
// type -> arg -> [ listener(arg, params) ]
this.listeners = {};
}
register(type, arg, listener) {
if (!this.listeners[type]) this.listeners[type] = {};
if (!this.listeners[type][arg]) this.listeners[type][arg] = [];
this.listeners[type][arg].push(listener);
}
unregister(type, arg, listener) {
if (this.listeners[type] && this.listeners[type][arg]) {
var i = this.listeners[type][arg].indexOf(listener);
if (i > -1) {
this.listeners[type][arg].splice(i, 1);
}
}
else {
console.warn("Unregistering unrecognised listener (type=" + type + ", arg=" + arg + ")");
}
}
dispatch(type, arg, params) {
if (!this.listeners[type] || !this.listeners[type][arg]) {
//console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")");
return;
}
this.listeners[type][arg].forEach(listener=>{
listener.call(arg, params);
});
}
}
if (!global.constantTimeDispatcher) {
global.constantTimeDispatcher = new ConstantTimeDispatcher();
}
module.exports = global.constantTimeDispatcher;

View file

@ -21,6 +21,7 @@ var extend = require('./extend');
var dis = require('./dispatcher'); var dis = require('./dispatcher');
var MatrixClientPeg = require('./MatrixClientPeg'); var MatrixClientPeg = require('./MatrixClientPeg');
var sdk = require('./index'); var sdk = require('./index');
import { _t } from './languageHandler';
var Modal = require('./Modal'); var Modal = require('./Modal');
var encrypt = require("browser-encrypt-attachment"); var encrypt = require("browser-encrypt-attachment");
@ -347,14 +348,14 @@ class ContentMessages {
}, function(err) { }, function(err) {
error = err; error = err;
if (!upload.canceled) { if (!upload.canceled) {
var desc = "The file '"+upload.fileName+"' failed to upload."; var desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.';
if (err.http_status == 413) { if (err.http_status == 413) {
desc = "The file '"+upload.fileName+"' exceeds this home server's size limit for uploads"; desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
} }
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Upload Failed", title: _t('Upload Failed'),
description: desc description: desc,
}); });
} }
}).finally(() => { }).finally(() => {

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,38 +16,89 @@ limitations under the License.
*/ */
'use strict'; 'use strict';
import { _t } from './languageHandler';
var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; function getDaysArray() {
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; return [
_t('Sun'),
_t('Mon'),
_t('Tue'),
_t('Wed'),
_t('Thu'),
_t('Fri'),
_t('Sat'),
];
}
function getMonthsArray() {
return [
_t('Jan'),
_t('Feb'),
_t('Mar'),
_t('Apr'),
_t('May'),
_t('Jun'),
_t('Jul'),
_t('Aug'),
_t('Sep'),
_t('Oct'),
_t('Nov'),
_t('Dec'),
];
}
module.exports = {
formatDate: function(date) {
// date.toLocaleTimeString is completely system dependent.
// just go 24h for now
function pad(n) { function pad(n) {
return (n < 10 ? '0' : '') + n; return (n < 10 ? '0' : '') + n;
} }
function twelveHourTime(date) {
let hours = date.getHours() % 12;
const minutes = pad(date.getMinutes());
const ampm = date.getHours() >= 12 ? 'PM' : 'AM';
hours = pad(hours ? hours : 12);
return `${hours}:${minutes}${ampm}`;
}
module.exports = {
formatDate: function(date, showTwelveHour=false) {
var now = new Date(); var now = new Date();
const days = getDaysArray();
const months = getMonthsArray();
if (date.toDateString() === now.toDateString()) { if (date.toDateString() === now.toDateString()) {
return pad(date.getHours()) + ':' + pad(date.getMinutes()); return this.formatTime(date);
} }
else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); // TODO: use standard date localize function provided in counterpart
return _t('%(weekDayName)s %(time)s', {weekDayName: days[date.getDay()], time: this.formatTime(date, showTwelveHour)});
} }
else /* if (now.getFullYear() === date.getFullYear()) */ { else if (now.getFullYear() === date.getFullYear()) {
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); // TODO: use standard date localize function provided in counterpart
return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', {
weekDayName: days[date.getDay()],
monthName: months[date.getMonth()],
day: date.getDate(),
time: this.formatTime(date),
});
} }
/* return this.formatFullDate(date, showTwelveHour);
else {
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
}
*/
}, },
formatTime: function(date) { formatFullDate: function(date, showTwelveHour=false) {
//return pad(date.getHours()) + ':' + pad(date.getMinutes()); const days = getDaysArray();
return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2); const months = getMonthsArray();
} return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', {
}; weekDayName: days[date.getDay()],
monthName: months[date.getMonth()],
day: date.getDate(),
fullYear: date.getFullYear(),
time: showTwelveHour ? twelveHourTime(date) : this.formatTime(date),
});
},
formatTime: function(date, showTwelveHour=false) {
if (showTwelveHour) {
return twelveHourTime(date);
}
return pad(date.getHours()) + ':' + pad(date.getMinutes());
},
};

View file

@ -148,7 +148,7 @@ var sanitizeHtmlParams = {
attribs.href = m[1]; attribs.href = m[1];
delete attribs.target; delete attribs.target;
} }
else {
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN); m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
if (m) { if (m) {
var entity = m[1]; var entity = m[1];
@ -161,6 +161,7 @@ var sanitizeHtmlParams = {
delete attribs.target; delete attribs.target;
} }
} }
}
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
return { tagName: tagName, attribs : attribs }; return { tagName: tagName, attribs : attribs };
}, },
@ -344,6 +345,7 @@ export function bodyToHtml(content, highlights, opts) {
} }
safeBody = sanitizeHtml(body, sanitizeHtmlParams); safeBody = sanitizeHtml(body, sanitizeHtmlParams);
safeBody = unicodeToImage(safeBody); safeBody = unicodeToImage(safeBody);
safeBody = addCodeCopyButton(safeBody);
} }
finally { finally {
delete sanitizeHtmlParams.textFilter; delete sanitizeHtmlParams.textFilter;
@ -359,7 +361,24 @@ export function bodyToHtml(content, highlights, opts) {
'mx_EventTile_bigEmoji': emojiBody, 'mx_EventTile_bigEmoji': emojiBody,
'markdown-body': isHtml, 'markdown-body': isHtml,
}); });
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} />; return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" />;
}
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) { export function emojifyText(text) {

View file

@ -32,5 +32,5 @@ module.exports = {
DELETE: 46, DELETE: 46,
KEY_D: 68, KEY_D: 68,
KEY_E: 69, KEY_E: 69,
KEY_K: 75, KEY_M: 77,
}; };

138
src/KeyRequestHandler.js Normal file
View file

@ -0,0 +1,138 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import sdk from './index';
import Modal from './Modal';
export default class KeyRequestHandler {
constructor(matrixClient) {
this._matrixClient = matrixClient;
// the user/device for which we currently have a dialog open
this._currentUser = null;
this._currentDevice = null;
// userId -> deviceId -> [keyRequest]
this._pendingKeyRequests = Object.create(null);
}
handleKeyRequest(keyRequest) {
const userId = keyRequest.userId;
const deviceId = keyRequest.deviceId;
const requestId = keyRequest.requestId;
if (!this._pendingKeyRequests[userId]) {
this._pendingKeyRequests[userId] = Object.create(null);
}
if (!this._pendingKeyRequests[userId][deviceId]) {
this._pendingKeyRequests[userId][deviceId] = [];
}
// check if we already have this request
const requests = this._pendingKeyRequests[userId][deviceId];
if (requests.find((r) => r.requestId === requestId)) {
console.log("Already have this key request, ignoring");
return;
}
requests.push(keyRequest);
if (this._currentUser) {
// ignore for now
console.log("Key request, but we already have a dialog open");
return;
}
this._processNextRequest();
}
handleKeyRequestCancellation(cancellation) {
// see if we can find the request in the queue
const userId = cancellation.userId;
const deviceId = cancellation.deviceId;
const requestId = cancellation.requestId;
if (userId === this._currentUser && deviceId === this._currentDevice) {
console.log(
"room key request cancellation for the user we currently have a"
+ " dialog open for",
);
// TODO: update the dialog. For now, we just ignore the
// cancellation.
return;
}
if (!this._pendingKeyRequests[userId]) {
return;
}
const requests = this._pendingKeyRequests[userId][deviceId];
if (!requests) {
return;
}
const idx = requests.findIndex((r) => r.requestId === requestId);
if (idx < 0) {
return;
}
console.log("Forgetting room key request");
requests.splice(idx, 1);
if (requests.length === 0) {
delete this._pendingKeyRequests[userId][deviceId];
if (Object.keys(this._pendingKeyRequests[userId]).length === 0) {
delete this._pendingKeyRequests[userId];
}
}
}
_processNextRequest() {
const userId = Object.keys(this._pendingKeyRequests)[0];
if (!userId) {
return;
}
const deviceId = Object.keys(this._pendingKeyRequests[userId])[0];
if (!deviceId) {
return;
}
console.log(`Starting KeyShareDialog for ${userId}:${deviceId}`);
const finished = (r) => {
this._currentUser = null;
this._currentDevice = null;
if (r) {
for (const req of this._pendingKeyRequests[userId][deviceId]) {
req.share();
}
}
delete this._pendingKeyRequests[userId][deviceId];
if (Object.keys(this._pendingKeyRequests[userId]).length === 0) {
delete this._pendingKeyRequests[userId];
}
this._processNextRequest();
};
const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog");
Modal.createDialog(KeyShareDialog, {
matrixClient: this._matrixClient,
userId: userId,
deviceId: deviceId,
onFinished: finished,
});
this._currentUser = userId;
this._currentDevice = deviceId;
}
}

View file

@ -19,6 +19,8 @@ import q from 'q';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import createMatrixClient from './utils/createMatrixClient';
import Analytics from './Analytics';
import Notifier from './Notifier'; import Notifier from './Notifier';
import UserActivity from './UserActivity'; import UserActivity from './UserActivity';
import Presence from './Presence'; import Presence from './Presence';
@ -32,29 +34,20 @@ import sdk from './index';
* Called at startup, to attempt to build a logged-in Matrix session. It tries * Called at startup, to attempt to build a logged-in Matrix session. It tries
* a number of things: * 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 * 1. if we have a guest access token in the fragment query params, it uses
* in.
*
* 2. if we have a guest access token in the fragment query params, it uses
* that. * 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. * 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. * turn will raise on_logged_in and will_start_client events.
* *
* @param {object} opts * @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 * @param {object} opts.fragmentQueryParams: string->string map of the
* query-parameters extracted from the #-fragment of the starting URI. * query-parameters extracted from the #-fragment of the starting URI.
* *
@ -68,54 +61,38 @@ import sdk from './index';
* true; defines the IS to use. * true; defines the IS to use.
* *
* @returns {Promise} a promise which resolves when the above process completes. * @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) { export function loadSession(opts) {
const realQueryParams = opts.realQueryParams || {};
const fragmentQueryParams = opts.fragmentQueryParams || {}; const fragmentQueryParams = opts.fragmentQueryParams || {};
let enableGuest = opts.enableGuest || false; let enableGuest = opts.enableGuest || false;
const guestHsUrl = opts.guestHsUrl; const guestHsUrl = opts.guestHsUrl;
const guestIsUrl = opts.guestIsUrl; const guestIsUrl = opts.guestIsUrl;
const defaultDeviceDisplayName = opts.defaultDeviceDisplayName; 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) { if (!guestHsUrl) {
console.warn("Cannot enable guest access: can't determine HS URL to use"); console.warn("Cannot enable guest access: can't determine HS URL to use");
enableGuest = false; 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 && if (enableGuest &&
fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_user_id &&
fragmentQueryParams.guest_access_token fragmentQueryParams.guest_access_token
) { ) {
console.log("Using guest access credentials"); console.log("Using guest access credentials");
setLoggedIn({ return _doSetLoggedIn({
userId: fragmentQueryParams.guest_user_id, userId: fragmentQueryParams.guest_user_id,
accessToken: fragmentQueryParams.guest_access_token, accessToken: fragmentQueryParams.guest_access_token,
homeserverUrl: guestHsUrl, homeserverUrl: guestHsUrl,
identityServerUrl: guestIsUrl, identityServerUrl: guestIsUrl,
guest: true, guest: true,
}); }, true).then(() => true);
return q();
} }
return _restoreFromLocalStorage().then((success) => { return _restoreFromLocalStorage().then((success) => {
if (success) { if (success) {
return; return true;
} }
if (enableGuest) { if (enableGuest) {
@ -123,10 +100,30 @@ export function loadSession(opts) {
} }
// fall back to login screen // 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 // create a temporary MatrixClient to do the login
const client = Matrix.createClient({ const client = Matrix.createClient({
baseUrl: queryParams.homeserver, baseUrl: queryParams.homeserver,
@ -139,7 +136,8 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
}, },
).then(function(data) { ).then(function(data) {
console.log("Logged in with token"); console.log("Logged in with token");
setLoggedIn({ return _clearStorage().then(() => {
_persistCredentialsToLocalStorage({
userId: data.user_id, userId: data.user_id,
deviceId: data.device_id, deviceId: data.device_id,
accessToken: data.access_token, accessToken: data.access_token,
@ -147,14 +145,17 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
identityServerUrl: queryParams.identityServer, identityServerUrl: queryParams.identityServer,
guest: false, guest: false,
}); });
}, (err) => { return true;
});
}).catch((err) => {
console.error("Failed to log in with login token: " + err + " " + console.error("Failed to log in with login token: " + err + " " +
err.data); err.data);
return false;
}); });
} }
function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
console.log("Doing guest login on %s", hsUrl); console.log(`Doing guest login on ${hsUrl}`);
// TODO: we should probably de-duplicate this and Login.loginAsGuest. // TODO: we should probably de-duplicate this and Login.loginAsGuest.
// Not really sure where the right home for it is. // Not really sure where the right home for it is.
@ -169,22 +170,31 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
initial_device_display_name: defaultDeviceDisplayName, initial_device_display_name: defaultDeviceDisplayName,
}, },
}).then((creds) => { }).then((creds) => {
console.log("Registered as guest: %s", creds.user_id); console.log(`Registered as guest: ${creds.user_id}`);
setLoggedIn({ return _doSetLoggedIn({
userId: creds.user_id, userId: creds.user_id,
deviceId: creds.device_id, deviceId: creds.device_id,
accessToken: creds.access_token, accessToken: creds.access_token,
homeserverUrl: hsUrl, homeserverUrl: hsUrl,
identityServerUrl: isUrl, identityServerUrl: isUrl,
guest: true, guest: true,
}); }, true).then(() => true);
}, (err) => { }, (err) => {
console.error("Failed to register as guest: " + err + " " + err.data); console.error("Failed to register as guest: " + err + " " + err.data);
return false;
}); });
} }
// returns a promise which resolves to true if a session is found in // returns a promise which resolves to true if a session is found in
// localstorage // localstorage
//
// N.B. Lifecycle.js should not maintain any further localStorage state, we
// are moving towards using SessionStore to keep track of state related
// to the current session (which is typically backed by localStorage).
//
// The plan is to gradually move the localStorage access done here into
// SessionStore to avoid bugs where the view becomes out-of-sync with
// localStorage (e.g. teamToken, isGuest etc.)
function _restoreFromLocalStorage() { function _restoreFromLocalStorage() {
if (!localStorage) { if (!localStorage) {
return q(false); return q(false);
@ -204,17 +214,16 @@ function _restoreFromLocalStorage() {
} }
if (accessToken && userId && hsUrl) { if (accessToken && userId && hsUrl) {
console.log("Restoring session for %s", userId); console.log(`Restoring session for ${userId}`);
try { try {
setLoggedIn({ return _doSetLoggedIn({
userId: userId, userId: userId,
deviceId: deviceId, deviceId: deviceId,
accessToken: accessToken, accessToken: accessToken,
homeserverUrl: hsUrl, homeserverUrl: hsUrl,
identityServerUrl: isUrl, identityServerUrl: isUrl,
guest: isGuest, guest: isGuest,
}); }, false).then(() => true);
return q(true);
} catch (e) { } catch (e) {
return _handleRestoreFailure(e); return _handleRestoreFailure(e);
} }
@ -227,25 +236,12 @@ function _restoreFromLocalStorage() {
function _handleRestoreFailure(e) { function _handleRestoreFailure(e) {
console.log("Unable to restore session", e); console.log("Unable to restore session", e);
let msg = e.message;
if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") {
msg = "You need to log back in to generate end-to-end encryption keys "
+ "for this device and submit the public key to your homeserver. "
+ "This is a once off; sorry for the inconvenience.";
_clearLocalStorage();
return q.reject(new Error(
"Unable to restore previous session: " + msg,
));
}
const def = q.defer(); const def = q.defer();
const SessionRestoreErrorDialog = const SessionRestoreErrorDialog =
sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
Modal.createDialog(SessionRestoreErrorDialog, { Modal.createDialog(SessionRestoreErrorDialog, {
error: msg, error: e.message,
onFinished: (success) => { onFinished: (success) => {
def.resolve(success); def.resolve(success);
}, },
@ -254,7 +250,7 @@ function _handleRestoreFailure(e) {
return def.promise.then((success) => { return def.promise.then((success) => {
if (success) { if (success) {
// user clicked continue. // user clicked continue.
_clearLocalStorage(); _clearStorage();
return false; return false;
} }
@ -265,50 +261,79 @@ function _handleRestoreFailure(e) {
let rtsClient = null; let rtsClient = null;
export function initRtsClient(url) { export function initRtsClient(url) {
if (url) {
rtsClient = new RtsClient(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 * @param {MatrixClientCreds} credentials The credentials to use
*
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
*/ */
export function setLoggedIn(credentials) { export function setLoggedIn(credentials) {
stopMatrixClient();
return _doSetLoggedIn(credentials, true);
}
/**
* fires on_logging_in, optionally clears localstorage, persists new credentials
* to localstorage, starts the new client.
*
* @param {MatrixClientCreds} credentials
* @param {Boolean} clearStorage
*
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
*/
async function _doSetLoggedIn(credentials, clearStorage) {
credentials.guest = Boolean(credentials.guest); credentials.guest = Boolean(credentials.guest);
console.log( console.log(
"setLoggedIn: mxid:", credentials.userId, "setLoggedIn: mxid: " + credentials.userId +
"deviceId:", credentials.deviceId, " deviceId: " + credentials.deviceId +
"guest:", credentials.guest, " guest: " + credentials.guest +
"hs:", credentials.homeserverUrl, " hs: " + credentials.homeserverUrl,
); );
// This is dispatched to indicate that the user is still in the process of logging in // 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 // 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 // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms
// later than MatrixChat might assume. // later than MatrixChat might assume.
dis.dispatch({action: 'on_logging_in'}); dis.dispatch({action: 'on_logging_in'});
if (clearStorage) {
await _clearStorage();
}
Analytics.setGuest(credentials.guest);
// Resolves by default // Resolves by default
let teamPromise = Promise.resolve(null); let teamPromise = Promise.resolve(null);
// persist the session
if (localStorage) { if (localStorage) {
try { try {
localStorage.setItem("mx_hs_url", credentials.homeserverUrl); _persistCredentialsToLocalStorage(credentials);
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, // The user registered as a PWLU (PassWord-Less User), the generated password
// rather than setting it to "undefined". // is cached here such that the user can change it at a later time.
// if (credentials.password) {
// (in this case MatrixClient doesn't bother with the crypto stuff // Update SessionStore
// - that's fine for us). dis.dispatch({
if (credentials.deviceId) { action: 'cached_password',
localStorage.setItem("mx_device_id", credentials.deviceId); cachedPassword: credentials.password,
});
} }
console.log("Session persisted for %s", credentials.userId);
} catch (e) { } catch (e) {
console.warn("Error using local storage: can't persist session!", e); console.warn("Error using local storage: can't persist session!", e);
} }
@ -325,9 +350,6 @@ export function setLoggedIn(credentials) {
console.warn("No local storage available: can't persist session!"); 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); MatrixClientPeg.replaceUsingCreds(credentials);
teamPromise.then((teamToken) => { teamPromise.then((teamToken) => {
@ -338,6 +360,26 @@ export function setLoggedIn(credentials) {
}); });
startMatrixClient(); startMatrixClient();
return MatrixClientPeg.get();
}
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 ${credentials.userId}`);
} }
/** /**
@ -376,7 +418,7 @@ export function logout() {
* Starts the matrix client and all other react-sdk services that * Starts the matrix client and all other react-sdk services that
* listen for events while a session is logged in. * listen for events while a session is logged in.
*/ */
export function startMatrixClient() { function startMatrixClient() {
// dispatch this before starting the matrix client: it's used // dispatch this before starting the matrix client: it's used
// to add listeners for the 'sync' event so otherwise we'd have // to add listeners for the 'sync' event so otherwise we'd have
// a race condition (and we need to dispatch synchronously for this // a race condition (and we need to dispatch synchronously for this
@ -392,19 +434,22 @@ export function startMatrixClient() {
} }
/* /*
* Stops a running client and all related services, used after * Stops a running client and all related services, and clears persistent
* a session has been logged out / ended. * storage. Used after a session has been logged out.
*/ */
export function onLoggedOut() { export function onLoggedOut() {
_clearLocalStorage();
stopMatrixClient(); stopMatrixClient();
_clearStorage().done();
dis.dispatch({action: 'on_logged_out'}); dis.dispatch({action: 'on_logged_out'});
} }
function _clearLocalStorage() { /**
if (!window.localStorage) { * @returns {Promise} promise which resolves once the stores have been cleared
return; */
} function _clearStorage() {
Analytics.logout();
if (window.localStorage) {
const hsUrl = window.localStorage.getItem("mx_hs_url"); const hsUrl = window.localStorage.getItem("mx_hs_url");
const isUrl = window.localStorage.getItem("mx_is_url"); const isUrl = window.localStorage.getItem("mx_is_url");
window.localStorage.clear(); window.localStorage.clear();
@ -417,8 +462,16 @@ function _clearLocalStorage() {
if (isUrl) window.localStorage.setItem("mx_is_url", isUrl); 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() { export function stopMatrixClient() {
Notifier.stop(); Notifier.stop();
@ -429,7 +482,6 @@ export function stopMatrixClient() {
if (cli) { if (cli) {
cli.stopClient(); cli.stopClient();
cli.removeAllListeners(); cli.removeAllListeners();
cli.store.deleteAllData();
MatrixClientPeg.unset(); MatrixClientPeg.unset();
} }
} }

View file

@ -16,6 +16,7 @@ limitations under the License.
*/ */
import Matrix from "matrix-js-sdk"; import Matrix from "matrix-js-sdk";
import { _t } from "./languageHandler";
import q from 'q'; import q from 'q';
import url from 'url'; import url from 'url';
@ -96,11 +97,6 @@ export default class Login {
guest: true guest: true
}; };
}, (error) => { }, (error) => {
if (error.httpStatus === 403) {
error.friendlyText = "Guest access is disabled on this Home Server.";
} else {
error.friendlyText = "Failed to register as guest: " + error.data;
}
throw error; throw error;
}); });
} }
@ -156,15 +152,7 @@ export default class Login {
accessToken: data.access_token accessToken: data.access_token
}); });
}, function(error) { }, function(error) {
if (error.httpStatus == 400 && loginParams.medium) { if (error.httpStatus === 403) {
error.friendlyText = (
'This Home Server does not support login using email address.'
);
}
else if (error.httpStatus === 403) {
error.friendlyText = (
'Incorrect username and/or password.'
);
if (self._fallbackHsUrl) { if (self._fallbackHsUrl) {
var fbClient = Matrix.createClient({ var fbClient = Matrix.createClient({
baseUrl: self._fallbackHsUrl, baseUrl: self._fallbackHsUrl,
@ -185,21 +173,23 @@ export default class Login {
}); });
} }
} }
else {
error.friendlyText = (
'There was a problem logging in. (HTTP ' + error.httpStatus + ")"
);
}
throw error; throw error;
}); });
} }
redirectToCas() { redirectToCas() {
var client = this._createTemporaryClient(); const client = this._createTemporaryClient();
var parsedUrl = url.parse(window.location.href, true); 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["homeserver"] = client.getHomeserverUrl();
parsedUrl.query["identityServer"] = client.getIdentityServerUrl(); parsedUrl.query["identityServer"] = client.getIdentityServerUrl();
var casUrl = client.getCasLoginUrl(url.format(parsedUrl)); const casUrl = client.getCasLoginUrl(url.format(parsedUrl));
window.location.href = casUrl; window.location.href = casUrl;
} }
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,13 +17,10 @@ limitations under the License.
'use strict'; 'use strict';
import q from "q";
import Matrix from 'matrix-js-sdk';
import utils from 'matrix-js-sdk/lib/utils'; import utils from 'matrix-js-sdk/lib/utils';
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline'; import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set'; import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
import createMatrixClient from './utils/createMatrixClient';
const localStorage = window.localStorage;
interface MatrixClientCreds { interface MatrixClientCreds {
homeserverUrl: string, homeserverUrl: string,
@ -50,7 +48,6 @@ class MatrixClientPeg {
this.opts = { this.opts = {
initialSyncLimit: 20, initialSyncLimit: 20,
}; };
this.indexedDbWorkerScript = null;
} }
/** /**
@ -61,7 +58,7 @@ class MatrixClientPeg {
* @param {string} script href to the script to be passed to the web worker * @param {string} script href to the script to be passed to the web worker
*/ */
setIndexedDbWorkerScript(script) { setIndexedDbWorkerScript(script) {
this.indexedDbWorkerScript = script; createMatrixClient.indexedDbWorkerScript = script;
} }
get(): MatrixClient { get(): MatrixClient {
@ -87,7 +84,9 @@ class MatrixClientPeg {
let promise = this.matrixClient.store.startup(); let promise = this.matrixClient.store.startup();
// log any errors when starting up the database (if one exists) // log any errors when starting up the database (if one exists)
promise.catch((err) => { console.error(err); }); promise.catch((err) => {
console.error(`Error starting matrixclient store: ${err}`);
});
// regardless of errors, start the client. If we did error out, we'll // regardless of errors, start the client. If we did error out, we'll
// just end up doing a full initial /sync. // just end up doing a full initial /sync.
@ -130,22 +129,7 @@ class MatrixClientPeg {
timelineSupport: true, timelineSupport: true,
}; };
if (localStorage) { this.matrixClient = createMatrixClient(opts, this.indexedDbWorkerScript);
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);
// we're going to add eventlisteners for each matrix event tile, so the // we're going to add eventlisteners for each matrix event tile, so the
// potential number of event listeners is quite high. // potential number of event listeners is quite high.

View file

@ -19,6 +19,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var ReactDOM = require('react-dom'); var ReactDOM = require('react-dom');
import Analytics from './Analytics';
import sdk from './index'; import sdk from './index';
const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
@ -63,7 +64,6 @@ const AsyncWrapper = React.createClass({
render: function() { render: function() {
const {loader, ...otherProps} = this.props; const {loader, ...otherProps} = this.props;
if (this.state.component) { if (this.state.component) {
const Component = this.state.component; const Component = this.state.component;
return <Component {...otherProps} />; return <Component {...otherProps} />;
@ -104,6 +104,9 @@ class ModalManager {
} }
createDialog(Element, props, className) { createDialog(Element, props, className) {
if (props && props.title) {
Analytics.trackEvent('Modal', props.title, 'createDialog');
}
return this.createDialogAsync((cb) => {cb(Element);}, props, className); return this.createDialogAsync((cb) => {cb(Element);}, props, className);
} }
@ -195,4 +198,7 @@ class ModalManager {
} }
} }
export default new ModalManager(); if (!global.singletonModalManager) {
global.singletonModalManager = new ModalManager();
}
export default global.singletonModalManager;

View file

@ -18,9 +18,11 @@ limitations under the License.
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import PlatformPeg from './PlatformPeg'; import PlatformPeg from './PlatformPeg';
import TextForEvent from './TextForEvent'; import TextForEvent from './TextForEvent';
import Analytics from './Analytics';
import Avatar from './Avatar'; import Avatar from './Avatar';
import dis from './dispatcher'; import dis from './dispatcher';
import sdk from './index'; import sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
/* /*
@ -120,6 +122,9 @@ const Notifier = {
setEnabled: function(enable, callback) { setEnabled: function(enable, callback) {
const plaf = PlatformPeg.get(); const plaf = PlatformPeg.get();
if (!plaf) return; if (!plaf) return;
Analytics.trackEvent('Notifier', 'Set Enabled', enable);
// make sure that we persist the current setting audio_enabled setting // make sure that we persist the current setting audio_enabled setting
// before changing anything // before changing anything
if (global.localStorage) { if (global.localStorage) {
@ -134,13 +139,11 @@ const Notifier = {
if (result !== 'granted') { if (result !== 'granted') {
// The permission request was dismissed or denied // The permission request was dismissed or denied
const description = result === 'denied' const description = result === 'denied'
? 'Riot does not have permission to send you notifications' ? _t('Riot does not have permission to send you notifications - please check your browser settings')
+ ' - please check your browser settings' : _t('Riot was not given permission to send notifications - please try again');
: 'Riot was not given permission to send notifications'
+ ' - please try again';
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: 'Unable to enable Notifications', title: _t('Unable to enable Notifications'),
description, description,
}); });
return; return;
@ -200,6 +203,8 @@ const Notifier = {
setToolbarHidden: function(hidden, persistent = true) { setToolbarHidden: function(hidden, persistent = true) {
this.toolbarHidden = hidden; this.toolbarHidden = hidden;
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
// XXX: why are we dispatching this here? // XXX: why are we dispatching this here?
// this is nothing to do with notifier_enabled // this is nothing to do with notifier_enabled
dis.dispatch({ dis.dispatch({
@ -245,6 +250,7 @@ const Notifier = {
this._displayPopupNotification(ev, room); this._displayPopupNotification(ev, room);
} }
if (actions.tweaks.sound && this.isAudioEnabled()) { if (actions.tweaks.sound && this.isAudioEnabled()) {
PlatformPeg.get().loudNotification(ev, room);
this._playAudioNotification(ev, room); this._playAudioNotification(ev, room);
} }
} }

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
var Matrix = require("matrix-js-sdk"); var Matrix = require("matrix-js-sdk");
import { _t } from './languageHandler';
/** /**
* Allows a user to reset their password on a homeserver. * Allows a user to reset their password on a homeserver.
@ -53,7 +54,7 @@ class PasswordReset {
return res; return res;
}, function(err) { }, function(err) {
if (err.errcode == 'M_THREEPID_NOT_FOUND') { if (err.errcode == 'M_THREEPID_NOT_FOUND') {
err.message = "This email address was not found"; err.message = _t('This email address was not found');
} else if (err.httpStatus) { } else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`; err.message = err.message + ` (Status ${err.httpStatus})`;
} }
@ -78,10 +79,10 @@ class PasswordReset {
} }
}, this.password).catch(function(err) { }, this.password).catch(function(err) {
if (err.httpStatus === 401) { if (err.httpStatus === 401) {
err.message = "Failed to verify email address: make sure you clicked the link in the email"; err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
} }
else if (err.httpStatus === 404) { else if (err.httpStatus === 404) {
err.message = "Your email address does not appear to be associated with a Matrix ID on this Homeserver."; err.message = _t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.');
} }
else if (err.httpStatus) { else if (err.httpStatus) {
err.message += ` (Status ${err.httpStatus})`; err.message += ` (Status ${err.httpStatus})`;

View file

@ -13,14 +13,19 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export const LEVEL_ROLE_MAP = { import { _t } from './languageHandler';
undefined: 'Default',
0: 'User', export function levelRoleMap() {
50: 'Moderator', return {
100: 'Admin', undefined: _t('Default'),
0: _t('User'),
50: _t('Moderator'),
100: _t('Admin'),
}; };
}
export function textualPowerLevel(level, userDefault) { export function textualPowerLevel(level, userDefault) {
const LEVEL_ROLE_MAP = this.levelRoleMap();
if (LEVEL_ROLE_MAP[level]) { if (LEVEL_ROLE_MAP[level]) {
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`);
} else { } else {

View file

@ -15,7 +15,6 @@ limitations under the License.
*/ */
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import DMRoomMap from './utils/DMRoomMap';
import q from 'q'; import q from 'q';
/** /**
@ -145,7 +144,18 @@ export function guessDMRoomTarget(room, me) {
let oldestTs; let oldestTs;
let oldestUser; 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()) { for (const user of room.currentState.getMembers()) {
if (user.userId == me.userId) continue; if (user.userId == me.userId) continue;

View file

@ -1,5 +1,7 @@
import 'whatwg-fetch'; import 'whatwg-fetch';
let fetchFunction = fetch;
function checkStatus(response) { function checkStatus(response) {
if (!response.ok) { if (!response.ok) {
return response.text().then((text) => { return response.text().then((text) => {
@ -31,7 +33,7 @@ const request = (url, opts) => {
opts.body = JSON.stringify(opts.body); opts.body = JSON.stringify(opts.body);
opts.headers['Content-Type'] = 'application/json'; opts.headers['Content-Type'] = 'application/json';
} }
return fetch(url, opts) return fetchFunction(url, opts)
.then(checkStatus) .then(checkStatus)
.then(parseJson); .then(parseJson);
}; };
@ -64,7 +66,7 @@ export default class RtsClient {
client_secret: clientSecret, client_secret: clientSecret,
}, },
method: 'POST', method: 'POST',
} },
); );
} }
@ -74,7 +76,7 @@ export default class RtsClient {
qs: { qs: {
team_token: teamToken, team_token: teamToken,
}, },
} },
); );
} }
@ -91,7 +93,12 @@ export default class RtsClient {
qs: { qs: {
user_id: userId, user_id: userId,
}, },
} },
); );
} }
// allow fetch to be replaced, for testing.
static setFetch(fn) {
fetchFunction = fn;
}
} }

View file

@ -94,6 +94,22 @@ Example:
} }
} }
get_membership_count
--------------------
Get the number of joined users in the room.
Request:
- room_id is the room to get the count in.
Response:
78
Example:
{
action: "get_membership_count",
room_id: "!foo:bar",
response: 78
}
membership_state AND bot_options membership_state AND bot_options
-------------------------------- --------------------------------
Get the content of the "m.room.member" or "m.room.bot.options" state event respectively. Get the content of the "m.room.member" or "m.room.bot.options" state event respectively.
@ -125,6 +141,7 @@ const SdkConfig = require('./SdkConfig');
const MatrixClientPeg = require("./MatrixClientPeg"); const MatrixClientPeg = require("./MatrixClientPeg");
const MatrixEvent = require("matrix-js-sdk").MatrixEvent; const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
const dis = require("./dispatcher"); const dis = require("./dispatcher");
import { _t } from './languageHandler';
function sendResponse(event, res) { function sendResponse(event, res) {
const data = JSON.parse(JSON.stringify(event.data)); const data = JSON.parse(JSON.stringify(event.data));
@ -150,7 +167,7 @@ function inviteUser(event, roomId, userId) {
console.log(`Received request to invite ${userId} into room ${roomId}`); console.log(`Received request to invite ${userId} into room ${roomId}`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, "You need to be logged in."); sendError(event, _t('You need to be logged in.'));
return; return;
} }
const room = client.getRoom(roomId); const room = client.getRoom(roomId);
@ -170,7 +187,7 @@ function inviteUser(event, roomId, userId) {
success: true, success: true,
}); });
}, function(err) { }, function(err) {
sendError(event, "You need to be able to invite users to do that.", err); sendError(event, _t('You need to be able to invite users to do that.'), err);
}); });
} }
@ -181,7 +198,7 @@ function setPlumbingState(event, roomId, status) {
console.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`); console.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, "You need to be logged in."); sendError(event, _t('You need to be logged in.'));
return; return;
} }
client.sendStateEvent(roomId, "m.room.plumbing", { status : status }).done(() => { client.sendStateEvent(roomId, "m.room.plumbing", { status : status }).done(() => {
@ -189,7 +206,7 @@ function setPlumbingState(event, roomId, status) {
success: true, success: true,
}); });
}, (err) => { }, (err) => {
sendError(event, err.message ? err.message : "Failed to send request.", err); sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
}); });
} }
@ -197,7 +214,7 @@ function setBotOptions(event, roomId, userId) {
console.log(`Received request to set options for bot ${userId} in room ${roomId}`); console.log(`Received request to set options for bot ${userId} in room ${roomId}`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, "You need to be logged in."); sendError(event, _t('You need to be logged in.'));
return; return;
} }
client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => { client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => {
@ -205,20 +222,20 @@ function setBotOptions(event, roomId, userId) {
success: true, success: true,
}); });
}, (err) => { }, (err) => {
sendError(event, err.message ? err.message : "Failed to send request.", err); sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
}); });
} }
function setBotPower(event, roomId, userId, level) { function setBotPower(event, roomId, userId, level) {
if (!(Number.isInteger(level) && level >= 0)) { if (!(Number.isInteger(level) && level >= 0)) {
sendError(event, "Power level must be positive integer."); sendError(event, _t('Power level must be positive integer.'));
return; return;
} }
console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`); console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, "You need to be logged in."); sendError(event, _t('You need to be logged in.'));
return; return;
} }
@ -235,7 +252,7 @@ function setBotPower(event, roomId, userId, level) {
success: true, success: true,
}); });
}, (err) => { }, (err) => {
sendError(event, err.message ? err.message : "Failed to send request.", err); sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
}); });
}); });
} }
@ -255,15 +272,30 @@ function botOptions(event, roomId, userId) {
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId); returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
} }
function returnStateEvent(event, roomId, eventType, stateKey) { function getMembershipCount(event, roomId) {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, "You need to be logged in."); sendError(event, _t('You need to be logged in.'));
return; return;
} }
const room = client.getRoom(roomId); const room = client.getRoom(roomId);
if (!room) { if (!room) {
sendError(event, "This room is not recognised."); sendError(event, _t('This room is not recognised.'));
return;
}
const count = room.getJoinedMembers().length;
sendResponse(event, count);
}
function returnStateEvent(event, roomId, eventType, stateKey) {
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
return;
}
const room = client.getRoom(roomId);
if (!room) {
sendError(event, _t('This room is not recognised.'));
return; return;
} }
const stateEvent = room.currentState.getStateEvents(eventType, stateKey); const stateEvent = room.currentState.getStateEvents(eventType, stateKey);
@ -313,13 +345,13 @@ const onMessage = function(event) {
const roomId = event.data.room_id; const roomId = event.data.room_id;
const userId = event.data.user_id; const userId = event.data.user_id;
if (!roomId) { if (!roomId) {
sendError(event, "Missing room_id in request"); sendError(event, _t('Missing room_id in request'));
return; return;
} }
let promise = Promise.resolve(currentRoomId); let promise = Promise.resolve(currentRoomId);
if (!currentRoomId) { if (!currentRoomId) {
if (!currentRoomAlias) { if (!currentRoomAlias) {
sendError(event, "Must be viewing a room"); sendError(event, _t('Must be viewing a room'));
return; return;
} }
// no room ID but there is an alias, look it up. // no room ID but there is an alias, look it up.
@ -331,7 +363,7 @@ const onMessage = function(event) {
promise.then((viewingRoomId) => { promise.then((viewingRoomId) => {
if (roomId !== viewingRoomId) { if (roomId !== viewingRoomId) {
sendError(event, "Room " + roomId + " not visible"); sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
return; return;
} }
@ -342,10 +374,13 @@ const onMessage = function(event) {
} else if (event.data.action === "set_plumbing_state") { } else if (event.data.action === "set_plumbing_state") {
setPlumbingState(event, roomId, event.data.status); setPlumbingState(event, roomId, event.data.status);
return; return;
} else if (event.data.action === "get_membership_count") {
getMembershipCount(event, roomId);
return;
} }
if (!userId) { if (!userId) {
sendError(event, "Missing user_id in request"); sendError(event, _t('Missing user_id in request'));
return; return;
} }
switch (event.data.action) { switch (event.data.action) {
@ -370,7 +405,7 @@ const onMessage = function(event) {
} }
}, (err) => { }, (err) => {
console.error(err); console.error(err);
sendError(event, "Failed to lookup current room."); sendError(event, _t('Failed to lookup current room') + '.');
}); });
}; };

View file

@ -25,22 +25,28 @@ class Skinner {
"Attempted to get a component before a skin has been loaded."+ "Attempted to get a component before a skin has been loaded."+
" This is probably because either:"+ " This is probably because either:"+
" a) Your app has not called sdk.loadSkin(), or"+ " a) Your app has not called sdk.loadSkin(), or"+
" b) A component has called getComponent at the root level" " b) A component has called getComponent at the root level",
); );
} }
var comp = this.components[name]; let comp = this.components[name];
if (comp) {
return comp;
}
// XXX: Temporarily also try 'views.' as we're currently // XXX: Temporarily also try 'views.' as we're currently
// leaving the 'views.' off views. // leaving the 'views.' off views.
var comp = this.components['views.'+name]; if (!comp) {
if (comp) { comp = this.components['views.'+name];
return comp;
} }
if (!comp) {
throw new Error("No such component: "+name); throw new Error("No such component: "+name);
} }
// components have to be functions.
const validType = typeof comp === 'function';
if (!validType) {
throw new Error(`Not a valid component: ${name}.`);
}
return comp;
}
load(skinObject) { load(skinObject) {
if (this.components !== null) { if (this.components !== null) {
throw new Error( throw new Error(

View file

@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var MatrixClientPeg = require("./MatrixClientPeg"); import MatrixClientPeg from "./MatrixClientPeg";
var dis = require("./dispatcher"); import dis from "./dispatcher";
var Tinter = require("./Tinter"); import Tinter from "./Tinter";
import sdk from './index'; import sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
@ -41,58 +42,64 @@ class Command {
} }
getUsage() { getUsage() {
return "Usage: " + this.getCommandWithArgs(); return _t('Usage') + ': ' + this.getCommandWithArgs();
} }
} }
var reject = function(msg) { function reject(msg) {
return { return {
error: msg error: msg,
};
}; };
}
var success = function(promise) { function success(promise) {
return { return {
promise: promise promise: promise,
};
}; };
}
var commands = { /* Disable the "unexpected this" error for these commands - all of the run
* functions are called with `this` bound to the Command instance.
*/
/* eslint-disable babel/no-invalid-this */
const commands = {
ddg: new Command("ddg", "<query>", function(roomId, args) { ddg: new Command("ddg", "<query>", function(roomId, args) {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
// TODO Don't explain this away, actually show a search UI here. // TODO Don't explain this away, actually show a search UI here.
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "/ddg is not a command", title: _t('/ddg is not a command'),
description: "To use it, just wait for autocomplete results to load and tab through them.", description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
}); });
return success(); return success();
}), }),
// Change your nickname // Change your nickname
nick: new Command("nick", "<display_name>", function(room_id, args) { nick: new Command("nick", "<display_name>", function(roomId, args) {
if (args) { if (args) {
return success( return success(
MatrixClientPeg.get().setDisplayName(args) MatrixClientPeg.get().setDisplayName(args),
); );
} }
return reject(this.getUsage()); return reject(this.getUsage());
}), }),
// Changes the colorscheme of your current room // Changes the colorscheme of your current room
tint: new Command("tint", "<color1> [<color2>]", function(room_id, args) { tint: new Command("tint", "<color1> [<color2>]", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); const matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
if (matches) { if (matches) {
Tinter.tint(matches[1], matches[4]); Tinter.tint(matches[1], matches[4]);
var colorScheme = {}; const colorScheme = {};
colorScheme.primary_color = matches[1]; colorScheme.primary_color = matches[1];
if (matches[4]) { if (matches[4]) {
colorScheme.secondary_color = matches[4]; colorScheme.secondary_color = matches[4];
} }
return success( return success(
MatrixClientPeg.get().setRoomAccountData( MatrixClientPeg.get().setRoomAccountData(
room_id, "org.matrix.room.color_scheme", colorScheme roomId, "org.matrix.room.color_scheme", colorScheme,
) ),
); );
} }
} }
@ -100,22 +107,22 @@ var commands = {
}), }),
// Change the room topic // Change the room topic
topic: new Command("topic", "<topic>", function(room_id, args) { topic: new Command("topic", "<topic>", function(roomId, args) {
if (args) { if (args) {
return success( return success(
MatrixClientPeg.get().setRoomTopic(room_id, args) MatrixClientPeg.get().setRoomTopic(roomId, args),
); );
} }
return reject(this.getUsage()); return reject(this.getUsage());
}), }),
// Invite a user // Invite a user
invite: new Command("invite", "<userId>", function(room_id, args) { invite: new Command("invite", "<userId>", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+)$/); const matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
return success( return success(
MatrixClientPeg.get().invite(room_id, matches[1]) MatrixClientPeg.get().invite(roomId, matches[1]),
); );
} }
} }
@ -123,21 +130,21 @@ var commands = {
}), }),
// Join a room // Join a room
join: new Command("join", "#alias:domain", function(room_id, args) { join: new Command("join", "#alias:domain", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+)$/); const matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
var room_alias = matches[1]; let roomAlias = matches[1];
if (room_alias[0] !== '#') { if (roomAlias[0] !== '#') {
return reject(this.getUsage()); return reject(this.getUsage());
} }
if (!room_alias.match(/:/)) { if (!roomAlias.match(/:/)) {
room_alias += ':' + MatrixClientPeg.get().getDomain(); roomAlias += ':' + MatrixClientPeg.get().getDomain();
} }
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
room_alias: room_alias, room_alias: roomAlias,
auto_join: true, auto_join: true,
}); });
@ -147,29 +154,29 @@ var commands = {
return reject(this.getUsage()); return reject(this.getUsage());
}), }),
part: new Command("part", "[#alias:domain]", function(room_id, args) { part: new Command("part", "[#alias:domain]", function(roomId, args) {
var targetRoomId; let targetRoomId;
if (args) { if (args) {
var matches = args.match(/^(\S+)$/); const matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
var room_alias = matches[1]; let roomAlias = matches[1];
if (room_alias[0] !== '#') { if (roomAlias[0] !== '#') {
return reject(this.getUsage()); return reject(this.getUsage());
} }
if (!room_alias.match(/:/)) { if (!roomAlias.match(/:/)) {
room_alias += ':' + MatrixClientPeg.get().getDomain(); roomAlias += ':' + MatrixClientPeg.get().getDomain();
} }
// Try to find a room with this alias // Try to find a room with this alias
var rooms = MatrixClientPeg.get().getRooms(); const rooms = MatrixClientPeg.get().getRooms();
for (var i = 0; i < rooms.length; i++) { for (let i = 0; i < rooms.length; i++) {
var aliasEvents = rooms[i].currentState.getStateEvents( const aliasEvents = rooms[i].currentState.getStateEvents(
"m.room.aliases" "m.room.aliases",
); );
for (var j = 0; j < aliasEvents.length; j++) { for (let j = 0; j < aliasEvents.length; j++) {
var aliases = aliasEvents[j].getContent().aliases || []; const aliases = aliasEvents[j].getContent().aliases || [];
for (var k = 0; k < aliases.length; k++) { for (let k = 0; k < aliases.length; k++) {
if (aliases[k] === room_alias) { if (aliases[k] === roomAlias) {
targetRoomId = rooms[i].roomId; targetRoomId = rooms[i].roomId;
break; break;
} }
@ -178,27 +185,28 @@ var commands = {
} }
if (targetRoomId) { break; } if (targetRoomId) { break; }
} }
}
if (!targetRoomId) { if (!targetRoomId) {
return reject("Unrecognised room alias: " + room_alias); return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
} }
} }
if (!targetRoomId) targetRoomId = room_id; }
if (!targetRoomId) targetRoomId = roomId;
return success( return success(
MatrixClientPeg.get().leave(targetRoomId).then( MatrixClientPeg.get().leave(targetRoomId).then(
function() { function() {
dis.dispatch({action: 'view_next_room'}); dis.dispatch({action: 'view_next_room'});
}) },
),
); );
}), }),
// Kick a user from the room with an optional reason // Kick a user from the room with an optional reason
kick: new Command("kick", "<userId> [<reason>]", function(room_id, args) { kick: new Command("kick", "<userId> [<reason>]", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+?)( +(.*))?$/); const matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) { if (matches) {
return success( return success(
MatrixClientPeg.get().kick(room_id, matches[1], matches[3]) MatrixClientPeg.get().kick(roomId, matches[1], matches[3]),
); );
} }
} }
@ -206,12 +214,12 @@ var commands = {
}), }),
// Ban a user from the room with an optional reason // Ban a user from the room with an optional reason
ban: new Command("ban", "<userId> [<reason>]", function(room_id, args) { ban: new Command("ban", "<userId> [<reason>]", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+?)( +(.*))?$/); const matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) { if (matches) {
return success( return success(
MatrixClientPeg.get().ban(room_id, matches[1], matches[3]) MatrixClientPeg.get().ban(roomId, matches[1], matches[3]),
); );
} }
} }
@ -219,13 +227,13 @@ var commands = {
}), }),
// Unban a user from the room // Unban a user from the room
unban: new Command("unban", "<userId>", function(room_id, args) { unban: new Command("unban", "<userId>", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+)$/); const matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
// Reset the user membership to "leave" to unban him // Reset the user membership to "leave" to unban him
return success( return success(
MatrixClientPeg.get().unban(room_id, matches[1]) MatrixClientPeg.get().unban(roomId, matches[1]),
); );
} }
} }
@ -233,27 +241,27 @@ var commands = {
}), }),
// Define the power level of a user // Define the power level of a user
op: new Command("op", "<userId> [<power level>]", function(room_id, args) { op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+?)( +(\d+))?$/); const matches = args.match(/^(\S+?)( +(\d+))?$/);
var powerLevel = 50; // default power level for op let powerLevel = 50; // default power level for op
if (matches) { if (matches) {
var user_id = matches[1]; const userId = matches[1];
if (matches.length === 4 && undefined !== matches[3]) { if (matches.length === 4 && undefined !== matches[3]) {
powerLevel = parseInt(matches[3]); powerLevel = parseInt(matches[3]);
} }
if (powerLevel !== NaN) { if (!isNaN(powerLevel)) {
var room = MatrixClientPeg.get().getRoom(room_id); const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) { if (!room) {
return reject("Bad room ID: " + room_id); return reject("Bad room ID: " + roomId);
} }
var powerLevelEvent = room.currentState.getStateEvents( const powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "" "m.room.power_levels", "",
); );
return success( return success(
MatrixClientPeg.get().setPowerLevel( MatrixClientPeg.get().setPowerLevel(
room_id, user_id, powerLevel, powerLevelEvent roomId, userId, powerLevel, powerLevelEvent,
) ),
); );
} }
} }
@ -262,32 +270,94 @@ var commands = {
}), }),
// Reset the power level of a user // Reset the power level of a user
deop: new Command("deop", "<userId>", function(room_id, args) { deop: new Command("deop", "<userId>", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+)$/); const matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
var room = MatrixClientPeg.get().getRoom(room_id); const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) { if (!room) {
return reject("Bad room ID: " + room_id); return reject("Bad room ID: " + roomId);
} }
var powerLevelEvent = room.currentState.getStateEvents( const powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "" "m.room.power_levels", "",
); );
return success( return success(
MatrixClientPeg.get().setPowerLevel( MatrixClientPeg.get().setPowerLevel(
room_id, args, undefined, powerLevelEvent roomId, args, undefined, powerLevelEvent,
) ),
); );
} }
} }
return reject(this.getUsage()); return reject(this.getUsage());
}) }),
// Verify a user, device, and pubkey tuple
verify: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
if (matches) {
const userId = matches[1];
const deviceId = matches[2];
const fingerprint = matches[3];
const device = MatrixClientPeg.get().getStoredDevice(userId, deviceId);
if (!device) {
return reject(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`);
}
if (device.isVerified()) {
if (device.getFingerprint() === fingerprint) {
return reject(_t(`Device already verified!`));
} else {
return reject(_t(`WARNING: Device already verified, but keys do NOT MATCH!`));
}
}
if (device.getFingerprint() === fingerprint) {
MatrixClientPeg.get().setDeviceVerified(
userId, deviceId, true,
);
// Tell the user we verified everything!
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: _t("Verified key"),
description: (
<div>
<p>
{
_t("The signing key you provided matches the signing key you received " +
"from %(userId)s's device %(deviceId)s. Device marked as verified.",
{userId: userId, deviceId: deviceId})
}
</p>
</div>
),
hasCancelButton: false,
});
return success();
} else {
const fprint = device.getFingerprint();
return reject(
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
' %(deviceId)s is "%(fprint)s" which does not match the provided key' +
' "%(fingerprint)s". This could mean your communications are being intercepted!',
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint})
);
}
}
}
return reject(this.getUsage());
}),
}; };
/* eslint-enable babel/no-invalid-this */
// helpful aliases // helpful aliases
var aliases = { const aliases = {
j: "join" j: "join",
}; };
module.exports = { module.exports = {
@ -304,13 +374,13 @@ module.exports = {
// IRC-style commands // IRC-style commands
input = input.replace(/\s+$/, ""); input = input.replace(/\s+$/, "");
if (input[0] === "/" && input[1] !== "/") { if (input[0] === "/" && input[1] !== "/") {
var bits = input.match(/^(\S+?)( +((.|\n)*))?$/); const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
var cmd, args; let cmd;
let args;
if (bits) { if (bits) {
cmd = bits[1].substring(1).toLowerCase(); cmd = bits[1].substring(1).toLowerCase();
args = bits[3]; args = bits[3];
} } else {
else {
cmd = input; cmd = input;
} }
if (cmd === "me") return null; if (cmd === "me") return null;
@ -319,9 +389,8 @@ module.exports = {
} }
if (commands[cmd]) { if (commands[cmd]) {
return commands[cmd].run(roomId, args); return commands[cmd].run(roomId, args);
} } else {
else { return reject(_t("Unrecognised command:") + ' ' + input);
return reject("Unrecognised command: " + input);
} }
} }
return null; // not a command return null; // not a command
@ -329,12 +398,12 @@ module.exports = {
getCommandList: function() { getCommandList: function() {
// Return all the commands plus /me and /markdown which aren't handled like normal commands // Return all the commands plus /me and /markdown which aren't handled like normal commands
var cmds = Object.keys(commands).sort().map(function(cmdKey) { const cmds = Object.keys(commands).sort().map(function(cmdKey) {
return commands[cmdKey]; return commands[cmdKey];
}); });
cmds.push(new Command("me", "<action>", function() {})); cmds.push(new Command("me", "<action>", function() {}));
cmds.push(new Command("markdown", "<on|off>", function() {})); cmds.push(new Command("markdown", "<on|off>", function() {}));
return cmds; return cmds;
} },
}; };

View file

@ -13,10 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import MatrixClientPeg from "./MatrixClientPeg";
var MatrixClientPeg = require("./MatrixClientPeg"); import CallHandler from "./CallHandler";
var CallHandler = require("./CallHandler"); import { _t } from './languageHandler';
import * as Roles from './Roles'; import * as Roles from './Roles';
function textForMemberEvent(ev) { function textForMemberEvent(ev) {
@ -25,45 +24,45 @@ function textForMemberEvent(ev) {
var targetName = ev.target ? ev.target.name : ev.getStateKey(); var targetName = ev.target ? ev.target.name : ev.getStateKey();
var ConferenceHandler = CallHandler.getConferenceHandler(); var ConferenceHandler = CallHandler.getConferenceHandler();
var reason = ev.getContent().reason ? ( var reason = ev.getContent().reason ? (
" Reason: " + ev.getContent().reason _t('Reason') + ': ' + ev.getContent().reason
) : ""; ) : "";
switch (ev.getContent().membership) { switch (ev.getContent().membership) {
case 'invite': case 'invite':
var threePidContent = ev.getContent().third_party_invite; var threePidContent = ev.getContent().third_party_invite;
if (threePidContent) { if (threePidContent) {
if (threePidContent.display_name) { if (threePidContent.display_name) {
return targetName + " accepted the invitation for " + return _t('%(targetName)s accepted the invitation for %(displayName)s.', {targetName: targetName, displayName: threePidContent.display_name});
threePidContent.display_name + ".";
} else { } else {
return targetName + " accepted an invitation."; return _t('%(targetName)s accepted an invitation.', {targetName: targetName});
} }
} }
else { else {
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
return senderName + " requested a VoIP conference"; return _t('%(senderName)s requested a VoIP conference.', {senderName: senderName});
} }
else { else {
return senderName + " invited " + targetName + "."; return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName});
} }
} }
case 'ban': case 'ban':
return senderName + " banned " + targetName + "." + reason; return _t(
'%(senderName)s banned %(targetName)s.',
{senderName: senderName, targetName: targetName}
) + ' ' + reason;
case 'join': case 'join':
if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') { if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') {
if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) { if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) {
return ev.getSender() + " changed their display name from " + return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname, displayName: ev.getContent().displayname});
ev.getPrevContent().displayname + " to " +
ev.getContent().displayname;
} else if (!ev.getPrevContent().displayname && ev.getContent().displayname) { } else if (!ev.getPrevContent().displayname && ev.getContent().displayname) {
return ev.getSender() + " set their display name to " + ev.getContent().displayname; return _t('%(senderName)s set their display name to %(displayName)s.', {senderName: ev.getSender(), displayName: ev.getContent().displayname});
} else if (ev.getPrevContent().displayname && !ev.getContent().displayname) { } else if (ev.getPrevContent().displayname && !ev.getContent().displayname) {
return ev.getSender() + " removed their display name (" + ev.getPrevContent().displayname + ")"; return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname});
} else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) { } else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) {
return senderName + " removed their profile picture"; return _t('%(senderName)s removed their profile picture.', {senderName: senderName});
} else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) { } else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) {
return senderName + " changed their profile picture"; return _t('%(senderName)s changed their profile picture.', {senderName: senderName});
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) { } else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
return senderName + " set a profile picture"; return _t('%(senderName)s set a profile picture.', {senderName: senderName});
} else { } else {
// suppress null rejoins // suppress null rejoins
return ''; return '';
@ -71,49 +70,57 @@ function textForMemberEvent(ev) {
} else { } else {
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
return "VoIP conference started"; return _t('VoIP conference started.');
} }
else { else {
return targetName + " joined the room."; return _t('%(targetName)s joined the room.', {targetName: targetName});
} }
} }
case 'leave': case 'leave':
if (ev.getSender() === ev.getStateKey()) { if (ev.getSender() === ev.getStateKey()) {
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
return "VoIP conference finished"; return _t('VoIP conference finished.');
} }
else if (ev.getPrevContent().membership === "invite") { else if (ev.getPrevContent().membership === "invite") {
return targetName + " rejected the invitation."; return _t('%(targetName)s rejected the invitation.', {targetName: targetName});
} }
else { else {
return targetName + " left the room."; return _t('%(targetName)s left the room.', {targetName: targetName});
} }
} }
else if (ev.getPrevContent().membership === "ban") { else if (ev.getPrevContent().membership === "ban") {
return senderName + " unbanned " + targetName + "."; return _t('%(senderName)s unbanned %(targetName)s.', {senderName: senderName, targetName: targetName});
} }
else if (ev.getPrevContent().membership === "join") { else if (ev.getPrevContent().membership === "join") {
return senderName + " kicked " + targetName + "." + reason; return _t(
'%(senderName)s kicked %(targetName)s.',
{senderName: senderName, targetName: targetName}
) + ' ' + reason;
} }
else if (ev.getPrevContent().membership === "invite") { else if (ev.getPrevContent().membership === "invite") {
return senderName + " withdrew " + targetName + "'s invitation." + reason; return _t(
'%(senderName)s withdrew %(targetName)s\'s invitation.',
{senderName: senderName, targetName: targetName}
) + ' ' + reason;
} }
else { else {
return targetName + " left the room."; return _t('%(targetName)s left the room.', {targetName: targetName});
} }
} }
} }
function textForTopicEvent(ev) { function textForTopicEvent(ev) {
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {senderDisplayName: senderDisplayName, topic: ev.getContent().topic});
return senderDisplayName + ' changed the topic to "' + ev.getContent().topic + '"';
} }
function textForRoomNameEvent(ev) { function textForRoomNameEvent(ev) {
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return senderDisplayName + ' changed the room name to "' + ev.getContent().name + '"'; if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName: senderDisplayName});
}
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {senderDisplayName: senderDisplayName, roomName: ev.getContent().name});
} }
function textForMessageEvent(ev) { function textForMessageEvent(ev) {
@ -122,66 +129,78 @@ function textForMessageEvent(ev) {
if (ev.getContent().msgtype === "m.emote") { if (ev.getContent().msgtype === "m.emote") {
message = "* " + senderDisplayName + " " + message; message = "* " + senderDisplayName + " " + message;
} else if (ev.getContent().msgtype === "m.image") { } else if (ev.getContent().msgtype === "m.image") {
message = senderDisplayName + " sent an image."; message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName: senderDisplayName});
} }
return message; return message;
} }
function textForCallAnswerEvent(event) { function textForCallAnswerEvent(event) {
var senderName = event.sender ? event.sender.name : "Someone"; var senderName = event.sender ? event.sender.name : _t('Someone');
var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)"; var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return senderName + " answered the call." + supported; return _t('%(senderName)s answered the call.', {senderName: senderName}) + ' ' + supported;
} }
function textForCallHangupEvent(event) { function textForCallHangupEvent(event) {
var senderName = event.sender ? event.sender.name : "Someone"; const senderName = event.sender ? event.sender.name : _t('Someone');
var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)"; const eventContent = event.getContent();
return senderName + " ended the call." + supported; 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) { function textForCallInviteEvent(event) {
var senderName = event.sender ? event.sender.name : "Someone"; var senderName = event.sender ? event.sender.name : _t('Someone');
// FIXME: Find a better way to determine this from the event? // FIXME: Find a better way to determine this from the event?
var type = "voice"; var type = "voice";
if (event.getContent().offer && event.getContent().offer.sdp && if (event.getContent().offer && event.getContent().offer.sdp &&
event.getContent().offer.sdp.indexOf('m=video') !== -1) { event.getContent().offer.sdp.indexOf('m=video') !== -1) {
type = "video"; type = "video";
} }
var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)"; var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return senderName + " placed a " + type + " call." + supported; return _t('%(senderName)s placed a %(callType)s call.', {senderName: senderName, callType: type}) + ' ' + supported;
} }
function textForThreePidInviteEvent(event) { function textForThreePidInviteEvent(event) {
var senderName = event.sender ? event.sender.name : event.getSender(); var senderName = event.sender ? event.sender.name : event.getSender();
return senderName + " sent an invitation to " + event.getContent().display_name + return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {senderName: senderName, targetDisplayName: event.getContent().display_name});
" to join the room.";
} }
function textForHistoryVisibilityEvent(event) { function textForHistoryVisibilityEvent(event) {
var senderName = event.sender ? event.sender.name : event.getSender(); var senderName = event.sender ? event.sender.name : event.getSender();
var vis = event.getContent().history_visibility; var vis = event.getContent().history_visibility;
var text = senderName + " made future room history visible to "; // XXX: This i18n just isn't going to work for languages with different sentence structure.
var text = _t('%(senderName)s made future room history visible to', {senderName: senderName}) + ' ';
if (vis === "invited") { if (vis === "invited") {
text += "all room members, from the point they are invited."; text += _t('all room members, from the point they are invited') + '.';
} }
else if (vis === "joined") { else if (vis === "joined") {
text += "all room members, from the point they joined."; text += _t('all room members, from the point they joined') + '.';
} }
else if (vis === "shared") { else if (vis === "shared") {
text += "all room members."; text += _t('all room members') + '.';
} }
else if (vis === "world_readable") { else if (vis === "world_readable") {
text += "anyone."; text += _t('anyone') + '.';
} }
else { else {
text += " unknown (" + vis + ")"; text += ' ' + _t('unknown') + ' (' + vis + ').';
} }
return text; return text;
} }
function textForEncryptionEvent(event) { function textForEncryptionEvent(event) {
var senderName = event.sender ? event.sender.name : event.getSender(); var senderName = event.sender ? event.sender.name : event.getSender();
return senderName + " turned on end-to-end encryption (algorithm " + event.getContent().algorithm + ")"; return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {senderName: senderName, algorithm: event.getContent().algorithm});
} }
// Currently will only display a change if a user's power level is changed // Currently will only display a change if a user's power level is changed
@ -204,6 +223,7 @@ function textForPowerEvent(event) {
} }
); );
let diff = []; let diff = [];
// XXX: This is also surely broken for i18n
users.forEach((userId) => { users.forEach((userId) => {
// Previous power level // Previous power level
const from = event.getPrevContent().users[userId]; const from = event.getPrevContent().users[userId];
@ -211,16 +231,21 @@ function textForPowerEvent(event) {
const to = event.getContent().users[userId]; const to = event.getContent().users[userId];
if (to !== from) { if (to !== from) {
diff.push( diff.push(
userId + _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
' from ' + Roles.textualPowerLevel(from, userDefault) + userId: userId,
' to ' + Roles.textualPowerLevel(to, userDefault) fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
toPowerLevel: Roles.textualPowerLevel(to, userDefault)
})
); );
} }
}); });
if (!diff.length) { if (!diff.length) {
return ''; return '';
} }
return senderName + ' changed the power level of ' + diff.join(', '); return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
senderName: senderName,
powerLevelDiffText: diff.join(", ")
});
} }
var handlers = { var handlers = {

View file

@ -22,7 +22,7 @@ let isDialogOpen = false;
const onAction = function(payload) { const onAction = function(payload) {
if (payload.action === 'unknown_device_error' && !isDialogOpen) { if (payload.action === 'unknown_device_error' && !isDialogOpen) {
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
isDialogOpen = true; isDialogOpen = true;
Modal.createDialog(UnknownDeviceDialog, { Modal.createDialog(UnknownDeviceDialog, {
devices: payload.err.devices, devices: payload.err.devices,
@ -33,9 +33,9 @@ const onAction = function(payload) {
// https://github.com/vector-im/riot-web/issues/3148 // https://github.com/vector-im/riot-web/issues/3148
console.log('UnknownDeviceDialog closed with '+r); console.log('UnknownDeviceDialog closed with '+r);
}, },
}, "mx_Dialog_unknownDevice"); }, 'mx_Dialog_unknownDevice');
}
} }
};
let ref = null; let ref = null;

View file

@ -25,7 +25,9 @@ module.exports = {
eventTriggersUnreadCount: function(ev) { eventTriggersUnreadCount: function(ev) {
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
return false; return false;
} else if (ev.getType() == "m.room.member") { } else if (ev.getType() == 'm.room.member') {
return false;
} else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') {
return false; return false;
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { } else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
return false; return false;

View file

@ -14,24 +14,29 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict';
import q from 'q'; import q from 'q';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import Notifier from './Notifier'; import Notifier from './Notifier';
import { _t } from './languageHandler';
/* /*
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage. * TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
*/ */
module.exports = { export default {
LABS_FEATURES: [ LABS_FEATURES: [
{ {
name: 'New Composer & Autocomplete', name: "-",
id: 'rich_text_editor', id: 'rich_text_editor',
default: false, default: false,
}, },
], ],
// horrible but it works. The locality makes this somewhat more palatable.
doTranslations: function() {
this.LABS_FEATURES[0].name = _t("New Composer & Autocomplete");
},
loadProfileInfo: function() { loadProfileInfo: function() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
return cli.getProfileInfo(cli.credentials.userId); return cli.getProfileInfo(cli.credentials.userId);

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
var MatrixClientPeg = require("./MatrixClientPeg"); var MatrixClientPeg = require("./MatrixClientPeg");
import { _t } from './languageHandler';
module.exports = { module.exports = {
usersTypingApartFromMe: function(room) { usersTypingApartFromMe: function(room) {
@ -56,18 +57,18 @@ module.exports = {
if (whoIsTyping.length == 0) { if (whoIsTyping.length == 0) {
return ''; return '';
} else if (whoIsTyping.length == 1) { } else if (whoIsTyping.length == 1) {
return whoIsTyping[0].name + ' is typing'; return _t('%(displayName)s is typing', {displayName: whoIsTyping[0].name});
} }
const names = whoIsTyping.map(function(m) { const names = whoIsTyping.map(function(m) {
return m.name; return m.name;
}); });
if (othersCount) { if (othersCount==1) {
const other = ' other' + (othersCount > 1 ? 's' : ''); return _t('%(names)s and one other are typing', {names: names.slice(0, limit - 1).join(', ')});
return names.slice(0, limit - 1).join(', ') + ' and ' + } else if (othersCount>1) {
othersCount + other + ' are typing'; return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount});
} else { } else {
const lastPerson = names.pop(); const lastPerson = names.pop();
return names.join(', ') + ' and ' + lastPerson + ' are typing'; return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson});
} }
} }
}; };

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
var React = require("react"); var React = require("react");
import { _t } from '../../../languageHandler';
var sdk = require('../../../index'); var sdk = require('../../../index');
var MatrixClientPeg = require("../../../MatrixClientPeg"); var MatrixClientPeg = require("../../../MatrixClientPeg");
@ -78,33 +79,33 @@ module.exports = React.createClass({
_renderDeviceInfo: function() { _renderDeviceInfo: function() {
var device = this.state.device; var device = this.state.device;
if (!device) { if (!device) {
return (<i>unknown device</i>); return (<i>{ _t('unknown device') }</i>);
} }
var verificationStatus = (<b>NOT verified</b>); var verificationStatus = (<b>{ _t('NOT verified') }</b>);
if (device.isBlocked()) { if (device.isBlocked()) {
verificationStatus = (<b>Blacklisted</b>); verificationStatus = (<b>{ _t('Blacklisted') }</b>);
} else if (device.isVerified()) { } else if (device.isVerified()) {
verificationStatus = "verified"; verificationStatus = _t('verified');
} }
return ( return (
<table> <table>
<tbody> <tbody>
<tr> <tr>
<td>Name</td> <td>{ _t('Name') }</td>
<td>{ device.getDisplayName() }</td> <td>{ device.getDisplayName() }</td>
</tr> </tr>
<tr> <tr>
<td>Device ID</td> <td>{ _t('Device ID') }</td>
<td><code>{ device.deviceId }</code></td> <td><code>{ device.deviceId }</code></td>
</tr> </tr>
<tr> <tr>
<td>Verification</td> <td>{ _t('Verification') }</td>
<td>{ verificationStatus }</td> <td>{ verificationStatus }</td>
</tr> </tr>
<tr> <tr>
<td>Ed25519 fingerprint</td> <td>{ _t('Ed25519 fingerprint') }</td>
<td><code>{device.getFingerprint()}</code></td> <td><code>{device.getFingerprint()}</code></td>
</tr> </tr>
</tbody> </tbody>
@ -119,32 +120,32 @@ module.exports = React.createClass({
<table> <table>
<tbody> <tbody>
<tr> <tr>
<td>User ID</td> <td>{ _t('User ID') }</td>
<td>{ event.getSender() }</td> <td>{ event.getSender() }</td>
</tr> </tr>
<tr> <tr>
<td>Curve25519 identity key</td> <td>{ _t('Curve25519 identity key') }</td>
<td><code>{ event.getSenderKey() || <i>none</i> }</code></td> <td><code>{ event.getSenderKey() || <i>{ _t('none') }</i> }</code></td>
</tr> </tr>
<tr> <tr>
<td>Claimed Ed25519 fingerprint key</td> <td>{ _t('Claimed Ed25519 fingerprint key') }</td>
<td><code>{ event.getKeysClaimed().ed25519 || <i>none</i> }</code></td> <td><code>{ event.getKeysClaimed().ed25519 || <i>{ _t('none') }</i> }</code></td>
</tr> </tr>
<tr> <tr>
<td>Algorithm</td> <td>{ _t('Algorithm') }</td>
<td>{ event.getWireContent().algorithm || <i>unencrypted</i> }</td> <td>{ event.getWireContent().algorithm || <i>{ _t('unencrypted') }</i> }</td>
</tr> </tr>
{ {
event.getContent().msgtype === 'm.bad.encrypted' ? ( event.getContent().msgtype === 'm.bad.encrypted' ? (
<tr> <tr>
<td>Decryption error</td> <td>{ _t('Decryption error') }</td>
<td>{ event.getContent().body }</td> <td>{ event.getContent().body }</td>
</tr> </tr>
) : null ) : null
} }
<tr> <tr>
<td>Session ID</td> <td>{ _t('Session ID') }</td>
<td><code>{ event.getWireContent().session_id || <i>none</i> }</code></td> <td><code>{ event.getWireContent().session_id || <i>{ _t('none') }</i> }</code></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -166,18 +167,18 @@ module.exports = React.createClass({
return ( return (
<div className="mx_EncryptedEventDialog" onKeyDown={ this.onKeyDown }> <div className="mx_EncryptedEventDialog" onKeyDown={ this.onKeyDown }>
<div className="mx_Dialog_title"> <div className="mx_Dialog_title">
End-to-end encryption information { _t('End-to-end encryption information') }
</div> </div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<h4>Event information</h4> <h4>{ _t('Event information') }</h4>
{this._renderEventInfo()} {this._renderEventInfo()}
<h4>Sender device information</h4> <h4>{ _t('Sender device information') }</h4>
{this._renderDeviceInfo()} {this._renderDeviceInfo()}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={ this.props.onFinished } autoFocus={ true }> <button className="mx_Dialog_primary" onClick={ this.props.onFinished } autoFocus={ true }>
OK { _t('OK') }
</button> </button>
{buttons} {buttons}
</div> </div>

View file

@ -16,6 +16,7 @@ limitations under the License.
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import React from 'react'; import React from 'react';
import { _t } from '../../../languageHandler';
import * as Matrix from 'matrix-js-sdk'; import * as Matrix from 'matrix-js-sdk';
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
@ -52,11 +53,11 @@ export default React.createClass({
const passphrase = this.refs.passphrase1.value; const passphrase = this.refs.passphrase1.value;
if (passphrase !== this.refs.passphrase2.value) { if (passphrase !== this.refs.passphrase2.value) {
this.setState({errStr: 'Passphrases must match'}); this.setState({errStr: _t('Passphrases must match')});
return false; return false;
} }
if (!passphrase) { if (!passphrase) {
this.setState({errStr: 'Passphrase must not be empty'}); this.setState({errStr: _t('Passphrase must not be empty')});
return false; return false;
} }
@ -80,11 +81,13 @@ export default React.createClass({
FileSaver.saveAs(blob, 'riot-keys.txt'); FileSaver.saveAs(blob, 'riot-keys.txt');
this.props.onFinished(true); this.props.onFinished(true);
}).catch((e) => { }).catch((e) => {
console.error("Error exporting e2e keys:", e);
if (this._unmounted) { if (this._unmounted) {
return; return;
} }
const msg = e.friendlyText || _t('Unknown error');
this.setState({ this.setState({
errStr: e.message, errStr: msg,
phase: PHASE_EDIT, phase: PHASE_EDIT,
}); });
}); });
@ -109,24 +112,28 @@ export default React.createClass({
return ( return (
<BaseDialog className='mx_exportE2eKeysDialog' <BaseDialog className='mx_exportE2eKeysDialog'
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title="Export room keys" title={_t("Export room keys")}
> >
<form onSubmit={this._onPassphraseFormSubmit}> <form onSubmit={this._onPassphraseFormSubmit}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<p> <p>
This process allows you to export the keys for messages { _t(
you have received in encrypted rooms to a local file. You 'This process allows you to export the keys for messages ' +
will then be able to import the file into another Matrix 'you have received in encrypted rooms to a local file. You ' +
client in the future, so that client will also be able to 'will then be able to import the file into another Matrix ' +
decrypt these messages. 'client in the future, so that client will also be able to ' +
'decrypt these messages.',
) }
</p> </p>
<p> <p>
The exported file will allow anyone who can read it to decrypt { _t(
any encrypted messages that you can see, so you should be 'The exported file will allow anyone who can read it to decrypt ' +
careful to keep it secure. To help with this, you should enter 'any encrypted messages that you can see, so you should be ' +
a passphrase below, which will be used to encrypt the exported 'careful to keep it secure. To help with this, you should enter ' +
data. It will only be possible to import the data by using the 'a passphrase below, which will be used to encrypt the exported ' +
same passphrase. 'data. It will only be possible to import the data by using the ' +
'same passphrase.',
) }
</p> </p>
<div className='error'> <div className='error'>
{this.state.errStr} {this.state.errStr}
@ -135,7 +142,7 @@ export default React.createClass({
<div className='mx_E2eKeysDialog_inputRow'> <div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'> <div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase1'> <label htmlFor='passphrase1'>
Enter passphrase {_t("Enter passphrase")}
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
@ -148,7 +155,7 @@ export default React.createClass({
<div className='mx_E2eKeysDialog_inputRow'> <div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'> <div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase2'> <label htmlFor='passphrase2'>
Confirm passphrase {_t("Confirm passphrase")}
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
@ -161,11 +168,11 @@ export default React.createClass({
</div> </div>
</div> </div>
<div className='mx_Dialog_buttons'> <div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value='Export' <input className='mx_Dialog_primary' type='submit' value={_t('Export')}
disabled={disableForm} disabled={disableForm}
/> />
<button onClick={this._onCancelClick} disabled={disableForm}> <button onClick={this._onCancelClick} disabled={disableForm}>
Cancel {_t("Cancel")}
</button> </button>
</div> </div>
</form> </form>

View file

@ -19,6 +19,7 @@ import React from 'react';
import * as Matrix from 'matrix-js-sdk'; import * as Matrix from 'matrix-js-sdk';
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
function readFileAsArrayBuffer(file) { function readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -88,11 +89,13 @@ export default React.createClass({
// TODO: it would probably be nice to give some feedback about what we've imported here. // TODO: it would probably be nice to give some feedback about what we've imported here.
this.props.onFinished(true); this.props.onFinished(true);
}).catch((e) => { }).catch((e) => {
console.error("Error importing e2e keys:", e);
if (this._unmounted) { if (this._unmounted) {
return; return;
} }
const msg = e.friendlyText || _t('Unknown error');
this.setState({ this.setState({
errStr: e.message, errStr: msg,
phase: PHASE_EDIT, phase: PHASE_EDIT,
}); });
}); });
@ -112,20 +115,23 @@ export default React.createClass({
return ( return (
<BaseDialog className='mx_importE2eKeysDialog' <BaseDialog className='mx_importE2eKeysDialog'
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title="Import room keys" title={_t("Import room keys")}
> >
<form onSubmit={this._onFormSubmit}> <form onSubmit={this._onFormSubmit}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<p> <p>
This process allows you to import encryption keys { _t(
that you had previously exported from another Matrix 'This process allows you to import encryption keys ' +
client. You will then be able to decrypt any 'that you had previously exported from another Matrix ' +
messages that the other client could decrypt. 'client. You will then be able to decrypt any ' +
'messages that the other client could decrypt.',
) }
</p> </p>
<p> <p>
The export file will be protected with a passphrase. { _t(
You should enter the passphrase here, to decrypt the 'The export file will be protected with a passphrase. ' +
file. 'You should enter the passphrase here, to decrypt the file.',
) }
</p> </p>
<div className='error'> <div className='error'>
{this.state.errStr} {this.state.errStr}
@ -134,7 +140,7 @@ export default React.createClass({
<div className='mx_E2eKeysDialog_inputRow'> <div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'> <div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='importFile'> <label htmlFor='importFile'>
File to import {_t("File to import")}
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
@ -147,7 +153,7 @@ export default React.createClass({
<div className='mx_E2eKeysDialog_inputRow'> <div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'> <div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase'> <label htmlFor='passphrase'>
Enter passphrase {_t("Enter passphrase")}
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
@ -160,11 +166,11 @@ export default React.createClass({
</div> </div>
</div> </div>
<div className='mx_Dialog_buttons'> <div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value='Import' <input className='mx_Dialog_primary' type='submit' value={_t('Import')}
disabled={!this.state.enableSubmit || disableForm} disabled={!this.state.enableSubmit || disableForm}
/> />
<button onClick={this._onCancelClick} disabled={disableForm}> <button onClick={this._onCancelClick} disabled={disableForm}>
Cancel {_t("Cancel")}
</button> </button>
</div> </div>
</form> </form>

View file

@ -1,3 +1,20 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import React from 'react'; import React from 'react';
import type {Completion, SelectionRange} from './Autocompleter'; import type {Completion, SelectionRange} from './Autocompleter';

View file

@ -1,3 +1,19 @@
/*
Copyright 2016 Aviral Dasgupta
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
// @flow // @flow
import type {Component} from 'react'; import type {Component} from 'react';

View file

@ -1,8 +1,27 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import React from 'react'; import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import FuzzyMatcher from './FuzzyMatcher'; import FuzzyMatcher from './FuzzyMatcher';
import {TextualCompletion} from './Components'; import {TextualCompletion} from './Components';
// Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file
const COMMANDS = [ const COMMANDS = [
{ {
command: '/me', command: '/me',
@ -61,7 +80,7 @@ const COMMANDS = [
} }
]; ];
let COMMAND_RE = /(^\/\w*)/g; const COMMAND_RE = /(^\/\w*)/g;
let instance = null; let instance = null;
@ -75,7 +94,7 @@ export default class CommandProvider extends AutocompleteProvider {
async getCompletions(query: string, selection: {start: number, end: number}) { async getCompletions(query: string, selection: {start: number, end: number}) {
let completions = []; let completions = [];
let {command, range} = this.getCurrentCommand(query, selection); const {command, range} = this.getCurrentCommand(query, selection);
if (command) { if (command) {
completions = this.matcher.match(command[0]).map(result => { completions = this.matcher.match(command[0]).map(result => {
return { return {
@ -83,7 +102,7 @@ export default class CommandProvider extends AutocompleteProvider {
component: (<TextualCompletion component: (<TextualCompletion
title={result.command} title={result.command}
subtitle={result.args} subtitle={result.args}
description={result.description} description={ _t(result.description) }
/>), />),
range, range,
}; };
@ -93,12 +112,11 @@ export default class CommandProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '*️⃣ Commands'; return '*️⃣ ' + _t('Commands');
} }
static getInstance(): CommandProvider { static getInstance(): CommandProvider {
if (instance == null) if (instance === null) instance = new CommandProvider();
{instance = new CommandProvider();}
return instance; return instance;
} }

View file

@ -1,5 +1,20 @@
/*
Copyright 2016 Aviral Dasgupta
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames'; import classNames from 'classnames';
/* These were earlier stateless functional components but had to be converted /* These were earlier stateless functional components but had to be converted

View file

@ -1,4 +1,22 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import React from 'react'; import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import 'whatwg-fetch'; import 'whatwg-fetch';
@ -75,7 +93,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '🔍 Results from DuckDuckGo'; return '🔍 ' + _t('Results from DuckDuckGo');
} }
static getInstance(): DuckDuckGoProvider { static getInstance(): DuckDuckGoProvider {

View file

@ -1,4 +1,22 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import React from 'react'; import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione'; import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
import FuzzyMatcher from './FuzzyMatcher'; import FuzzyMatcher from './FuzzyMatcher';
@ -45,7 +63,7 @@ export default class EmojiProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '😃 Emoji'; return '😃 ' + _t('Emoji');
} }
static getInstance() { static getInstance() {

View file

@ -1,4 +1,22 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import React from 'react'; import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import MatrixClientPeg from '../MatrixClientPeg'; import MatrixClientPeg from '../MatrixClientPeg';
import FuzzyMatcher from './FuzzyMatcher'; import FuzzyMatcher from './FuzzyMatcher';
@ -48,7 +66,7 @@ export default class RoomProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '💬 Rooms'; return '💬 ' + _t('Rooms');
} }
static getInstance() { static getInstance() {

View file

@ -1,7 +1,24 @@
//@flow //@flow
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import React from 'react'; import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import Q from 'q';
import {PillCompletion} from './Components'; import {PillCompletion} from './Components';
import sdk from '../index'; import sdk from '../index';
import FuzzyMatcher from './FuzzyMatcher'; import FuzzyMatcher from './FuzzyMatcher';
@ -57,7 +74,7 @@ export default class UserProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '👥 Users'; return '👥 ' + _t('Users');
} }
setUserListFromRoom(room: Room) { setUserListFromRoom(room: Room) {

View file

@ -1,265 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
/*
* THIS FILE IS AUTO-GENERATED
* You can edit it you like, but your changes will be overwritten,
* so you'd just be trying to swim upstream like a salmon.
* You are not a salmon.
*
* To update it, run:
* ./reskindex.js -h header
*/
module.exports.components = {};
import structures$ContextualMenu from './components/structures/ContextualMenu';
structures$ContextualMenu && (module.exports.components['structures.ContextualMenu'] = structures$ContextualMenu);
import structures$CreateRoom from './components/structures/CreateRoom';
structures$CreateRoom && (module.exports.components['structures.CreateRoom'] = structures$CreateRoom);
import structures$FilePanel from './components/structures/FilePanel';
structures$FilePanel && (module.exports.components['structures.FilePanel'] = structures$FilePanel);
import structures$InteractiveAuth from './components/structures/InteractiveAuth';
structures$InteractiveAuth && (module.exports.components['structures.InteractiveAuth'] = structures$InteractiveAuth);
import structures$LoggedInView from './components/structures/LoggedInView';
structures$LoggedInView && (module.exports.components['structures.LoggedInView'] = structures$LoggedInView);
import structures$MatrixChat from './components/structures/MatrixChat';
structures$MatrixChat && (module.exports.components['structures.MatrixChat'] = structures$MatrixChat);
import structures$MessagePanel from './components/structures/MessagePanel';
structures$MessagePanel && (module.exports.components['structures.MessagePanel'] = structures$MessagePanel);
import structures$NotificationPanel from './components/structures/NotificationPanel';
structures$NotificationPanel && (module.exports.components['structures.NotificationPanel'] = structures$NotificationPanel);
import structures$RoomStatusBar from './components/structures/RoomStatusBar';
structures$RoomStatusBar && (module.exports.components['structures.RoomStatusBar'] = structures$RoomStatusBar);
import structures$RoomView from './components/structures/RoomView';
structures$RoomView && (module.exports.components['structures.RoomView'] = structures$RoomView);
import structures$ScrollPanel from './components/structures/ScrollPanel';
structures$ScrollPanel && (module.exports.components['structures.ScrollPanel'] = structures$ScrollPanel);
import structures$TimelinePanel from './components/structures/TimelinePanel';
structures$TimelinePanel && (module.exports.components['structures.TimelinePanel'] = structures$TimelinePanel);
import structures$UploadBar from './components/structures/UploadBar';
structures$UploadBar && (module.exports.components['structures.UploadBar'] = structures$UploadBar);
import structures$UserSettings from './components/structures/UserSettings';
structures$UserSettings && (module.exports.components['structures.UserSettings'] = structures$UserSettings);
import structures$login$ForgotPassword from './components/structures/login/ForgotPassword';
structures$login$ForgotPassword && (module.exports.components['structures.login.ForgotPassword'] = structures$login$ForgotPassword);
import structures$login$Login from './components/structures/login/Login';
structures$login$Login && (module.exports.components['structures.login.Login'] = structures$login$Login);
import structures$login$PostRegistration from './components/structures/login/PostRegistration';
structures$login$PostRegistration && (module.exports.components['structures.login.PostRegistration'] = structures$login$PostRegistration);
import structures$login$Registration from './components/structures/login/Registration';
structures$login$Registration && (module.exports.components['structures.login.Registration'] = structures$login$Registration);
import views$avatars$BaseAvatar from './components/views/avatars/BaseAvatar';
views$avatars$BaseAvatar && (module.exports.components['views.avatars.BaseAvatar'] = views$avatars$BaseAvatar);
import views$avatars$MemberAvatar from './components/views/avatars/MemberAvatar';
views$avatars$MemberAvatar && (module.exports.components['views.avatars.MemberAvatar'] = views$avatars$MemberAvatar);
import views$avatars$RoomAvatar from './components/views/avatars/RoomAvatar';
views$avatars$RoomAvatar && (module.exports.components['views.avatars.RoomAvatar'] = views$avatars$RoomAvatar);
import views$create_room$CreateRoomButton from './components/views/create_room/CreateRoomButton';
views$create_room$CreateRoomButton && (module.exports.components['views.create_room.CreateRoomButton'] = views$create_room$CreateRoomButton);
import views$create_room$Presets from './components/views/create_room/Presets';
views$create_room$Presets && (module.exports.components['views.create_room.Presets'] = views$create_room$Presets);
import views$create_room$RoomAlias from './components/views/create_room/RoomAlias';
views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias);
import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog';
views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog);
import views$dialogs$ChatCreateOrReuseDialog from './components/views/dialogs/ChatCreateOrReuseDialog';
views$dialogs$ChatCreateOrReuseDialog && (module.exports.components['views.dialogs.ChatCreateOrReuseDialog'] = views$dialogs$ChatCreateOrReuseDialog);
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog);
import views$dialogs$ConfirmRedactDialog from './components/views/dialogs/ConfirmRedactDialog';
views$dialogs$ConfirmRedactDialog && (module.exports.components['views.dialogs.ConfirmRedactDialog'] = views$dialogs$ConfirmRedactDialog);
import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog';
views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog);
import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog);
import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog';
views$dialogs$ErrorDialog && (module.exports.components['views.dialogs.ErrorDialog'] = views$dialogs$ErrorDialog);
import views$dialogs$InteractiveAuthDialog from './components/views/dialogs/InteractiveAuthDialog';
views$dialogs$InteractiveAuthDialog && (module.exports.components['views.dialogs.InteractiveAuthDialog'] = views$dialogs$InteractiveAuthDialog);
import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedToRegisterDialog';
views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog);
import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog';
views$dialogs$QuestionDialog && (module.exports.components['views.dialogs.QuestionDialog'] = views$dialogs$QuestionDialog);
import views$dialogs$SessionRestoreErrorDialog from './components/views/dialogs/SessionRestoreErrorDialog';
views$dialogs$SessionRestoreErrorDialog && (module.exports.components['views.dialogs.SessionRestoreErrorDialog'] = views$dialogs$SessionRestoreErrorDialog);
import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDisplayNameDialog';
views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog);
import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog';
views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog);
import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog';
views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog);
import views$elements$AccessibleButton from './components/views/elements/AccessibleButton';
views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
import views$elements$ActionButton from './components/views/elements/ActionButton';
views$elements$ActionButton && (module.exports.components['views.elements.ActionButton'] = views$elements$ActionButton);
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
import views$elements$AddressTile from './components/views/elements/AddressTile';
views$elements$AddressTile && (module.exports.components['views.elements.AddressTile'] = views$elements$AddressTile);
import views$elements$CreateRoomButton from './components/views/elements/CreateRoomButton';
views$elements$CreateRoomButton && (module.exports.components['views.elements.CreateRoomButton'] = views$elements$CreateRoomButton);
import views$elements$DeviceVerifyButtons from './components/views/elements/DeviceVerifyButtons';
views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons);
import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox';
views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox);
import views$elements$Dropdown from './components/views/elements/Dropdown';
views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown);
import views$elements$EditableText from './components/views/elements/EditableText';
views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText);
import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer';
views$elements$EditableTextContainer && (module.exports.components['views.elements.EditableTextContainer'] = views$elements$EditableTextContainer);
import views$elements$EmojiText from './components/views/elements/EmojiText';
views$elements$EmojiText && (module.exports.components['views.elements.EmojiText'] = views$elements$EmojiText);
import views$elements$HomeButton from './components/views/elements/HomeButton';
views$elements$HomeButton && (module.exports.components['views.elements.HomeButton'] = views$elements$HomeButton);
import views$elements$MemberEventListSummary from './components/views/elements/MemberEventListSummary';
views$elements$MemberEventListSummary && (module.exports.components['views.elements.MemberEventListSummary'] = views$elements$MemberEventListSummary);
import views$elements$PowerSelector from './components/views/elements/PowerSelector';
views$elements$PowerSelector && (module.exports.components['views.elements.PowerSelector'] = views$elements$PowerSelector);
import views$elements$ProgressBar from './components/views/elements/ProgressBar';
views$elements$ProgressBar && (module.exports.components['views.elements.ProgressBar'] = views$elements$ProgressBar);
import views$elements$RoomDirectoryButton from './components/views/elements/RoomDirectoryButton';
views$elements$RoomDirectoryButton && (module.exports.components['views.elements.RoomDirectoryButton'] = views$elements$RoomDirectoryButton);
import views$elements$SettingsButton from './components/views/elements/SettingsButton';
views$elements$SettingsButton && (module.exports.components['views.elements.SettingsButton'] = views$elements$SettingsButton);
import views$elements$StartChatButton from './components/views/elements/StartChatButton';
views$elements$StartChatButton && (module.exports.components['views.elements.StartChatButton'] = views$elements$StartChatButton);
import views$elements$TintableSvg from './components/views/elements/TintableSvg';
views$elements$TintableSvg && (module.exports.components['views.elements.TintableSvg'] = views$elements$TintableSvg);
import views$elements$TruncatedList from './components/views/elements/TruncatedList';
views$elements$TruncatedList && (module.exports.components['views.elements.TruncatedList'] = views$elements$TruncatedList);
import views$elements$UserSelector from './components/views/elements/UserSelector';
views$elements$UserSelector && (module.exports.components['views.elements.UserSelector'] = views$elements$UserSelector);
import views$login$CaptchaForm from './components/views/login/CaptchaForm';
views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm);
import views$login$CasLogin from './components/views/login/CasLogin';
views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin);
import views$login$CountryDropdown from './components/views/login/CountryDropdown';
views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown);
import views$login$CustomServerDialog from './components/views/login/CustomServerDialog';
views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog);
import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents';
views$login$InteractiveAuthEntryComponents && (module.exports.components['views.login.InteractiveAuthEntryComponents'] = views$login$InteractiveAuthEntryComponents);
import views$login$LoginFooter from './components/views/login/LoginFooter';
views$login$LoginFooter && (module.exports.components['views.login.LoginFooter'] = views$login$LoginFooter);
import views$login$LoginHeader from './components/views/login/LoginHeader';
views$login$LoginHeader && (module.exports.components['views.login.LoginHeader'] = views$login$LoginHeader);
import views$login$PasswordLogin from './components/views/login/PasswordLogin';
views$login$PasswordLogin && (module.exports.components['views.login.PasswordLogin'] = views$login$PasswordLogin);
import views$login$RegistrationForm from './components/views/login/RegistrationForm';
views$login$RegistrationForm && (module.exports.components['views.login.RegistrationForm'] = views$login$RegistrationForm);
import views$login$ServerConfig from './components/views/login/ServerConfig';
views$login$ServerConfig && (module.exports.components['views.login.ServerConfig'] = views$login$ServerConfig);
import views$messages$MAudioBody from './components/views/messages/MAudioBody';
views$messages$MAudioBody && (module.exports.components['views.messages.MAudioBody'] = views$messages$MAudioBody);
import views$messages$MFileBody from './components/views/messages/MFileBody';
views$messages$MFileBody && (module.exports.components['views.messages.MFileBody'] = views$messages$MFileBody);
import views$messages$MImageBody from './components/views/messages/MImageBody';
views$messages$MImageBody && (module.exports.components['views.messages.MImageBody'] = views$messages$MImageBody);
import views$messages$MVideoBody from './components/views/messages/MVideoBody';
views$messages$MVideoBody && (module.exports.components['views.messages.MVideoBody'] = views$messages$MVideoBody);
import views$messages$MessageEvent from './components/views/messages/MessageEvent';
views$messages$MessageEvent && (module.exports.components['views.messages.MessageEvent'] = views$messages$MessageEvent);
import views$messages$SenderProfile from './components/views/messages/SenderProfile';
views$messages$SenderProfile && (module.exports.components['views.messages.SenderProfile'] = views$messages$SenderProfile);
import views$messages$TextualBody from './components/views/messages/TextualBody';
views$messages$TextualBody && (module.exports.components['views.messages.TextualBody'] = views$messages$TextualBody);
import views$messages$TextualEvent from './components/views/messages/TextualEvent';
views$messages$TextualEvent && (module.exports.components['views.messages.TextualEvent'] = views$messages$TextualEvent);
import views$messages$UnknownBody from './components/views/messages/UnknownBody';
views$messages$UnknownBody && (module.exports.components['views.messages.UnknownBody'] = views$messages$UnknownBody);
import views$room_settings$AliasSettings from './components/views/room_settings/AliasSettings';
views$room_settings$AliasSettings && (module.exports.components['views.room_settings.AliasSettings'] = views$room_settings$AliasSettings);
import views$room_settings$ColorSettings from './components/views/room_settings/ColorSettings';
views$room_settings$ColorSettings && (module.exports.components['views.room_settings.ColorSettings'] = views$room_settings$ColorSettings);
import views$room_settings$UrlPreviewSettings from './components/views/room_settings/UrlPreviewSettings';
views$room_settings$UrlPreviewSettings && (module.exports.components['views.room_settings.UrlPreviewSettings'] = views$room_settings$UrlPreviewSettings);
import views$rooms$Autocomplete from './components/views/rooms/Autocomplete';
views$rooms$Autocomplete && (module.exports.components['views.rooms.Autocomplete'] = views$rooms$Autocomplete);
import views$rooms$AuxPanel from './components/views/rooms/AuxPanel';
views$rooms$AuxPanel && (module.exports.components['views.rooms.AuxPanel'] = views$rooms$AuxPanel);
import views$rooms$EntityTile from './components/views/rooms/EntityTile';
views$rooms$EntityTile && (module.exports.components['views.rooms.EntityTile'] = views$rooms$EntityTile);
import views$rooms$EventTile from './components/views/rooms/EventTile';
views$rooms$EventTile && (module.exports.components['views.rooms.EventTile'] = views$rooms$EventTile);
import views$rooms$LinkPreviewWidget from './components/views/rooms/LinkPreviewWidget';
views$rooms$LinkPreviewWidget && (module.exports.components['views.rooms.LinkPreviewWidget'] = views$rooms$LinkPreviewWidget);
import views$rooms$MemberDeviceInfo from './components/views/rooms/MemberDeviceInfo';
views$rooms$MemberDeviceInfo && (module.exports.components['views.rooms.MemberDeviceInfo'] = views$rooms$MemberDeviceInfo);
import views$rooms$MemberInfo from './components/views/rooms/MemberInfo';
views$rooms$MemberInfo && (module.exports.components['views.rooms.MemberInfo'] = views$rooms$MemberInfo);
import views$rooms$MemberList from './components/views/rooms/MemberList';
views$rooms$MemberList && (module.exports.components['views.rooms.MemberList'] = views$rooms$MemberList);
import views$rooms$MemberTile from './components/views/rooms/MemberTile';
views$rooms$MemberTile && (module.exports.components['views.rooms.MemberTile'] = views$rooms$MemberTile);
import views$rooms$MessageComposer from './components/views/rooms/MessageComposer';
views$rooms$MessageComposer && (module.exports.components['views.rooms.MessageComposer'] = views$rooms$MessageComposer);
import views$rooms$MessageComposerInput from './components/views/rooms/MessageComposerInput';
views$rooms$MessageComposerInput && (module.exports.components['views.rooms.MessageComposerInput'] = views$rooms$MessageComposerInput);
import views$rooms$MessageComposerInputOld from './components/views/rooms/MessageComposerInputOld';
views$rooms$MessageComposerInputOld && (module.exports.components['views.rooms.MessageComposerInputOld'] = views$rooms$MessageComposerInputOld);
import views$rooms$PresenceLabel from './components/views/rooms/PresenceLabel';
views$rooms$PresenceLabel && (module.exports.components['views.rooms.PresenceLabel'] = views$rooms$PresenceLabel);
import views$rooms$ReadReceiptMarker from './components/views/rooms/ReadReceiptMarker';
views$rooms$ReadReceiptMarker && (module.exports.components['views.rooms.ReadReceiptMarker'] = views$rooms$ReadReceiptMarker);
import views$rooms$RoomHeader from './components/views/rooms/RoomHeader';
views$rooms$RoomHeader && (module.exports.components['views.rooms.RoomHeader'] = views$rooms$RoomHeader);
import views$rooms$RoomList from './components/views/rooms/RoomList';
views$rooms$RoomList && (module.exports.components['views.rooms.RoomList'] = views$rooms$RoomList);
import views$rooms$RoomNameEditor from './components/views/rooms/RoomNameEditor';
views$rooms$RoomNameEditor && (module.exports.components['views.rooms.RoomNameEditor'] = views$rooms$RoomNameEditor);
import views$rooms$RoomPreviewBar from './components/views/rooms/RoomPreviewBar';
views$rooms$RoomPreviewBar && (module.exports.components['views.rooms.RoomPreviewBar'] = views$rooms$RoomPreviewBar);
import views$rooms$RoomSettings from './components/views/rooms/RoomSettings';
views$rooms$RoomSettings && (module.exports.components['views.rooms.RoomSettings'] = views$rooms$RoomSettings);
import views$rooms$RoomTile from './components/views/rooms/RoomTile';
views$rooms$RoomTile && (module.exports.components['views.rooms.RoomTile'] = views$rooms$RoomTile);
import views$rooms$RoomTopicEditor from './components/views/rooms/RoomTopicEditor';
views$rooms$RoomTopicEditor && (module.exports.components['views.rooms.RoomTopicEditor'] = views$rooms$RoomTopicEditor);
import views$rooms$SearchResultTile from './components/views/rooms/SearchResultTile';
views$rooms$SearchResultTile && (module.exports.components['views.rooms.SearchResultTile'] = views$rooms$SearchResultTile);
import views$rooms$SearchableEntityList from './components/views/rooms/SearchableEntityList';
views$rooms$SearchableEntityList && (module.exports.components['views.rooms.SearchableEntityList'] = views$rooms$SearchableEntityList);
import views$rooms$SimpleRoomHeader from './components/views/rooms/SimpleRoomHeader';
views$rooms$SimpleRoomHeader && (module.exports.components['views.rooms.SimpleRoomHeader'] = views$rooms$SimpleRoomHeader);
import views$rooms$TabCompleteBar from './components/views/rooms/TabCompleteBar';
views$rooms$TabCompleteBar && (module.exports.components['views.rooms.TabCompleteBar'] = views$rooms$TabCompleteBar);
import views$rooms$TopUnreadMessagesBar from './components/views/rooms/TopUnreadMessagesBar';
views$rooms$TopUnreadMessagesBar && (module.exports.components['views.rooms.TopUnreadMessagesBar'] = views$rooms$TopUnreadMessagesBar);
import views$rooms$UserTile from './components/views/rooms/UserTile';
views$rooms$UserTile && (module.exports.components['views.rooms.UserTile'] = views$rooms$UserTile);
import views$settings$AddPhoneNumber from './components/views/settings/AddPhoneNumber';
views$settings$AddPhoneNumber && (module.exports.components['views.settings.AddPhoneNumber'] = views$settings$AddPhoneNumber);
import views$settings$ChangeAvatar from './components/views/settings/ChangeAvatar';
views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar);
import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName';
views$settings$ChangeDisplayName && (module.exports.components['views.settings.ChangeDisplayName'] = views$settings$ChangeDisplayName);
import views$settings$ChangePassword from './components/views/settings/ChangePassword';
views$settings$ChangePassword && (module.exports.components['views.settings.ChangePassword'] = views$settings$ChangePassword);
import views$settings$DevicesPanel from './components/views/settings/DevicesPanel';
views$settings$DevicesPanel && (module.exports.components['views.settings.DevicesPanel'] = views$settings$DevicesPanel);
import views$settings$DevicesPanelEntry from './components/views/settings/DevicesPanelEntry';
views$settings$DevicesPanelEntry && (module.exports.components['views.settings.DevicesPanelEntry'] = views$settings$DevicesPanelEntry);
import views$settings$EnableNotificationsButton from './components/views/settings/EnableNotificationsButton';
views$settings$EnableNotificationsButton && (module.exports.components['views.settings.EnableNotificationsButton'] = views$settings$EnableNotificationsButton);
import views$voip$CallView from './components/views/voip/CallView';
views$voip$CallView && (module.exports.components['views.voip.CallView'] = views$voip$CallView);
import views$voip$IncomingCallBox from './components/views/voip/IncomingCallBox';
views$voip$IncomingCallBox && (module.exports.components['views.voip.IncomingCallBox'] = views$voip$IncomingCallBox);
import views$voip$VideoFeed from './components/views/voip/VideoFeed';
views$voip$VideoFeed && (module.exports.components['views.voip.VideoFeed'] = views$voip$VideoFeed);
import views$voip$VideoView from './components/views/voip/VideoView';
views$voip$VideoView && (module.exports.components['views.voip.VideoView'] = views$voip$VideoView);

View file

@ -16,15 +16,15 @@ limitations under the License.
'use strict'; 'use strict';
var React = require("react"); import React from 'react';
var MatrixClientPeg = require("../../MatrixClientPeg"); import { _t } from '../../languageHandler';
var PresetValues = { import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
const PresetValues = {
PrivateChat: "private_chat", PrivateChat: "private_chat",
PublicChat: "public_chat", PublicChat: "public_chat",
Custom: "custom", Custom: "custom",
}; };
var q = require('q');
var sdk = require('../../index');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'CreateRoom', displayName: 'CreateRoom',
@ -231,7 +231,7 @@ module.exports = React.createClass({
if (curr_phase == this.phases.ERROR) { if (curr_phase == this.phases.ERROR) {
error_box = ( error_box = (
<div className="mx_Error"> <div className="mx_Error">
An error occured: {this.state.error_string} {_t('An error occurred: %(error_string)s', {error_string: this.state.error_string})}
</div> </div>
); );
} }
@ -246,29 +246,29 @@ module.exports = React.createClass({
return ( return (
<div className="mx_CreateRoom"> <div className="mx_CreateRoom">
<SimpleRoomHeader title="CreateRoom" collapsedRhs={ this.props.collapsedRhs }/> <SimpleRoomHeader title={_t("Create Room")} collapsedRhs={ this.props.collapsedRhs }/>
<div className="mx_CreateRoom_body"> <div className="mx_CreateRoom_body">
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder="Name"/> <br /> <input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder={_t('Name')}/> <br />
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder="Topic"/> <br /> <textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder={_t('Topic')}/> <br />
<RoomAlias ref="alias" alias={this.state.alias} homeserver={ domain } onChange={this.onAliasChanged}/> <br /> <RoomAlias ref="alias" alias={this.state.alias} homeserver={ domain } onChange={this.onAliasChanged}/> <br />
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br /> <UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br />
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br /> <Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
<div> <div>
<label> <label>
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/> <input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/>
Make this room private {_t('Make this room private')}
</label> </label>
</div> </div>
<div> <div>
<label> <label>
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged}/> <input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged}/>
Share message history with new users {_t('Share message history with new users')}
</label> </label>
</div> </div>
<div className="mx_CreateRoom_encrypt"> <div className="mx_CreateRoom_encrypt">
<label> <label>
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/> <input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/>
Encrypt room {_t('Encrypt room')}
</label> </label>
</div> </div>
<div> <div>

View file

@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); import React from 'react';
var ReactDOM = require("react-dom");
var Matrix = require("matrix-js-sdk"); import Matrix from 'matrix-js-sdk';
var sdk = require('../../index'); import sdk from '../../index';
var MatrixClientPeg = require("../../MatrixClientPeg"); import MatrixClientPeg from '../../MatrixClientPeg';
var dis = require("../../dispatcher"); import { _t, _tJsx } from '../../languageHandler';
/* /*
* Component which shows the filtered file using a TimelinePanel * Component which shows the filtered file using a TimelinePanel
@ -59,6 +58,8 @@ var FilePanel = React.createClass({
var client = MatrixClientPeg.get(); var client = MatrixClientPeg.get();
var room = client.getRoom(roomId); var room = client.getRoom(roomId);
this.noRoom = !room;
if (room) { if (room) {
var filter = new Matrix.Filter(client.credentials.userId); var filter = new Matrix.Filter(client.credentials.userId);
filter.setDefinition( filter.setDefinition(
@ -82,13 +83,24 @@ var FilePanel = React.createClass({
console.error("Failed to get or create file panel filter", error); console.error("Failed to get or create file panel filter", error);
} }
); );
} } else {
else {
console.error("Failed to add filtered timelineSet for FilePanel as no room!"); console.error("Failed to add filtered timelineSet for FilePanel as no room!");
} }
}, },
render: function() { render: function() {
if (MatrixClientPeg.get().isGuest()) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty">
{_tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{sub}</a>)}
</div>
</div>;
} else if (this.noRoom) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty">{_t("You must join the room to see its files")}</div>
</div>;
}
// wrap a TimelinePanel with the jump-to-event bits turned off. // wrap a TimelinePanel with the jump-to-event bits turned off.
var TimelinePanel = sdk.getComponent("structures.TimelinePanel"); var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
var Loader = sdk.getComponent("elements.Spinner"); var Loader = sdk.getComponent("elements.Spinner");
@ -105,7 +117,7 @@ var FilePanel = React.createClass({
showUrlPreview = { false } showUrlPreview = { false }
tileShape="file_grid" tileShape="file_grid"
opacity={ this.props.opacity } opacity={ this.props.opacity }
empty="There are no visible files in this room" empty={_t('There are no visible files in this room')}
/> />
); );
} }

View file

@ -19,8 +19,6 @@ const InteractiveAuth = Matrix.InteractiveAuth;
import React from 'react'; import React from 'react';
import sdk from '../../index';
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents'; import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
export default React.createClass({ export default React.createClass({

View file

@ -18,11 +18,15 @@ limitations under the License.
import * as Matrix from 'matrix-js-sdk'; import * as Matrix from 'matrix-js-sdk';
import React from 'react'; import React from 'react';
import UserSettingsStore from '../../UserSettingsStore';
import KeyCode from '../../KeyCode'; import KeyCode from '../../KeyCode';
import Notifier from '../../Notifier'; import Notifier from '../../Notifier';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler';
import sdk from '../../index'; import sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import sessionStore from '../../stores/SessionStore';
import MatrixClientPeg from '../../MatrixClientPeg';
/** /**
* This is what our MatrixChat shows when we are logged in. The precise view is * This is what our MatrixChat shows when we are logged in. The precise view is
@ -39,10 +43,13 @@ export default React.createClass({
propTypes: { propTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired, matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
page_type: React.PropTypes.string.isRequired, page_type: React.PropTypes.string.isRequired,
onRoomIdResolved: React.PropTypes.func,
onRoomCreated: React.PropTypes.func, onRoomCreated: React.PropTypes.func,
onUserSettingsClose: React.PropTypes.func, onUserSettingsClose: React.PropTypes.func,
// Called with the credentials of a registered user (if they were a ROU that
// transitioned to PWLU)
onRegistered: React.PropTypes.func,
teamToken: React.PropTypes.string, teamToken: React.PropTypes.string,
// and lots and lots of other stuff. // and lots and lots of other stuff.
@ -63,6 +70,13 @@ export default React.createClass({
}; };
}, },
getInitialState: function() {
return {
// use compact timeline view
useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'),
};
},
componentWillMount: function() { componentWillMount: function() {
// stash the MatrixClient in case we log out before we are unmounted // stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient; this._matrixClient = this.props.matrixClient;
@ -71,11 +85,35 @@ export default React.createClass({
// RoomView.getScrollState() // RoomView.getScrollState()
this._scrollStateMap = {}; this._scrollStateMap = {};
CallMediaHandler.loadDevices();
document.addEventListener('keydown', this._onKeyDown); document.addEventListener('keydown', this._onKeyDown);
this._sessionStore = sessionStore;
this._sessionStoreToken = this._sessionStore.addListener(
this._setStateFromSessionStore,
);
this._setStateFromSessionStore();
this._matrixClient.on("accountData", this.onAccountData);
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
document.removeEventListener('keydown', this._onKeyDown); document.removeEventListener('keydown', this._onKeyDown);
this._matrixClient.removeListener("accountData", this.onAccountData);
if (this._sessionStoreToken) {
this._sessionStoreToken.remove();
}
},
// Child components assume that the client peg will not be null, so give them some
// sort of assurance here by only allowing a re-render if the client is truthy.
//
// This is required because `LoggedInView` maintains its own state and if this state
// updates after the client peg has been made null (during logout), then it will
// attempt to re-render and the children will throw errors.
shouldComponentUpdate: function() {
return Boolean(MatrixClientPeg.get());
}, },
getScrollStateForRoom: function(roomId) { getScrollStateForRoom: function(roomId) {
@ -89,6 +127,20 @@ export default React.createClass({
return this.refs.roomView.canResetTimeline(); return this.refs.roomView.canResetTimeline();
}, },
_setStateFromSessionStore() {
this.setState({
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
});
},
onAccountData: function(event) {
if (event.getType() === "im.vector.web.settings") {
this.setState({
useCompactLayout: event.getContent().useCompactLayout,
});
}
},
_onKeyDown: function(ev) { _onKeyDown: function(ev) {
/* /*
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
@ -107,18 +159,6 @@ export default React.createClass({
var handled = false; var handled = false;
switch (ev.keyCode) { switch (ev.keyCode) {
case KeyCode.ESCAPE:
// Implemented this way so possible handling for other pages is neater
switch (this.props.page_type) {
case PageTypes.UserSettings:
this.props.onUserSettingsClose();
handled = true;
break;
}
break;
case KeyCode.UP: case KeyCode.UP:
case KeyCode.DOWN: case KeyCode.DOWN:
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
@ -171,8 +211,9 @@ export default React.createClass({
const RoomDirectory = sdk.getComponent('structures.RoomDirectory'); const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
const HomePage = sdk.getComponent('structures.HomePage'); const HomePage = sdk.getComponent('structures.HomePage');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
const NewVersionBar = sdk.getComponent('globals.NewVersionBar'); const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
let page_element; let page_element;
let right_panel = ''; let right_panel = '';
@ -181,33 +222,29 @@ export default React.createClass({
case PageTypes.RoomView: case PageTypes.RoomView:
page_element = <RoomView page_element = <RoomView
ref='roomView' ref='roomView'
roomAddress={this.props.currentRoomAlias || this.props.currentRoomId}
autoJoin={this.props.autoJoin} autoJoin={this.props.autoJoin}
onRoomIdResolved={this.props.onRoomIdResolved} onRegistered={this.props.onRegistered}
eventId={this.props.initialEventId}
thirdPartyInvite={this.props.thirdPartyInvite} thirdPartyInvite={this.props.thirdPartyInvite}
oobData={this.props.roomOobData} oobData={this.props.roomOobData}
highlightedEventId={this.props.highlightedEventId}
eventPixelOffset={this.props.initialEventPixelOffset} eventPixelOffset={this.props.initialEventPixelOffset}
key={this.props.currentRoomAlias || this.props.currentRoomId} key={this.props.currentRoomId || 'roomview'}
opacity={this.props.middleOpacity} opacity={this.props.middleOpacity}
collapsedRhs={this.props.collapse_rhs} collapsedRhs={this.props.collapse_rhs}
ConferenceHandler={this.props.ConferenceHandler} ConferenceHandler={this.props.ConferenceHandler}
scrollStateMap={this._scrollStateMap} scrollStateMap={this._scrollStateMap}
/>; />;
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.sideOpacity} />; if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
break; break;
case PageTypes.UserSettings: case PageTypes.UserSettings:
page_element = <UserSettings page_element = <UserSettings
onClose={this.props.onUserSettingsClose} onClose={this.props.onUserSettingsClose}
brand={this.props.config.brand} brand={this.props.config.brand}
collapsedRhs={this.props.collapse_rhs}
enableLabs={this.props.config.enableLabs} enableLabs={this.props.config.enableLabs}
referralBaseUrl={this.props.config.referralBaseUrl} referralBaseUrl={this.props.config.referralBaseUrl}
teamToken={this.props.teamToken} teamToken={this.props.teamToken}
/>; />;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>; if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
break; break;
case PageTypes.CreateRoom: case PageTypes.CreateRoom:
@ -215,7 +252,7 @@ export default React.createClass({
onRoomCreated={this.props.onRoomCreated} onRoomCreated={this.props.onRoomCreated}
collapsedRhs={this.props.collapse_rhs} collapsedRhs={this.props.collapse_rhs}
/>; />;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>; if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
break; break;
case PageTypes.RoomDirectory: case PageTypes.RoomDirectory:
@ -226,30 +263,36 @@ export default React.createClass({
break; break;
case PageTypes.HomePage: case PageTypes.HomePage:
// If team server config is present, pass the teamServerURL. props.teamToken
// must also be set for the team page to be displayed, otherwise the
// welcomePageUrl is used (which might be undefined).
const teamServerUrl = this.props.config.teamServerConfig ?
this.props.config.teamServerConfig.teamServerURL : null;
page_element = <HomePage page_element = <HomePage
collapsedRhs={this.props.collapse_rhs} teamServerUrl={teamServerUrl}
teamServerUrl={this.props.config.teamServerConfig.teamServerURL}
teamToken={this.props.teamToken} teamToken={this.props.teamToken}
/> homePageUrl={this.props.config.welcomePageUrl}
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/> />;
break; break;
case PageTypes.UserView: case PageTypes.UserView:
page_element = null; // deliberately null for now page_element = null; // deliberately null for now
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.sideOpacity} />; right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
break; break;
} }
var topBar; let topBar;
const isGuest = this.props.matrixClient.isGuest();
if (this.props.hasNewVersion) { if (this.props.hasNewVersion) {
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion} topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
releaseNotes={this.props.newVersionReleaseNotes} releaseNotes={this.props.newVersionReleaseNotes}
/>; />;
} } else if (this.props.checkingForUpdate) {
else if (this.props.matrixClient.isGuest()) { topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
topBar = <GuestWarningBar />; } else if (this.state.userHasGeneratedPassword) {
} topBar = <PasswordNagBar />;
else if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) { } else if (!isGuest && Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
topBar = <MatrixToolbar />; topBar = <MatrixToolbar />;
} }
@ -257,6 +300,9 @@ export default React.createClass({
if (topBar) { if (topBar) {
bodyClasses += ' mx_MatrixChat_toolbarShowing'; bodyClasses += ' mx_MatrixChat_toolbarShowing';
} }
if (this.state.useCompactLayout) {
bodyClasses += ' mx_MatrixChat_useCompactLayout';
}
return ( return (
<div className='mx_MatrixChat_wrapper'> <div className='mx_MatrixChat_wrapper'>
@ -265,8 +311,7 @@ export default React.createClass({
<LeftPanel <LeftPanel
selectedRoom={this.props.currentRoomId} selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapse_lhs || false} collapsed={this.props.collapse_lhs || false}
opacity={this.props.sideOpacity} opacity={this.props.leftOpacity}
teamToken={this.props.teamToken}
/> />
<main className='mx_MatrixChat_middlePanel'> <main className='mx_MatrixChat_middlePanel'>
{page_element} {page_element}

File diff suppressed because it is too large Load diff

View file

@ -84,6 +84,15 @@ module.exports = React.createClass({
// shape parameter to be passed to EventTiles // shape parameter to be passed to EventTiles
tileShape: React.PropTypes.string, tileShape: React.PropTypes.string,
// show twelve hour timestamps
isTwelveHour: React.PropTypes.bool,
// show timestamps always
alwaysShowTimestamps: React.PropTypes.bool,
// hide redacted events as per old behaviour
hideRedactions: React.PropTypes.bool,
}, },
componentWillMount: function() { componentWillMount: function() {
@ -230,8 +239,8 @@ module.exports = React.createClass({
}, },
_getEventTiles: function() { _getEventTiles: function() {
var EventTile = sdk.getComponent('rooms.EventTile'); const EventTile = sdk.getComponent('rooms.EventTile');
var DateSeparator = sdk.getComponent('messages.DateSeparator'); const DateSeparator = sdk.getComponent('messages.DateSeparator');
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
this.eventNodes = {}; this.eventNodes = {};
@ -310,7 +319,7 @@ module.exports = React.createClass({
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial"); const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) { if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1}/></li>; let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
ret.push(dateSeparator); ret.push(dateSeparator);
} }
@ -413,8 +422,8 @@ module.exports = React.createClass({
}, },
_getTilesForEvent: function(prevEvent, mxEv, last) { _getTilesForEvent: function(prevEvent, mxEv, last) {
var EventTile = sdk.getComponent('rooms.EventTile'); const EventTile = sdk.getComponent('rooms.EventTile');
var DateSeparator = sdk.getComponent('messages.DateSeparator'); const DateSeparator = sdk.getComponent('messages.DateSeparator');
var ret = []; var ret = [];
// is this a continuation of the previous message? // is this a continuation of the previous message?
@ -452,11 +461,13 @@ module.exports = React.createClass({
// do we need a date separator since the last event? // do we need a date separator since the last event?
if (this._wantsDateSeparator(prevEvent, eventDate)) { if (this._wantsDateSeparator(prevEvent, eventDate)) {
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1}/></li>; var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
ret.push(dateSeparator); ret.push(dateSeparator);
continuation = false; continuation = false;
} }
if (mxEv.isRedacted() && this.props.hideRedactions) return ret;
var eventId = mxEv.getId(); var eventId = mxEv.getId();
var highlight = (eventId == this.props.highlightedEventId); var highlight = (eventId == this.props.highlightedEventId);
@ -468,7 +479,6 @@ module.exports = React.createClass({
if (this.props.manageReadReceipts) { if (this.props.manageReadReceipts) {
readReceipts = this._getReadReceiptsForEvent(mxEv); readReceipts = this._getReadReceiptsForEvent(mxEv);
} }
ret.push( ret.push(
<li key={eventId} <li key={eventId}
ref={this._collectEventNode.bind(this, eventId)} ref={this._collectEventNode.bind(this, eventId)}
@ -482,6 +492,7 @@ module.exports = React.createClass({
checkUnmounting={this._isUnmounting} checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.status} eventSendStatus={mxEv.status}
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
last={last} isSelectedEvent={highlight}/> last={last} isSelectedEvent={highlight}/>
</li> </li>
); );
@ -615,8 +626,13 @@ module.exports = React.createClass({
var style = this.props.hidden ? { display: 'none' } : {}; var style = this.props.hidden ? { display: 'none' } : {};
style.opacity = this.props.opacity; style.opacity = this.props.opacity;
var className = this.props.className + " mx_fadable";
if (this.props.alwaysShowTimestamps) {
className += " mx_MessagePanel_alwaysShowTimestamps";
}
return ( return (
<ScrollPanel ref="scrollPanel" className={ this.props.className + " mx_fadable" } <ScrollPanel ref="scrollPanel" className={ className }
onScroll={ this.props.onScroll } onScroll={ this.props.onScroll }
onResize={ this.onResize } onResize={ this.onResize }
onFillRequest={ this.props.onFillRequest } onFillRequest={ this.props.onFillRequest }

View file

@ -16,7 +16,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var ReactDOM = require("react-dom"); var ReactDOM = require("react-dom");
import { _t } from '../../languageHandler';
var Matrix = require("matrix-js-sdk"); var Matrix = require("matrix-js-sdk");
var sdk = require('../../index'); var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg"); var MatrixClientPeg = require("../../MatrixClientPeg");
@ -37,7 +37,6 @@ var NotificationPanel = React.createClass({
var Loader = sdk.getComponent("elements.Spinner"); var Loader = sdk.getComponent("elements.Spinner");
var timelineSet = MatrixClientPeg.get().getNotifTimelineSet(); var timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) { if (timelineSet) {
return ( return (
<TimelinePanel key={"NotificationPanel_" + this.props.roomId} <TimelinePanel key={"NotificationPanel_" + this.props.roomId}
@ -48,7 +47,7 @@ var NotificationPanel = React.createClass({
showUrlPreview = { false } showUrlPreview = { false }
opacity={ this.props.opacity } opacity={ this.props.opacity }
tileShape="notif" tileShape="notif"
empty="You have no visible notifications" empty={ _t('You have no visible notifications') }
/> />
); );
} }

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); import React from 'react';
var sdk = require('../../index'); import { _t, _tJsx } from '../../languageHandler';
var dis = require("../../dispatcher"); import sdk from '../../index';
var WhoIsTyping = require("../../WhoIsTyping"); import WhoIsTyping from '../../WhoIsTyping';
var MatrixClientPeg = require("../../MatrixClientPeg"); import MatrixClientPeg from '../../MatrixClientPeg';
const MemberAvatar = require("../views/avatars/MemberAvatar"); import MemberAvatar from '../views/avatars/MemberAvatar';
const HIDE_DEBOUNCE_MS = 10000; const HIDE_DEBOUNCE_MS = 10000;
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
@ -175,8 +175,8 @@ module.exports = React.createClass({
<div className="mx_RoomStatusBar_scrollDownIndicator" <div className="mx_RoomStatusBar_scrollDownIndicator"
onClick={ this.props.onScrollToBottomClick }> onClick={ this.props.onScrollToBottomClick }>
<img src="img/scrolldown.svg" width="24" height="24" <img src="img/scrolldown.svg" width="24" height="24"
alt="Scroll to bottom of page" alt={ _t("Scroll to bottom of page") }
title="Scroll to bottom of page"/> title={ _t("Scroll to bottom of page") }/>
</div> </div>
); );
} }
@ -250,10 +250,10 @@ module.exports = React.createClass({
<div className="mx_RoomStatusBar_connectionLostBar"> <div className="mx_RoomStatusBar_connectionLostBar">
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/> <img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
<div className="mx_RoomStatusBar_connectionLostBar_title"> <div className="mx_RoomStatusBar_connectionLostBar_title">
Connectivity to the server has been lost. {_t('Connectivity to the server has been lost.')}
</div> </div>
<div className="mx_RoomStatusBar_connectionLostBar_desc"> <div className="mx_RoomStatusBar_connectionLostBar_desc">
Sent messages will be stored until your connection has returned. {_t('Sent messages will be stored until your connection has returned.')}
</div> </div>
</div> </div>
); );
@ -266,7 +266,7 @@ module.exports = React.createClass({
<TabCompleteBar tabComplete={this.props.tabComplete} /> <TabCompleteBar tabComplete={this.props.tabComplete} />
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|"> <div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
<TintableSvg src="img/eol.svg" width="22" height="16"/> <TintableSvg src="img/eol.svg" width="22" height="16"/>
Auto-complete {_t('Auto-complete')}
</div> </div>
</div> </div>
</div> </div>
@ -281,15 +281,13 @@ module.exports = React.createClass({
{ this.props.unsentMessageError } { this.props.unsentMessageError }
</div> </div>
<div className="mx_RoomStatusBar_connectionLostBar_desc"> <div className="mx_RoomStatusBar_connectionLostBar_desc">
<a className="mx_RoomStatusBar_resend_link" {_tJsx("<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
onClick={ this.props.onResendAllClick }> [/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
Resend all [
</a> or <a (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={ this.props.onResendAllClick }>{sub}</a>,
className="mx_RoomStatusBar_resend_link" (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={ this.props.onCancelAllClick }>{sub}</a>,
onClick={ this.props.onCancelAllClick }> ]
cancel all )}
</a> now. You can also select individual messages to
resend or cancel.
</div> </div>
</div> </div>
); );
@ -298,8 +296,8 @@ module.exports = React.createClass({
// unread count trumps who is typing since the unread count is only // unread count trumps who is typing since the unread count is only
// set when you've scrolled up // set when you've scrolled up
if (this.props.numUnreadMessages) { if (this.props.numUnreadMessages) {
var unreadMsgs = this.props.numUnreadMessages + " new message" + // MUST use var name "count" for pluralization to kick in
(this.props.numUnreadMessages > 1 ? "s" : ""); var unreadMsgs = _t("%(count)s new messages", {count: this.props.numUnreadMessages});
return ( return (
<div className="mx_RoomStatusBar_unreadMessagesBar" <div className="mx_RoomStatusBar_unreadMessagesBar"
@ -324,7 +322,7 @@ module.exports = React.createClass({
if (this.props.hasActiveCall) { if (this.props.hasActiveCall) {
return ( return (
<div className="mx_RoomStatusBar_callBar"> <div className="mx_RoomStatusBar_callBar">
<b>Active call</b> <b>{_t('Active call')}</b>
</div> </div>
); );
} }

View file

@ -25,6 +25,7 @@ var ReactDOM = require("react-dom");
var q = require("q"); var q = require("q");
var classNames = require("classnames"); var classNames = require("classnames");
var Matrix = require("matrix-js-sdk"); var Matrix = require("matrix-js-sdk");
import { _t } from '../../languageHandler';
var UserSettingsStore = require('../../UserSettingsStore'); var UserSettingsStore = require('../../UserSettingsStore');
var MatrixClientPeg = require("../../MatrixClientPeg"); var MatrixClientPeg = require("../../MatrixClientPeg");
@ -44,6 +45,8 @@ import KeyCode from '../../KeyCode';
import UserProvider from '../../autocomplete/UserProvider'; import UserProvider from '../../autocomplete/UserProvider';
import RoomViewStore from '../../stores/RoomViewStore';
var DEBUG = false; var DEBUG = false;
if (DEBUG) { if (DEBUG) {
@ -58,16 +61,9 @@ module.exports = React.createClass({
propTypes: { propTypes: {
ConferenceHandler: React.PropTypes.any, ConferenceHandler: React.PropTypes.any,
// Either a room ID or room alias for the room to display. // Called with the credentials of a registered user (if they were a ROU that
// If the room is being displayed as a result of the user clicking // transitioned to PWLU)
// on a room alias, the alias should be supplied. Otherwise, a room onRegistered: React.PropTypes.func,
// ID should be supplied.
roomAddress: React.PropTypes.string.isRequired,
// If a room alias is passed to roomAddress, a function can be
// provided here that will be called with the ID of the room
// once it has been resolved.
onRoomIdResolved: React.PropTypes.func,
// An object representing a third party invite to join this room // An object representing a third party invite to join this room
// Fields: // Fields:
@ -87,36 +83,8 @@ module.exports = React.createClass({
// * invited us tovthe room // * invited us tovthe room
oobData: React.PropTypes.object, 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? // is the RightPanel collapsed?
collapsedRhs: React.PropTypes.bool, 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() { getInitialState: function() {
@ -124,6 +92,17 @@ module.exports = React.createClass({
room: null, room: null,
roomId: null, roomId: null,
roomLoading: true, 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, editingRoomSettings: false,
uploadingRoomSettings: false, uploadingRoomSettings: false,
numUnreadMessages: 0, numUnreadMessages: 0,
@ -169,40 +148,72 @@ module.exports = React.createClass({
onClickCompletes: true, onClickCompletes: true,
onStateChange: (isCompleting) => { onStateChange: (isCompleting) => {
this.forceUpdate(); this.forceUpdate();
} },
}); });
if (this.props.roomAddress[0] == '#') { // Start listening for RoomViewStore updates
// we always look up the alias from the directory server: this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
// we want the room that the given alias is pointing to this._onRoomViewStoreUpdate(true);
// right now. We may have joined that alias before but there's },
// no guarantee the alias hasn't subsequently been remapped.
MatrixClientPeg.get().getRoomIdForAlias(this.props.roomAddress).done((result) => { _onRoomViewStoreUpdate: function(initial) {
if (this.props.onRoomIdResolved) { if (this.unmounted) {
this.props.onRoomIdResolved(result.room_id); return;
} }
var room = MatrixClientPeg.get().getRoom(result.room_id); const newState = {
this.setState({ roomId: RoomViewStore.getRoomId(),
room: room, roomAlias: RoomViewStore.getRoomAlias(),
roomId: result.room_id, roomLoading: RoomViewStore.isRoomLoading(),
roomLoading: !room, roomLoadError: RoomViewStore.getRoomLoadError(),
unsentMessageError: this._getUnsentMessageError(room), joining: RoomViewStore.isJoining(),
}, this._onHaveRoom); initialEventId: RoomViewStore.getInitialEventId(),
}, (err) => { initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(),
this.setState({ isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
roomLoading: false, forwardingEvent: RoomViewStore.getForwardingEvent(),
roomLoadError: err, shouldPeek: RoomViewStore.shouldPeek(),
}); };
});
} else { // finished joining, start waiting for a room and show a spinner. See onRoom.
var room = MatrixClientPeg.get().getRoom(this.props.roomAddress); newState.waitingForRoom = this.state.joining && !newState.joining &&
this.setState({ !RoomViewStore.getJoinError();
roomId: this.props.roomAddress,
room: room, // Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
roomLoading: !room, console.log(
unsentMessageError: this._getUnsentMessageError(room), 'RVS update:',
}, this._onHaveRoom); 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();
}
});
}, },
_onHaveRoom: function() { _onHaveRoom: function() {
@ -217,29 +228,31 @@ module.exports = React.createClass({
// which must be by alias or invite wherever possible (peeking currently does // which must be by alias or invite wherever possible (peeking currently does
// not work over federation). // not work over federation).
// NB. We peek if we are not in the room, although if we try to peek into // NB. We peek if we have never seen the room before (i.e. js-sdk does not know
// a room in which we have a member event (ie. we've left) synapse will just // about it). We don't peek in the historical case where we were joined but are
// send us the same data as we get in the sync (ie. the last events we saw). // now not joined because the js-sdk peeking API will clobber our historical room,
var user_is_in_room = null; // making it impossible to indicate a newly joined room.
if (this.state.room) { const room = this.state.room;
user_is_in_room = this.state.room.hasMembershipState( if (room) {
MatrixClientPeg.get().credentials.userId, 'join' UserProvider.getInstance().setUserListFromRoom(room);
); this.tabComplete.loadEntries(room);
this.setState({
UserProvider.getInstance().setUserListFromRoom(this.state.room); unsentMessageError: this._getUnsentMessageError(room),
this.tabComplete.loadEntries(this.state.room); });
this._onRoomLoaded(room);
} }
if (!this.state.joining && this.state.roomId) {
if (!user_is_in_room && this.state.roomId) {
if (this.props.autoJoin) { if (this.props.autoJoin) {
this.onJoinButtonClicked(); this.onJoinButtonClicked();
} else if (this.state.roomId) { } else if (!room && this.state.shouldPeek) {
console.log("Attempting to peek into room %s", this.state.roomId); console.log("Attempting to peek into room %s", this.state.roomId);
this.setState({
peekLoading: true,
});
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => { MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
this.setState({ this.setState({
room: room, room: room,
roomLoading: false, peekLoading: false,
}); });
this._onRoomLoaded(room); this._onRoomLoaded(room);
}, (err) => { }, (err) => {
@ -249,16 +262,16 @@ module.exports = React.createClass({
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") { if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
// This is fine: the room just isn't peekable (we assume). // This is fine: the room just isn't peekable (we assume).
this.setState({ this.setState({
roomLoading: false, peekLoading: false,
}); });
} else { } else {
throw err; throw err;
} }
}).done(); }).done();
} }
} else if (user_is_in_room) { } else if (room) {
// Stop peeking because we have joined this room previously
MatrixClientPeg.get().stopPeeking(); MatrixClientPeg.get().stopPeeking();
this._onRoomLoaded(this.state.room);
} }
}, },
@ -294,17 +307,6 @@ module.exports = React.createClass({
} }
}, },
componentWillReceiveProps: function(newProps) {
if (newProps.roomAddress != this.props.roomAddress) {
throw new Error("changing room on a RoomView is not supported");
}
if (newProps.eventId != this.props.eventId) {
// when we change focussed event id, hide the search results.
this.setState({searchResults: null});
}
},
shouldComponentUpdate: function(nextProps, nextState) { shouldComponentUpdate: function(nextProps, nextState) {
return (!ObjectUtils.shallowEqual(this.props, nextProps) || return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
!ObjectUtils.shallowEqual(this.state, nextState)); !ObjectUtils.shallowEqual(this.state, nextState));
@ -330,7 +332,7 @@ module.exports = React.createClass({
this.unmounted = true; this.unmounted = true;
// update the scroll map before we get unmounted // update the scroll map before we get unmounted
this._updateScrollMap(); this._updateScrollMap(this.state.roomId);
if (this.refs.roomView) { if (this.refs.roomView) {
// disconnect the D&D event listeners from the room view. This // disconnect the D&D event listeners from the room view. This
@ -359,6 +361,11 @@ module.exports = React.createClass({
document.removeEventListener("keydown", this.onKeyDown); document.removeEventListener("keydown", this.onKeyDown);
// Remove RoomStore listener
if (this._roomStoreToken) {
this._roomStoreToken.remove();
}
// cancel any pending calls to the rate_limited_funcs // cancel any pending calls to the rate_limited_funcs
this._updateRoomMembers.cancelPendingCall(); this._updateRoomMembers.cancelPendingCall();
@ -370,10 +377,10 @@ module.exports = React.createClass({
onPageUnload(event) { onPageUnload(event) {
if (ContentMessages.getCurrentUploads().length > 0) { if (ContentMessages.getCurrentUploads().length > 0) {
return event.returnValue = return event.returnValue =
'You seem to be uploading files, are you sure you want to quit?'; _t("You seem to be uploading files, are you sure you want to quit?");
} else if (this._getCallForRoom() && this.state.callState !== 'ended') { } else if (this._getCallForRoom() && this.state.callState !== 'ended') {
return event.returnValue = return event.returnValue =
'You seem to be in a call, are you sure you want to quit?'; _t("You seem to be in a call, are you sure you want to quit?");
} }
}, },
@ -529,14 +536,14 @@ module.exports = React.createClass({
if (!userHasUsedEncryption) { if (!userHasUsedEncryption) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Warning!", title: _t("Warning!"),
hasCancelButton: false, hasCancelButton: false,
description: ( description: (
<div> <div>
<p>End-to-end encryption is in beta and may not be reliable.</p> <p>{ _t("End-to-end encryption is in beta and may not be reliable") }.</p>
<p>You should <b>not</b> yet trust it to secure data.</p> <p>{ _t("You should not yet trust it to secure data") }.</p>
<p>Devices will <b>not</b> yet be able to decrypt history from before they joined the room.</p> <p>{ _t("Devices will not yet be able to decrypt history from before they joined the room") }.</p>
<p>Encrypted messages will not be visible on clients that do not yet implement encryption.</p> <p>{ _t("Encrypted messages will not be visible on clients that do not yet implement encryption") }.</p>
</div> </div>
), ),
}); });
@ -598,21 +605,28 @@ 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) { onRoom: function(room) {
// This event is fired when the room is 'stored' by the JS SDK, which if (!room || room.roomId !== this.state.roomId) {
// means it's now a fully-fledged room object ready to be used, so return;
// set it in our state and start using it (ie. init the timeline) }
// This will happen if we start off viewing a room we're not joined,
// then join it whilst RoomView is looking at that room.
if (!this.state.room && room.roomId == this._joiningRoomId) {
this._joiningRoomId = undefined;
this.setState({ this.setState({
room: room, room: room,
joining: false, waitingForRoom: false,
}); }, () => {
this._onRoomLoaded(room); this._onRoomLoaded(room);
} });
}, },
updateTint: function() { updateTint: function() {
@ -665,8 +679,15 @@ module.exports = React.createClass({
onRoomMemberMembership: function(ev, member, oldMembership) { onRoomMemberMembership: function(ev, member, oldMembership) {
if (member.userId == MatrixClientPeg.get().credentials.userId) { if (member.userId == MatrixClientPeg.get().credentials.userId) {
if (member.membership === 'join') {
this.setState({
waitingForRoom: false,
});
} else {
this.forceUpdate(); this.forceUpdate();
} }
}
}, },
// rate limited because a power level change will emit an event for every // rate limited because a power level change will emit an event for every
@ -695,10 +716,6 @@ module.exports = React.createClass({
// compatability workaround, let's not bother. // compatability workaround, let's not bother.
Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()).done(); Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()).done();
} }
this.setState({
joining: false
});
} }
}, 500), }, 500),
@ -707,10 +724,10 @@ module.exports = React.createClass({
if (!unsentMessages.length) return ""; if (!unsentMessages.length) return "";
for (const event of unsentMessages) { for (const event of unsentMessages) {
if (!event.error || event.error.name !== "UnknownDeviceError") { if (!event.error || event.error.name !== "UnknownDeviceError") {
return "Some of your messages have not been sent."; return _t("Some of your messages have not been sent.");
} }
} }
return "Message not sent due to unknown devices being present"; return _t("Message not sent due to unknown devices being present");
}, },
_getUnsentMessages: function(room) { _getUnsentMessages: function(room) {
@ -773,41 +790,62 @@ module.exports = React.createClass({
}, },
onJoinButtonClicked: function(ev) { onJoinButtonClicked: function(ev) {
var self = this; const cli = MatrixClientPeg.get();
var cli = MatrixClientPeg.get(); // If the user is a ROU, allow them to transition to a PWLU
var display_name_promise = q(); if (cli && cli.isGuest()) {
// if this is the first room we're joining, check the user has a display name // Join this room once the user has registered and logged in
// and if they don't, prompt them to set one. const signUrl = this.props.thirdPartyInvite ?
// NB. This unfortunately does not re-use the ChangeDisplayName component because this.props.thirdPartyInvite.inviteSignUrl : undefined;
// it doesn't behave quite as desired here (we want an input field here rather than dis.dispatch({
// content-editable, and we want a default). action: 'do_after_sync_prepared',
if (cli.getRooms().filter((r) => { deferred_action: {
return r.hasMembershipState(cli.credentials.userId, "join"); action: 'join_room',
})) { opts: { inviteSignUrl: signUrl },
display_name_promise = cli.getProfileInfo(cli.credentials.userId).then((result) => { },
if (!result.displayname) { });
var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog');
var dialog_defer = q.defer(); // Don't peek whilst registering otherwise getPendingEventList complains
Modal.createDialog(SetDisplayNameDialog, { // Do this by indicating our intention to join
currentDisplayName: result.displayname, dis.dispatch({
onFinished: (submitted, newDisplayName) => { action: 'will_join',
});
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
const close = Modal.createDialog(SetMxIdDialog, {
homeserverUrl: cli.getHomeserverUrl(),
onFinished: (submitted, credentials) => {
if (submitted) { if (submitted) {
cli.setDisplayName(newDisplayName).done(() => { this.props.onRegistered(credentials);
dialog_defer.resolve(); } else {
dis.dispatch({
action: 'cancel_after_sync_prepared',
});
dis.dispatch({
action: 'cancel_join',
}); });
} }
else { },
dialog_defer.reject(); onDifferentServerClicked: (ev) => {
} dis.dispatch({action: 'start_registration'});
} close();
}); },
return dialog_defer.promise; onLoginClick: (ev) => {
} dis.dispatch({action: 'start_login'});
}); close();
},
}).close;
return;
} }
display_name_promise.then(() => { q().then(() => {
const signUrl = this.props.thirdPartyInvite ?
this.props.thirdPartyInvite.inviteSignUrl : undefined;
dis.dispatch({
action: 'join_room',
opts: { inviteSignUrl: signUrl },
});
// if this is an invite and has the 'direct' hint set, mark it as a DM room now. // if this is an invite and has the 'direct' hint set, mark it as a DM room now.
if (this.state.room) { if (this.state.room) {
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId); const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
@ -819,72 +857,7 @@ module.exports = React.createClass({
} }
} }
} }
return q(); return q();
}).then(() => {
var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined;
return MatrixClientPeg.get().joinRoom(this.props.roomAddress,
{ inviteSignUrl: sign_url } );
}).then(function(resp) {
var roomId = resp.roomId;
// It is possible that there is no Room yet if state hasn't come down
// from /sync - joinRoom will resolve when the HTTP request to join succeeds,
// NOT when it comes down /sync. If there is no room, we'll keep the
// joining flag set until we see it.
// We'll need to initialise the timeline when joining, but due to
// the above, we can't do it here: we do it in onRoom instead,
// once we have a useable room object.
var room = MatrixClientPeg.get().getRoom(roomId);
if (!room) {
// wait for the room to turn up in onRoom.
self._joiningRoomId = roomId;
} else {
// we've got a valid room, but that might also just mean that
// it was peekable (so we had one before anyway). If we are
// not yet a member of the room, we will need to wait for that
// to happen, in onRoomStateMember.
var me = MatrixClientPeg.get().credentials.userId;
self.setState({
joining: !room.hasMembershipState(me, "join"),
room: room
});
}
}).catch(function(error) {
self.setState({
joining: false,
joinError: error
});
if (!error) return;
// https://matrix.org/jira/browse/SYN-659
// Need specific error message if joining a room is refused because the user is a guest and guest access is not allowed
if (
error.errcode == 'M_GUEST_ACCESS_FORBIDDEN' ||
(
error.errcode == 'M_FORBIDDEN' &&
MatrixClientPeg.get().isGuest()
)
) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Failed to join the room",
description: "This room is private or inaccessible to guests. You may be able to join if you register."
});
} else {
var msg = error.message ? error.message : JSON.stringify(error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to join room",
description: msg
});
}
}).done();
this.setState({
joining: true
}); });
}, },
@ -936,11 +909,7 @@ module.exports = React.createClass({
uploadFile: function(file) { uploadFile: function(file) {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); dis.dispatch({action: 'view_set_mxid'});
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't upload files. Please register to upload."
});
return; return;
} }
@ -958,8 +927,8 @@ module.exports = React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to upload file " + file + " " + error); console.error("Failed to upload file " + file + " " + error);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to upload file", title: _t('Failed to upload file'),
description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or the file too big"), description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
}); });
}); });
}, },
@ -1045,8 +1014,8 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Search failed: " + error); console.error("Search failed: " + error);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Search failed", title: _t("Search failed"),
description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or search timed out :("), description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")),
}); });
}).finally(function() { }).finally(function() {
self.setState({ self.setState({
@ -1081,12 +1050,12 @@ module.exports = React.createClass({
if (!this.state.searchResults.next_batch) { if (!this.state.searchResults.next_batch) {
if (this.state.searchResults.results.length == 0) { if (this.state.searchResults.results.length == 0) {
ret.push(<li key="search-top-marker"> ret.push(<li key="search-top-marker">
<h2 className="mx_RoomView_topMarker">No results</h2> <h2 className="mx_RoomView_topMarker">{ _t("No results") }</h2>
</li> </li>
); );
} else { } else {
ret.push(<li key="search-top-marker"> ret.push(<li key="search-top-marker">
<h2 className="mx_RoomView_topMarker">No more results</h2> <h2 className="mx_RoomView_topMarker">{ _t("No more results") }</h2>
</li> </li>
); );
} }
@ -1123,10 +1092,10 @@ module.exports = React.createClass({
// it. We should tell the js sdk to go and find out about // it. We should tell the js sdk to go and find out about
// it. But that's not an issue currently, as synapse only // it. But that's not an issue currently, as synapse only
// returns results for rooms we're joined to. // returns results for rooms we're joined to.
var roomName = room ? room.name : "Unknown room "+roomId; var roomName = room ? room.name : _t("Unknown room %(roomId)s", { roomId: roomId });
ret.push(<li key={mxEv.getId() + "-room"}> ret.push(<li key={mxEv.getId() + "-room"}>
<h1>Room: { roomName }</h1> <h1>{ _t("Room") }: { roomName }</h1>
</li>); </li>);
lastRoomId = roomId; lastRoomId = roomId;
} }
@ -1172,7 +1141,7 @@ module.exports = React.createClass({
}); });
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to save settings", title: _t("Failed to save settings"),
description: fails.map(function(result) { return result.reason; }).join("\n"), description: fails.map(function(result) { return result.reason; }).join("\n"),
}); });
// still editing room settings // still editing room settings
@ -1193,7 +1162,15 @@ module.exports = React.createClass({
onCancelClick: function() { onCancelClick: function() {
console.log("updateTint from onCancelClick"); console.log("updateTint from onCancelClick");
this.updateTint(); this.updateTint();
this.setState({editingRoomSettings: false}); this.setState({
editingRoomSettings: false,
});
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'forward_event',
event: null,
});
}
dis.dispatch({action: 'focus_composer'}); dis.dispatch({action: 'focus_composer'});
}, },
@ -1208,11 +1185,11 @@ module.exports = React.createClass({
MatrixClientPeg.get().forget(this.state.room.roomId).done(function() { MatrixClientPeg.get().forget(this.state.room.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' }); dis.dispatch({ action: 'view_next_room' });
}, function(err) { }, function(err) {
var errCode = err.errcode || "unknown error code"; var errCode = err.errcode || _t("unknown error code");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: _t("Error"),
description: `Failed to forget room (${errCode})` description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
}); });
}); });
}, },
@ -1233,8 +1210,8 @@ module.exports = React.createClass({
var msg = error.message ? error.message : JSON.stringify(error); var msg = error.message ? error.message : JSON.stringify(error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to reject invite", title: _t("Failed to reject invite"),
description: msg description: msg,
}); });
self.setState({ self.setState({
@ -1295,21 +1272,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 // get the current scroll position of the room, so that it can be
// restored when we switch back to it. // restored when we switch back to it.
// //
@ -1463,26 +1425,30 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var RoomHeader = sdk.getComponent('rooms.RoomHeader'); const RoomHeader = sdk.getComponent('rooms.RoomHeader');
var MessageComposer = sdk.getComponent('rooms.MessageComposer'); const MessageComposer = sdk.getComponent('rooms.MessageComposer');
var RoomSettings = sdk.getComponent("rooms.RoomSettings"); const ForwardMessage = sdk.getComponent("rooms.ForwardMessage");
var AuxPanel = sdk.getComponent("rooms.AuxPanel"); const RoomSettings = sdk.getComponent("rooms.RoomSettings");
var SearchBar = sdk.getComponent("rooms.SearchBar"); const AuxPanel = sdk.getComponent("rooms.AuxPanel");
var ScrollPanel = sdk.getComponent("structures.ScrollPanel"); const SearchBar = sdk.getComponent("rooms.SearchBar");
var TintableSvg = sdk.getComponent("elements.TintableSvg"); const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
var RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
var Loader = sdk.getComponent("elements.Spinner"); const RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
var TimelinePanel = sdk.getComponent("structures.TimelinePanel"); 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.room) {
if (this.state.roomLoading) { if (this.state.roomLoading || this.state.peekLoading) {
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<Loader /> <Loader />
</div> </div>
); );
} } else {
else {
var inviterName = undefined; var inviterName = undefined;
if (this.props.oobData) { if (this.props.oobData) {
inviterName = this.props.oobData.inviterName; inviterName = this.props.oobData.inviterName;
@ -1494,7 +1460,7 @@ module.exports = React.createClass({
// We have no room object for this room, only the ID. // 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. // We've got to this room by following a link, possibly a third party invite.
var room_alias = this.props.roomAddress[0] == '#' ? this.props.roomAddress : null; const roomAlias = this.state.roomAlias;
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<RoomHeader ref="header" <RoomHeader ref="header"
@ -1507,8 +1473,8 @@ module.exports = React.createClass({
onForgetClick={ this.onForgetClick } onForgetClick={ this.onForgetClick }
onRejectClick={ this.onRejectThreepidInviteButtonClicked } onRejectClick={ this.onRejectThreepidInviteButtonClicked }
canPreview={ false } error={ this.state.roomLoadError } canPreview={ false } error={ this.state.roomLoadError }
roomAlias={room_alias} roomAlias={roomAlias}
spinner={this.state.joining} spinner={previewBarSpinner}
inviterName={inviterName} inviterName={inviterName}
invitedEmail={invitedEmail} invitedEmail={invitedEmail}
room={this.state.room} room={this.state.room}
@ -1551,7 +1517,7 @@ module.exports = React.createClass({
onRejectClick={ this.onRejectButtonClicked } onRejectClick={ this.onRejectButtonClicked }
inviterName={ inviterName } inviterName={ inviterName }
canPreview={ false } canPreview={ false }
spinner={this.state.joining} spinner={previewBarSpinner}
room={this.state.room} room={this.state.room}
/> />
</div> </div>
@ -1600,17 +1566,18 @@ module.exports = React.createClass({
/>; />;
} }
var aux = null; let aux = null;
let hideCancel = false;
if (this.state.editingRoomSettings) { if (this.state.editingRoomSettings) {
aux = <RoomSettings ref="room_settings" onSaveClick={this.onSettingsSaveClick} onCancelClick={this.onCancelClick} room={this.state.room} />; aux = <RoomSettings ref="room_settings" onSaveClick={this.onSettingsSaveClick} onCancelClick={this.onCancelClick} room={this.state.room} />;
} } else if (this.state.uploadingRoomSettings) {
else if (this.state.uploadingRoomSettings) {
aux = <Loader/>; aux = <Loader/>;
} } else if (this.state.forwardingEvent !== null) {
else if (this.state.searching) { aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
} else if (this.state.searching) {
hideCancel = true; // has own cancel
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress } onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch}/>; aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress } onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch}/>;
} } else if (!myMember || myMember.membership !== "join") {
else if (!myMember || myMember.membership !== "join") {
// We do have a room object for this room, but we're not currently in it. // We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it. // We may have a 3rd party invite to it.
var inviterName = undefined; var inviterName = undefined;
@ -1621,11 +1588,12 @@ module.exports = React.createClass({
if (this.props.thirdPartyInvite) { if (this.props.thirdPartyInvite) {
invitedEmail = this.props.thirdPartyInvite.invitedEmail; invitedEmail = this.props.thirdPartyInvite.invitedEmail;
} }
hideCancel = true;
aux = ( aux = (
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} <RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={ this.onForgetClick } onForgetClick={ this.onForgetClick }
onRejectClick={this.onRejectThreepidInviteButtonClicked} onRejectClick={this.onRejectThreepidInviteButtonClicked}
spinner={this.state.joining} spinner={previewBarSpinner}
inviterName={inviterName} inviterName={inviterName}
invitedEmail={invitedEmail} invitedEmail={invitedEmail}
canPreview={this.state.canPeek} canPreview={this.state.canPeek}
@ -1672,7 +1640,7 @@ module.exports = React.createClass({
if (call.type === "video") { if (call.type === "video") {
zoomButton = ( zoomButton = (
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title="Fill screen"> <div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title={ _t("Fill screen") }>
<TintableSvg src="img/fullscreen.svg" width="29" height="22" style={{ marginTop: 1, marginRight: 4 }}/> <TintableSvg src="img/fullscreen.svg" width="29" height="22" style={{ marginTop: 1, marginRight: 4 }}/>
</div> </div>
); );
@ -1680,14 +1648,14 @@ module.exports = React.createClass({
videoMuteButton = videoMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}> <div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
<TintableSvg src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"} <TintableSvg src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"}
alt={call.isLocalVideoMuted() ? "Click to unmute video" : "Click to mute video"} alt={call.isLocalVideoMuted() ? _t("Click to unmute video") : _t("Click to mute video")}
width="31" height="27"/> width="31" height="27"/>
</div>; </div>;
} }
voiceMuteButton = voiceMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}> <div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
<TintableSvg src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"} <TintableSvg src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"}
alt={call.isMicrophoneMuted() ? "Click to unmute audio" : "Click to mute audio"} alt={call.isMicrophoneMuted() ? _t("Click to unmute audio") : _t("Click to mute audio")}
width="21" height="26"/> width="21" height="26"/>
</div>; </div>;
@ -1722,17 +1690,24 @@ module.exports = React.createClass({
hideMessagePanel = true; hideMessagePanel = true;
} }
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); const shouldHighlight = this.state.isInitialEventHighlighted;
let highlightedEventId = null;
if (this.state.forwardingEvent) {
highlightedEventId = this.state.forwardingEvent.getId();
} else if (shouldHighlight) {
highlightedEventId = this.state.initialEventId;
}
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
var messagePanel = ( var messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef} <TimelinePanel ref={this._gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()} timelineSet={this.state.room.getUnfilteredTimelineSet()}
manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)} manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
manageReadMarkers={true} manageReadMarkers={true}
hidden={hideMessagePanel} hidden={hideMessagePanel}
highlightedEventId={this.props.highlightedEventId} highlightedEventId={highlightedEventId}
eventId={this.props.eventId} eventId={this.state.initialEventId}
eventPixelOffset={this.props.eventPixelOffset} eventPixelOffset={this.state.initialEventPixelOffset}
onScroll={ this.onMessageListScroll } onScroll={ this.onMessageListScroll }
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar } onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
showUrlPreview = { this.state.showUrlPreview } showUrlPreview = { this.state.showUrlPreview }
@ -1763,17 +1738,15 @@ module.exports = React.createClass({
oobData={this.props.oobData} oobData={this.props.oobData}
editing={this.state.editingRoomSettings} editing={this.state.editingRoomSettings}
saving={this.state.uploadingRoomSettings} saving={this.state.uploadingRoomSettings}
inRoom={myMember && myMember.membership === 'join'}
collapsedRhs={ this.props.collapsedRhs } collapsedRhs={ this.props.collapsedRhs }
onSearchClick={this.onSearchClick} onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick} onSettingsClick={this.onSettingsClick}
onSaveClick={this.onSettingsSaveClick} onSaveClick={this.onSettingsSaveClick}
onCancelClick={this.onCancelClick} onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={ onForgetClick={(myMember && myMember.membership === "leave") ? this.onForgetClick : null}
(myMember && myMember.membership === "leave") ? this.onForgetClick : null onLeaveClick={(myMember && myMember.membership === "join") ? this.onLeaveClick : null}
} />
onLeaveClick={
(myMember && myMember.membership === "join") ? this.onLeaveClick : null
} />
{ auxPanel } { auxPanel }
{ topUnreadMessagesBar } { topUnreadMessagesBar }
{ messagePanel } { messagePanel }

View file

@ -352,13 +352,14 @@ module.exports = React.createClass({
const tile = tiles[backwards ? i : tiles.length - 1 - i]; const tile = tiles[backwards ? i : tiles.length - 1 - i];
// Subtract height of tile as if it were unpaginated // Subtract height of tile as if it were unpaginated
excessHeight -= tile.clientHeight; excessHeight -= tile.clientHeight;
//If removing the tile would lead to future pagination, break before setting scroll token
if (tile.clientHeight > excessHeight) {
break;
}
// The tile may not have a scroll token, so guard it // The tile may not have a scroll token, so guard it
if (tile.dataset.scrollTokens) { if (tile.dataset.scrollTokens) {
markerScrollToken = tile.dataset.scrollTokens.split(',')[0]; markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
} }
if (tile.clientHeight > excessHeight) {
break;
}
} }
if (markerScrollToken) { if (markerScrollToken) {

View file

@ -23,12 +23,14 @@ var Matrix = require("matrix-js-sdk");
var EventTimeline = Matrix.EventTimeline; var EventTimeline = Matrix.EventTimeline;
var sdk = require('../../index'); var sdk = require('../../index');
import { _t } from '../../languageHandler';
var MatrixClientPeg = require("../../MatrixClientPeg"); var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher"); var dis = require("../../dispatcher");
var ObjectUtils = require('../../ObjectUtils'); var ObjectUtils = require('../../ObjectUtils');
var Modal = require("../../Modal"); var Modal = require("../../Modal");
var UserActivity = require("../../UserActivity"); var UserActivity = require("../../UserActivity");
var KeyCode = require('../../KeyCode'); var KeyCode = require('../../KeyCode');
import UserSettingsStore from '../../UserSettingsStore';
var PAGINATE_SIZE = 20; var PAGINATE_SIZE = 20;
var INITIAL_SIZE = 20; var INITIAL_SIZE = 20;
@ -129,6 +131,8 @@ var TimelinePanel = React.createClass({
} }
} }
const syncedSettings = UserSettingsStore.getSyncedSettings();
return { return {
events: [], events: [],
timelineLoading: true, // track whether our room timeline is loading timelineLoading: true, // track whether our room timeline is loading
@ -171,14 +175,23 @@ var TimelinePanel = React.createClass({
// cache of matrixClient.getSyncState() (but from the 'sync' event) // cache of matrixClient.getSyncState() (but from the 'sync' event)
clientSyncState: MatrixClientPeg.get().getSyncState(), clientSyncState: MatrixClientPeg.get().getSyncState(),
// should the event tiles have twelve hour times
isTwelveHour: syncedSettings.showTwelveHourTimestamps,
// always show timestamps on event tiles?
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps,
// hide redacted events as per old behaviour
hideRedactions: syncedSettings.hideRedactions,
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
debuglog("TimelinePanel: mounting"); debuglog("TimelinePanel: mounting");
this.last_rr_sent_event_id = undefined; this.lastRRSentEventId = undefined;
this.last_rm_sent_event_id = undefined; this.lastRMSentEventId = undefined;
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
@ -504,12 +517,13 @@ var TimelinePanel = React.createClass({
// very possible have logged out within that timeframe, so check // very possible have logged out within that timeframe, so check
// we still have a client. // we still have a client.
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
// if no client or client is guest don't send RR // if no client or client is guest don't send RR or RM
if (!cli || cli.isGuest()) return; if (!cli || cli.isGuest()) return;
var currentReadUpToEventId = this._getCurrentReadReceipt(true); let shouldSendRR = true;
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
const currentRREventId = this._getCurrentReadReceipt(true);
const currentRREventIndex = this._indexForEventId(currentRREventId);
// We want to avoid sending out read receipts when we are looking at // We want to avoid sending out read receipts when we are looking at
// events in the past which are before the latest RR. // events in the past which are before the latest RR.
// //
@ -523,43 +537,60 @@ var TimelinePanel = React.createClass({
// RRs) - but that is a bit of a niche case. It will sort itself out when // RRs) - but that is a bit of a niche case. It will sort itself out when
// the user eventually hits the live timeline. // the user eventually hits the live timeline.
// //
if (currentReadUpToEventId && currentReadUpToEventIndex === null && if (currentRREventId && currentRREventIndex === null &&
this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
return; shouldSendRR = false;
} }
var lastReadEventIndex = this._getLastDisplayedEventIndex({ const lastReadEventIndex = this._getLastDisplayedEventIndex({
ignoreOwn: true ignoreOwn: true,
}); });
if (lastReadEventIndex === null) return; if (lastReadEventIndex === null) {
shouldSendRR = false;
}
let lastReadEvent = this.state.events[lastReadEventIndex];
shouldSendRR = shouldSendRR &&
// Only send a RR if the last read event is ahead in the timeline relative to
// the current RR event.
lastReadEventIndex > currentRREventIndex &&
// Only send a RR if the last RR set != the one we would send
this.lastRRSentEventId != lastReadEvent.getId();
var lastReadEvent = this.state.events[lastReadEventIndex]; // Only send a RM if the last RM sent != the one we would send
const shouldSendRM =
this.lastRMSentEventId != this.state.readMarkerEventId;
// we also remember the last read receipt we sent to avoid spamming the // we also remember the last read receipt we sent to avoid spamming the
// same one at the server repeatedly // same one at the server repeatedly
if ((lastReadEventIndex > currentReadUpToEventIndex && if (shouldSendRR || shouldSendRM) {
this.last_rr_sent_event_id != lastReadEvent.getId()) || if (shouldSendRR) {
this.last_rm_sent_event_id != this.state.readMarkerEventId) { this.lastRRSentEventId = lastReadEvent.getId();
} else {
this.last_rr_sent_event_id = lastReadEvent.getId(); lastReadEvent = null;
this.last_rm_sent_event_id = this.state.readMarkerEventId; }
this.lastRMSentEventId = this.state.readMarkerEventId;
debuglog('TimelinePanel: Sending Read Markers for ',
this.props.timelineSet.room.roomId,
'rm', this.state.readMarkerEventId,
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
);
MatrixClientPeg.get().setRoomReadMarkers( MatrixClientPeg.get().setRoomReadMarkers(
this.props.timelineSet.room.roomId, this.props.timelineSet.room.roomId,
this.state.readMarkerEventId, this.state.readMarkerEventId,
lastReadEvent lastReadEvent, // Could be null, in which case no RR is sent
).catch((e) => { ).catch((e) => {
// /read_markers API is not implemented on this HS, fallback to just RR // /read_markers API is not implemented on this HS, fallback to just RR
if (e.errcode === 'M_UNRECOGNIZED') { if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
return MatrixClientPeg.get().sendReadReceipt( return MatrixClientPeg.get().sendReadReceipt(
lastReadEvent lastReadEvent,
).catch(() => { ).catch(() => {
this.last_rr_sent_event_id = undefined; this.lastRRSentEventId = undefined;
}); });
} }
// it failed, so allow retries next time the user is active // it failed, so allow retries next time the user is active
this.last_rr_sent_event_id = undefined; this.lastRRSentEventId = undefined;
this.last_rm_sent_event_id = undefined; this.lastRMSentEventId = undefined;
}); });
// do a quick-reset of our unreadNotificationCount to avoid having // do a quick-reset of our unreadNotificationCount to avoid having
@ -572,7 +603,6 @@ var TimelinePanel = React.createClass({
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
dis.dispatch({ dis.dispatch({
action: 'on_room_read', action: 'on_room_read',
room: this.props.timelineSet.room,
}); });
} }
} }
@ -872,6 +902,9 @@ var TimelinePanel = React.createClass({
var onError = (error) => { var onError = (error) => {
this.setState({timelineLoading: false}); this.setState({timelineLoading: false});
console.error(
`Error loading timeline panel at ${eventId}: ${error}`,
);
var msg = error.message ? error.message : JSON.stringify(error); var msg = error.message ? error.message : JSON.stringify(error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -890,14 +923,11 @@ var TimelinePanel = React.createClass({
}); });
}; };
} }
var message = "Tried to load a specific point in this room's timeline, but "; var message = (error.errcode == 'M_FORBIDDEN')
if (error.errcode == 'M_FORBIDDEN') { ? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.")
message += "you do not have permission to view the message in question."; : _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
} else {
message += "was unable to find it.";
}
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to load timeline position", title: _t("Failed to load timeline position"),
description: message, description: message,
onFinished: onFinished, onFinished: onFinished,
}); });
@ -1089,10 +1119,10 @@ var TimelinePanel = React.createClass({
const forwardPaginating = ( const forwardPaginating = (
this.state.forwardPaginating || this.state.clientSyncState == 'PREPARED' this.state.forwardPaginating || this.state.clientSyncState == 'PREPARED'
); );
return ( return (
<MessagePanel ref="messagePanel" <MessagePanel ref="messagePanel"
hidden={ this.props.hidden } hidden={ this.props.hidden }
hideRedactions={ this.state.hideRedactions }
backPaginating={ this.state.backPaginating } backPaginating={ this.state.backPaginating }
forwardPaginating={ forwardPaginating } forwardPaginating={ forwardPaginating }
events={ this.state.events } events={ this.state.events }
@ -1108,6 +1138,8 @@ var TimelinePanel = React.createClass({
onFillRequest={ this.onMessageListFillRequest } onFillRequest={ this.onMessageListFillRequest }
onUnfillRequest={ this.onMessageListUnfillRequest } onUnfillRequest={ this.onMessageListUnfillRequest }
opacity={ this.props.opacity } opacity={ this.props.opacity }
isTwelveHour={ this.state.isTwelveHour }
alwaysShowTimestamps={ this.state.alwaysShowTimestamps }
className={ this.props.className } className={ this.props.className }
tileShape={ this.props.tileShape } tileShape={ this.props.tileShape }
/> />

View file

@ -18,6 +18,7 @@ var React = require('react');
var ContentMessages = require('../../ContentMessages'); var ContentMessages = require('../../ContentMessages');
var dis = require('../../dispatcher'); var dis = require('../../dispatcher');
var filesize = require('filesize'); var filesize = require('filesize');
import { _t } from '../../languageHandler';
module.exports = React.createClass({displayName: 'UploadBar', module.exports = React.createClass({displayName: 'UploadBar',
propTypes: { propTypes: {
@ -81,10 +82,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
uploadedSize = uploadedSize.replace(/ .*/, ''); uploadedSize = uploadedSize.replace(/ .*/, '');
} }
var others; // MUST use var name 'count' for pluralization to kick in
if (uploads.length > 1) { var uploadText = _t("Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)});
others = ' and ' + (uploads.length - 1) + ' other' + (uploads.length > 2 ? 's' : '');
}
return ( return (
<div className="mx_UploadBar"> <div className="mx_UploadBar">
@ -98,7 +97,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
<div className="mx_UploadBar_uploadBytes"> <div className="mx_UploadBar_uploadBytes">
{ uploadedSize } / { totalSize } { uploadedSize } / { totalSize }
</div> </div>
<div className="mx_UploadBar_uploadFilename">Uploading {upload.fileName}{others}</div> <div className="mx_UploadBar_uploadFilename">{uploadText}</div>
</div> </div>
); );
} }

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); var React = require('react');
import { _t } from '../../../languageHandler';
var sdk = require('../../../index'); var sdk = require('../../../index');
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
@ -54,7 +55,7 @@ module.exports = React.createClass({
progress: "sent_email" progress: "sent_email"
}); });
}, (err) => { }, (err) => {
this.showErrorDialog("Failed to send email: " + err.message); this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
this.setState({ this.setState({
progress: null progress: null
}); });
@ -78,30 +79,33 @@ module.exports = React.createClass({
ev.preventDefault(); ev.preventDefault();
if (!this.state.email) { if (!this.state.email) {
this.showErrorDialog("The email address linked to your account must be entered."); this.showErrorDialog(_t('The email address linked to your account must be entered.'));
} }
else if (!this.state.password || !this.state.password2) { else if (!this.state.password || !this.state.password2) {
this.showErrorDialog("A new password must be entered."); this.showErrorDialog(_t('A new password must be entered.'));
} }
else if (this.state.password !== this.state.password2) { else if (this.state.password !== this.state.password2) {
this.showErrorDialog("New passwords must match each other."); this.showErrorDialog(_t('New passwords must match each other.'));
} }
else { else {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Warning", title: _t('Warning!'),
description: description:
<div> <div>
Resetting password will currently reset any end-to-end encryption keys on all devices, { _t(
making encrypted chat history unreadable, unless you first export your room keys 'Resetting password will currently reset any ' +
and re-import them afterwards. 'end-to-end encryption keys on all devices, ' +
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">will be improved</a>. 'making encrypted chat history unreadable, ' +
'unless you first export your room keys and re-import ' +
'them afterwards. In future this will be improved.'
) }
</div>, </div>,
button: "Continue", button: _t('Continue'),
extraButtons: [ extraButtons: [
<button className="mx_Dialog_primary" <button className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}> onClick={this._onExportE2eKeysClicked}>
Export E2E room keys { _t('Export E2E room keys') }
</button> </button>
], ],
onFinished: (confirmed) => { onFinished: (confirmed) => {
@ -150,7 +154,7 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: title, title: title,
description: body description: body,
}); });
}, },
@ -168,22 +172,20 @@ module.exports = React.createClass({
else if (this.state.progress === "sent_email") { else if (this.state.progress === "sent_email") {
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div>
An email has been sent to {this.state.email}. Once you&#39;ve followed { _t('An email has been sent to') } {this.state.email}. { _t('Once you&#39;ve followed the link it contains, click below') }.
the link it contains, click below.
<br /> <br />
<input className="mx_Login_submit" type="button" onClick={this.onVerify} <input className="mx_Login_submit" type="button" onClick={this.onVerify}
value="I have verified my email address" /> value={ _t('I have verified my email address') } />
</div> </div>
); );
} }
else if (this.state.progress === "complete") { else if (this.state.progress === "complete") {
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div>
<p>Your password has been reset.</p> <p>{ _t('Your password has been reset') }.</p>
<p>You have been logged out of all devices and will no longer receive push notifications. <p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
To re-enable notifications, sign in again on each device.</p>
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete} <input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
value="Return to login screen" /> value={ _t('Return to login screen') } />
</div> </div>
); );
} }
@ -191,7 +193,7 @@ module.exports = React.createClass({
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div>
<div className="mx_Login_prompt"> <div className="mx_Login_prompt">
To reset your password, enter the email address linked to your account: { _t('To reset your password, enter the email address linked to your account') }:
</div> </div>
<div> <div>
<form onSubmit={this.onSubmitForm}> <form onSubmit={this.onSubmitForm}>
@ -199,21 +201,21 @@ module.exports = React.createClass({
name="reset_email" // define a name so browser's password autofill gets less confused name="reset_email" // define a name so browser's password autofill gets less confused
value={this.state.email} value={this.state.email}
onChange={this.onInputChanged.bind(this, "email")} onChange={this.onInputChanged.bind(this, "email")}
placeholder="Email address" autoFocus /> placeholder={ _t('Email address') } autoFocus />
<br /> <br />
<input className="mx_Login_field" ref="pass" type="password" <input className="mx_Login_field" ref="pass" type="password"
name="reset_password" name="reset_password"
value={this.state.password} value={this.state.password}
onChange={this.onInputChanged.bind(this, "password")} onChange={this.onInputChanged.bind(this, "password")}
placeholder="New password" /> placeholder={ _t('New password') } />
<br /> <br />
<input className="mx_Login_field" ref="pass" type="password" <input className="mx_Login_field" ref="pass" type="password"
name="reset_password_confirm" name="reset_password_confirm"
value={this.state.password2} value={this.state.password2}
onChange={this.onInputChanged.bind(this, "password2")} onChange={this.onInputChanged.bind(this, "password2")}
placeholder="Confirm your new password" /> placeholder={ _t('Confirm your new password') } />
<br /> <br />
<input className="mx_Login_submit" type="submit" value="Send Reset Email" /> <input className="mx_Login_submit" type="submit" value={ _t('Send Reset Email') } />
</form> </form>
<ServerConfig ref="serverConfig" <ServerConfig ref="serverConfig"
withToggleButton={true} withToggleButton={true}
@ -227,10 +229,10 @@ module.exports = React.createClass({
<div className="mx_Login_error"> <div className="mx_Login_error">
</div> </div>
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#"> <a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
Return to login {_t('Return to login screen')}
</a> </a>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#"> <a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
Create a new account { _t('Create an account') }
</a> </a>
<LoginFooter /> <LoginFooter />
</div> </div>

View file

@ -18,8 +18,7 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import { _t, _tJsx } from '../../../languageHandler';
import url from 'url';
import sdk from '../../../index'; import sdk from '../../../index';
import Login from '../../../Login'; import Login from '../../../Login';
@ -88,7 +87,27 @@ module.exports = React.createClass({
).then((data) => { ).then((data) => {
this.props.onLoggedIn(data); this.props.onLoggedIn(data);
}, (error) => { }, (error) => {
this._setStateFromError(error, true); let errorText;
// Some error strings only apply for logging in
const usingEmail = username.indexOf("@") > 0;
if (error.httpStatus == 400 && usingEmail) {
errorText = _t('This Home Server does not support login using email address.');
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
errorText = _t('Incorrect username and/or password.');
} else {
// other errors, not specific to doing a password login
errorText = this._errorTextFromError(error);
}
this.setState({
errorText: errorText,
// 401 would be the sensible status code for 'incorrect password'
// but the login API gives a 403 https://matrix.org/jira/browse/SYN-744
// mentions this (although the bug is for UI auth which is not this)
// We treat both as an incorrect password
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
});
}).finally(() => { }).finally(() => {
this.setState({ this.setState({
busy: false busy: false
@ -111,7 +130,16 @@ module.exports = React.createClass({
this._loginLogic.loginAsGuest().then(function(data) { this._loginLogic.loginAsGuest().then(function(data) {
self.props.onLoggedIn(data); self.props.onLoggedIn(data);
}, function(error) { }, function(error) {
self._setStateFromError(error, true); let errorText;
if (error.httpStatus === 403) {
errorText = _t("Guest access is disabled on this Home Server.");
} else {
errorText = self._errorTextFromError(error);
}
self.setState({
errorText: errorText,
loginIncorrect: false,
});
}).finally(function() { }).finally(function() {
self.setState({ self.setState({
busy: false busy: false
@ -130,7 +158,7 @@ module.exports = React.createClass({
onPhoneNumberChanged: function(phoneNumber) { onPhoneNumberChanged: function(phoneNumber) {
// Validate the phone number entered // Validate the phone number entered
if (!PHONE_NUMBER_REGEX.test(phoneNumber)) { if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
this.setState({ errorText: 'The phone number entered looks invalid' }); this.setState({ errorText: _t('The phone number entered looks invalid') });
return; return;
} }
@ -184,53 +212,48 @@ module.exports = React.createClass({
currentFlow: self._getCurrentFlowStep(), currentFlow: self._getCurrentFlowStep(),
}); });
}, function(err) { }, function(err) {
self._setStateFromError(err, false); self.setState({
errorText: self._errorTextFromError(err),
loginIncorrect: false,
});
}).finally(function() { }).finally(function() {
self.setState({ self.setState({
busy: false, busy: false,
}); });
}); }).done();
}, },
_getCurrentFlowStep: function() { _getCurrentFlowStep: function() {
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null; return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
}, },
_setStateFromError: function(err, isLoginAttempt) {
this.setState({
errorText: this._errorTextFromError(err),
// https://matrix.org/jira/browse/SYN-744
loginIncorrect: isLoginAttempt && (err.httpStatus == 401 || err.httpStatus == 403)
});
},
_errorTextFromError(err) { _errorTextFromError(err) {
if (err.friendlyText) {
return err.friendlyText;
}
let errCode = err.errcode; let errCode = err.errcode;
if (!errCode && err.httpStatus) { if (!errCode && err.httpStatus) {
errCode = "HTTP " + err.httpStatus; errCode = "HTTP " + err.httpStatus;
} }
let errorText = "Error: Problem communicating with the given homeserver " + let errorText = _t("Error: Problem communicating with the given homeserver.") +
(errCode ? " (" + errCode + ")" : ""); (errCode ? " (" + errCode + ")" : "");
if (err.cors === 'rejected') { if (err.cors === 'rejected') {
if (window.location.protocol === 'https:' && if (window.location.protocol === 'https:' &&
(this.state.enteredHomeserverUrl.startsWith("http:") || (this.state.enteredHomeserverUrl.startsWith("http:") ||
!this.state.enteredHomeserverUrl.startsWith("http"))) !this.state.enteredHomeserverUrl.startsWith("http"))
{ ) {
errorText = <span> errorText = <span>
Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. { _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
Either use HTTPS or <a href='https://www.google.com/search?&q=enable%20unsafe%20scripts'>enable unsafe scripts</a> "Either use HTTPS or <a>enable unsafe scripts</a>.",
/<a>(.*?)<\/a>/,
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; }
)}
</span>; </span>;
} } else {
else {
errorText = <span> errorText = <span>
Can't connect to homeserver - please check your connectivity and ensure { _tJsx("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
your <a href={ this.state.enteredHomeserverUrl }>homeserver's SSL certificate</a> is trusted. /<a>(.*?)<\/a>/,
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; }
)}
</span>; </span>;
} }
} }
@ -242,12 +265,6 @@ module.exports = React.createClass({
switch (step) { switch (step) {
case 'm.login.password': case 'm.login.password':
const PasswordLogin = sdk.getComponent('login.PasswordLogin'); const PasswordLogin = sdk.getComponent('login.PasswordLogin');
// HSs that are not matrix.org may not be configured to have their
// domain name === domain part.
let hsDomain = url.parse(this.state.enteredHomeserverUrl).hostname;
if (hsDomain !== 'matrix.org') {
hsDomain = null;
}
return ( return (
<PasswordLogin <PasswordLogin
onSubmit={this.onPasswordLogin} onSubmit={this.onPasswordLogin}
@ -259,7 +276,6 @@ module.exports = React.createClass({
onPhoneNumberChanged={this.onPhoneNumberChanged} onPhoneNumberChanged={this.onPhoneNumberChanged}
onForgotPasswordClick={this.props.onForgotPasswordClick} onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect} loginIncorrect={this.state.loginIncorrect}
hsDomain={hsDomain}
/> />
); );
case 'm.login.cas': case 'm.login.cas':
@ -273,8 +289,7 @@ module.exports = React.createClass({
} }
return ( return (
<div> <div>
Sorry, this homeserver is using a login which is not { _t('Sorry, this homeserver is using a login which is not recognised ')}({step})
recognised ({step})
</div> </div>
); );
} }
@ -291,7 +306,7 @@ module.exports = React.createClass({
if (this.props.enableGuest) { if (this.props.enableGuest) {
loginAsGuestJsx = loginAsGuestJsx =
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#"> <a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
Login as guest { _t('Login as guest')}
</a>; </a>;
} }
@ -299,7 +314,7 @@ module.exports = React.createClass({
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
returnToAppJsx = returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#"> <a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app { _t('Return to app')}
</a>; </a>;
} }
@ -308,7 +323,7 @@ module.exports = React.createClass({
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader /> <LoginHeader />
<div> <div>
<h2>Sign in <h2>{ _t('Sign in')}
{ loader } { loader }
</h2> </h2>
{ this.componentForStep(this.state.currentFlow) } { this.componentForStep(this.state.currentFlow) }
@ -324,7 +339,7 @@ module.exports = React.createClass({
{ this.state.errorText } { this.state.errorText }
</div> </div>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#"> <a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
Create a new account { _t('Create an account')}
</a> </a>
{ loginAsGuestJsx } { loginAsGuestJsx }
{ returnToAppJsx } { returnToAppJsx }

View file

@ -16,9 +16,10 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); import React from 'react';
var sdk = require('../../../index'); import sdk from '../../../index';
var MatrixClientPeg = require('../../../MatrixClientPeg'); import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'PostRegistration', displayName: 'PostRegistration',
@ -49,7 +50,7 @@ module.exports = React.createClass({
}); });
}, function(error) { }, function(error) {
self.setState({ self.setState({
errorString: "Failed to fetch avatar URL", errorString: _t("Failed to fetch avatar URL"),
busy: false busy: false
}); });
}); });
@ -64,12 +65,12 @@ module.exports = React.createClass({
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader /> <LoginHeader />
<div className="mx_Login_profile"> <div className="mx_Login_profile">
Set a display name: { _t('Set a display name:') }
<ChangeDisplayName /> <ChangeDisplayName />
Upload an avatar: { _t('Upload an avatar:') }
<ChangeAvatar <ChangeAvatar
initialAvatarUrl={this.state.avatarUrl} /> initialAvatarUrl={this.state.avatarUrl} />
<button onClick={this.props.onComplete}>Continue</button> <button onClick={this.props.onComplete}>{ _t('Continue') }</button>
{this.state.errorString} {this.state.errorString}
</div> </div>
</div> </div>

View file

@ -21,12 +21,11 @@ import q from 'q';
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher';
import ServerConfig from '../../views/login/ServerConfig'; import ServerConfig from '../../views/login/ServerConfig';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import RegistrationForm from '../../views/login/RegistrationForm'; import RegistrationForm from '../../views/login/RegistrationForm';
import CaptchaForm from '../../views/login/CaptchaForm';
import RtsClient from '../../../RtsClient'; import RtsClient from '../../../RtsClient';
import { _t } from '../../../languageHandler';
const MIN_PASSWORD_LENGTH = 6; const MIN_PASSWORD_LENGTH = 6;
@ -46,8 +45,6 @@ module.exports = React.createClass({
brand: React.PropTypes.string, brand: React.PropTypes.string,
email: React.PropTypes.string, email: React.PropTypes.string,
referrer: React.PropTypes.string, referrer: React.PropTypes.string,
username: React.PropTypes.string,
guestAccessToken: React.PropTypes.string,
teamServerConfig: React.PropTypes.shape({ teamServerConfig: React.PropTypes.shape({
// Email address to request new teams // Email address to request new teams
supportEmail: React.PropTypes.string.isRequired, supportEmail: React.PropTypes.string.isRequired,
@ -98,7 +95,7 @@ module.exports = React.createClass({
this.props.teamServerConfig.teamServerURL && this.props.teamServerConfig.teamServerURL &&
!this._rtsClient !this._rtsClient
) { ) {
this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL); this._rtsClient = this.props.rtsClient || new RtsClient(this.props.teamServerConfig.teamServerURL);
this.setState({ this.setState({
teamServerBusy: true, teamServerBusy: true,
@ -162,7 +159,7 @@ module.exports = React.createClass({
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1; msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
} }
if (!msisdn_available) { if (!msisdn_available) {
msg = "This server does not support authentication with a phone number"; msg = _t('This server does not support authentication with a phone number.');
} }
} }
this.setState({ this.setState({
@ -221,30 +218,29 @@ module.exports = React.createClass({
} }
trackPromise.then((teamToken) => { trackPromise.then((teamToken) => {
console.info('Team token promise',teamToken); return this.props.onLoggedIn({
this.props.onLoggedIn({
userId: response.user_id, userId: response.user_id,
deviceId: response.device_id, deviceId: response.device_id,
homeserverUrl: this._matrixClient.getHomeserverUrl(), homeserverUrl: this._matrixClient.getHomeserverUrl(),
identityServerUrl: this._matrixClient.getIdentityServerUrl(), identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token accessToken: response.access_token
}, teamToken); }, teamToken);
}).then(() => { }).then((cli) => {
return this._setupPushers(); return this._setupPushers(cli);
}); });
}, },
_setupPushers: function() { _setupPushers: function(matrixClient) {
if (!this.props.brand) { if (!this.props.brand) {
return q(); return q();
} }
return MatrixClientPeg.get().getPushers().then((resp)=>{ return matrixClient.getPushers().then((resp)=>{
const pushers = resp.pushers; const pushers = resp.pushers;
for (let i = 0; i < pushers.length; ++i) { for (let i = 0; i < pushers.length; ++i) {
if (pushers[i].kind == 'email') { if (pushers[i].kind == 'email') {
const emailPusher = pushers[i]; const emailPusher = pushers[i];
emailPusher.data = { brand: this.props.brand }; emailPusher.data = { brand: this.props.brand };
MatrixClientPeg.get().setPusher(emailPusher).done(() => { matrixClient.setPusher(emailPusher).done(() => {
console.log("Set email branding to " + this.props.brand); console.log("Set email branding to " + this.props.brand);
}, (error) => { }, (error) => {
console.error("Couldn't set email branding: " + error); console.error("Couldn't set email branding: " + error);
@ -260,29 +256,29 @@ module.exports = React.createClass({
var errMsg; var errMsg;
switch (errCode) { switch (errCode) {
case "RegistrationForm.ERR_PASSWORD_MISSING": case "RegistrationForm.ERR_PASSWORD_MISSING":
errMsg = "Missing password."; errMsg = _t('Missing password.');
break; break;
case "RegistrationForm.ERR_PASSWORD_MISMATCH": case "RegistrationForm.ERR_PASSWORD_MISMATCH":
errMsg = "Passwords don't match."; errMsg = _t('Passwords don\'t match.');
break; break;
case "RegistrationForm.ERR_PASSWORD_LENGTH": case "RegistrationForm.ERR_PASSWORD_LENGTH":
errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`; errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH: MIN_PASSWORD_LENGTH});
break; break;
case "RegistrationForm.ERR_EMAIL_INVALID": case "RegistrationForm.ERR_EMAIL_INVALID":
errMsg = "This doesn't look like a valid email address"; errMsg = _t('This doesn\'t look like a valid email address.');
break; break;
case "RegistrationForm.ERR_PHONE_NUMBER_INVALID": case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
errMsg = "This doesn't look like a valid phone number"; errMsg = _t('This doesn\'t look like a valid phone number.');
break; break;
case "RegistrationForm.ERR_USERNAME_INVALID": case "RegistrationForm.ERR_USERNAME_INVALID":
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores."; errMsg = _t('User names may only contain letters, numbers, dots, hyphens and underscores.');
break; break;
case "RegistrationForm.ERR_USERNAME_BLANK": case "RegistrationForm.ERR_USERNAME_BLANK":
errMsg = "You need to enter a user name"; errMsg = _t('You need to enter a user name.');
break; break;
default: default:
console.error("Unknown error code: %s", errCode); console.error("Unknown error code: %s", errCode);
errMsg = "An unknown error occurred."; errMsg = _t('An unknown error occurred.');
break; break;
} }
this.setState({ this.setState({
@ -297,17 +293,6 @@ module.exports = React.createClass({
}, },
_makeRegisterRequest: function(auth) { _makeRegisterRequest: function(auth) {
let guestAccessToken = this.props.guestAccessToken;
if (
this.state.formVals.username !== this.props.username ||
this.state.hsUrl != this.props.defaultHsUrl
) {
// don't try to upgrade if we changed our username
// or are registering on a different HS
guestAccessToken = null;
}
// Only send the bind params if we're sending username / pw params // Only send the bind params if we're sending username / pw params
// (Since we need to send no params at all to use the ones saved in the // (Since we need to send no params at all to use the ones saved in the
// session). // session).
@ -322,7 +307,7 @@ module.exports = React.createClass({
undefined, // session id: included in the auth dict already undefined, // session id: included in the auth dict already
auth, auth,
bindThreepids, bindThreepids,
guestAccessToken, null,
); );
}, },
@ -359,10 +344,6 @@ module.exports = React.createClass({
} else if (this.state.busy || this.state.teamServerBusy) { } else if (this.state.busy || this.state.teamServerBusy) {
registerBody = <Spinner />; registerBody = <Spinner />;
} else { } else {
let guestUsername = this.props.username;
if (this.state.hsUrl != this.props.defaultHsUrl) {
guestUsername = null;
}
let errorSection; let errorSection;
if (this.state.errorText) { if (this.state.errorText) {
errorSection = <div className="mx_Login_error">{this.state.errorText}</div>; errorSection = <div className="mx_Login_error">{this.state.errorText}</div>;
@ -376,7 +357,6 @@ module.exports = React.createClass({
defaultPhoneNumber={this.state.formVals.phoneNumber} defaultPhoneNumber={this.state.formVals.phoneNumber}
defaultPassword={this.state.formVals.password} defaultPassword={this.state.formVals.password}
teamsConfig={this.state.teamsConfig} teamsConfig={this.state.teamsConfig}
guestUsername={guestUsername}
minPasswordLength={MIN_PASSWORD_LENGTH} minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed} onError={this.onFormValidationFailed}
onRegisterClick={this.onFormSubmit} onRegisterClick={this.onFormSubmit}
@ -400,7 +380,7 @@ module.exports = React.createClass({
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
returnToAppJsx = ( returnToAppJsx = (
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#"> <a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app {_t('Return to app')}
</a> </a>
); );
} }
@ -413,10 +393,10 @@ module.exports = React.createClass({
this.state.teamSelected.domain + "/icon.png" : this.state.teamSelected.domain + "/icon.png" :
null} null}
/> />
<h2>Create an account</h2> <h2>{_t('Create an account')}</h2>
{registerBody} {registerBody}
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#"> <a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
I already have an account {_t('I already have an account')}
</a> </a>
{returnToAppJsx} {returnToAppJsx}
<LoginFooter /> <LoginFooter />

View file

@ -32,6 +32,7 @@ module.exports = React.createClass({
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority] urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
width: React.PropTypes.number, width: React.PropTypes.number,
height: React.PropTypes.number, height: React.PropTypes.number,
// XXX resizeMethod not actually used.
resizeMethod: React.PropTypes.string, resizeMethod: React.PropTypes.string,
defaultToInitialLetter: React.PropTypes.bool // true to add default url defaultToInitialLetter: React.PropTypes.bool // true to add default url
}, },

View file

@ -16,8 +16,8 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); import React from 'react';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'CreateRoomButton', displayName: 'CreateRoomButton',
propTypes: { propTypes: {
@ -36,7 +36,7 @@ module.exports = React.createClass({
render: function() { render: function() {
return ( return (
<button className="mx_CreateRoomButton" onClick={this.onClick}>Create Room</button> <button className="mx_CreateRoomButton" onClick={this.onClick}>{_t("Create Room")}</button>
); );
} }
}); });

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); var React = require('react');
import { _t } from '../../../languageHandler';
var Presets = { var Presets = {
PrivateChat: "private_chat", PrivateChat: "private_chat",
@ -46,9 +47,9 @@ module.exports = React.createClass({
render: function() { render: function() {
return ( return (
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}> <select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
<option value={this.Presets.PrivateChat}>Private Chat</option> <option value={this.Presets.PrivateChat}>{_t("Private Chat")}</option>
<option value={this.Presets.PublicChat}>Public Chat</option> <option value={this.Presets.PublicChat}>{_t("Public Chat")}</option>
<option value={this.Presets.Custom}>Custom</option> <option value={this.Presets.Custom}>{_t("Custom")}</option>
</select> </select>
); );
} }

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
var React = require('react'); var React = require('react');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomAlias', displayName: 'RoomAlias',
@ -94,7 +95,7 @@ module.exports = React.createClass({
render: function() { render: function() {
return ( return (
<input type="text" className="mx_RoomAlias" placeholder="Alias (optional)" <input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur} onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
value={this.props.alias}/> value={this.props.alias}/>
); );

View file

@ -47,19 +47,7 @@ export default React.createClass({
children: React.PropTypes.node, children: React.PropTypes.node,
}, },
componentWillMount: function() { _onKeyDown: function(e) {
this.priorActiveElement = document.activeElement;
},
componentWillUnmount: function() {
if (this.priorActiveElement !== null) {
this.priorActiveElement.focus();
}
},
// Must be when the key is released (and not pressed) otherwise componentWillUnmount
// will focus another element which will receive future key events
_onKeyUp: function(e) {
if (e.keyCode === KeyCode.ESCAPE) { if (e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -81,7 +69,7 @@ export default React.createClass({
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
return ( return (
<div onKeyUp={this._onKeyUp} className={this.props.className}> <div onKeyDown={this._onKeyDown} className={this.props.className}>
<AccessibleButton onClick={this._onCancelClick} <AccessibleButton onClick={this._onCancelClick}
className="mx_Dialog_cancelButton" className="mx_Dialog_cancelButton"
> >

View file

@ -16,36 +16,30 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap'; import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import Unread from '../../../Unread'; import Unread from '../../../Unread';
import classNames from 'classnames'; import classNames from 'classnames';
import createRoom from '../../../createRoom';
export default class ChatCreateOrReuseDialog extends React.Component { export default class ChatCreateOrReuseDialog extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.onNewDMClick = this.onNewDMClick.bind(this);
this.onRoomTileClick = this.onRoomTileClick.bind(this); this.onRoomTileClick = this.onRoomTileClick.bind(this);
this.state = {
tiles: [],
profile: {
displayName: null,
avatarUrl: null,
},
profileError: null,
};
} }
onNewDMClick() { componentWillMount() {
createRoom({dmUserId: this.props.userId});
this.props.onFinished(true);
}
onRoomTileClick(roomId) {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
this.props.onFinished(true);
}
render() {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const dmRoomMap = new DMRoomMap(client); const dmRoomMap = new DMRoomMap(client);
@ -70,40 +64,123 @@ export default class ChatCreateOrReuseDialog extends React.Component {
highlight={highlight} highlight={highlight}
isInvite={me.membership == "invite"} isInvite={me.membership == "invite"}
onClick={this.onRoomTileClick} onClick={this.onRoomTileClick}
/> />,
); );
} }
} }
this.setState({
tiles: tiles,
});
if (tiles.length === 0) {
this.setState({
busyProfile: true,
});
MatrixClientPeg.get().getProfileInfo(this.props.userId).done((resp) => {
const profile = {
displayName: resp.displayname,
avatarUrl: null,
};
if (resp.avatar_url) {
profile.avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(
resp.avatar_url, 48, 48, "crop",
);
}
this.setState({
busyProfile: false,
profile: profile,
});
}, (err) => {
console.error(
'Unable to get profile for user ' + this.props.userId + ':',
err,
);
this.setState({
busyProfile: false,
profileError: err,
});
});
}
}
onRoomTileClick(roomId) {
this.props.onExistingRoomSelected(roomId);
}
render() {
let title = '';
let content = null;
if (this.state.tiles.length > 0) {
// Show the existing rooms with a "+" to add a new dm
title = _t('Create a new chat or reuse an existing one');
const labelClasses = classNames({ const labelClasses = classNames({
mx_MemberInfo_createRoom_label: true, mx_MemberInfo_createRoom_label: true,
mx_RoomTile_name: true, mx_RoomTile_name: true,
}); });
const startNewChat = <AccessibleButton const startNewChat = <AccessibleButton
className="mx_MemberInfo_createRoom" className="mx_MemberInfo_createRoom"
onClick={this.onNewDMClick} onClick={this.props.onNewDMClick}
> >
<div className="mx_RoomTile_avatar"> <div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" /> <img src="img/create-big.svg" width="26" height="26" />
</div> </div>
<div className={labelClasses}><i>Start new chat</i></div> <div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
</AccessibleButton>; </AccessibleButton>;
content = <div className="mx_Dialog_content">
{ _t('You already have existing direct chats with this user:') }
<div className="mx_ChatCreateOrReuseDialog_tiles">
{ this.state.tiles }
{ startNewChat }
</div>
</div>;
} else {
// Show the avatar, name and a button to confirm that a new chat is requested
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const Spinner = sdk.getComponent('elements.Spinner');
title = _t('Start chatting');
let profile = null;
if (this.state.busyProfile) {
profile = <Spinner />;
} else if (this.state.profileError) {
profile = <div className="error">
Unable to load profile information for { this.props.userId }
</div>;
} else {
profile = <div className="mx_ChatCreateOrReuseDialog_profile">
<BaseAvatar
name={this.state.profile.displayName || this.props.userId}
url={this.state.profile.avatarUrl}
width={48} height={48}
/>
<div className="mx_ChatCreateOrReuseDialog_profile_name">
{this.state.profile.displayName || this.props.userId}
</div>
</div>;
}
content = <div>
<div className="mx_Dialog_content">
<p>
{ _t('Click on the button below to start chatting!') }
</p>
{ profile }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onNewDMClick}>
{ _t('Start Chatting') }
</button>
</div>
</div>;
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<BaseDialog className='mx_ChatCreateOrReuseDialog' <BaseDialog className='mx_ChatCreateOrReuseDialog'
onFinished={() => { onFinished={ this.props.onFinished.bind(false) }
this.props.onFinished(false) title={title}
}}
title='Create a new chat or reuse an existing one'
> >
<div className="mx_Dialog_content"> { content }
You already have existing direct chats with this user:
<div className="mx_ChatCreateOrReuseDialog_tiles">
{tiles}
{startNewChat}
</div>
</div>
</BaseDialog> </BaseDialog>
); );
} }
@ -111,5 +188,8 @@ export default class ChatCreateOrReuseDialog extends React.Component {
ChatCreateOrReuseDialog.propTyps = { ChatCreateOrReuseDialog.propTyps = {
userId: React.PropTypes.string.isRequired, userId: React.PropTypes.string.isRequired,
// Called when clicking outside of the dialog
onFinished: React.PropTypes.func.isRequired, onFinished: React.PropTypes.func.isRequired,
onNewDMClick: React.PropTypes.func.isRequired,
onExistingRoomSelected: React.PropTypes.func.isRequired,
}; };

View file

@ -15,25 +15,24 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import { getAddressType, inviteMultipleToRoom } from '../../../Invite'; import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
import createRoom from '../../../createRoom'; import createRoom from '../../../createRoom';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap'; import DMRoomMap from '../../../utils/DMRoomMap';
import rate_limited_func from '../../../ratelimitedfunc';
import dis from '../../../dispatcher';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import q from 'q'; import q from 'q';
import Fuse from 'fuse.js'; import dis from '../../../dispatcher';
const TRUNCATE_QUERY_LIST = 40; const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
module.exports = React.createClass({ module.exports = React.createClass({
displayName: "ChatInviteDialog", displayName: "ChatInviteDialog",
propTypes: { propTypes: {
title: React.PropTypes.string, title: React.PropTypes.string.isRequired,
description: React.PropTypes.oneOfType([ description: React.PropTypes.oneOfType([
React.PropTypes.element, React.PropTypes.element,
React.PropTypes.string, React.PropTypes.string,
@ -43,17 +42,13 @@ module.exports = React.createClass({
roomId: React.PropTypes.string, roomId: React.PropTypes.string,
button: React.PropTypes.string, button: React.PropTypes.string,
focus: React.PropTypes.bool, focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired onFinished: React.PropTypes.func.isRequired,
}, },
getDefaultProps: function() { getDefaultProps: function() {
return { return {
title: "Start a chat",
description: "Who would you like to communicate with?",
value: "", value: "",
placeholder: "Email, name or matrix ID", focus: true,
button: "Start Chat",
focus: true
}; };
}, },
@ -61,12 +56,20 @@ module.exports = React.createClass({
return { return {
error: false, error: false,
// List of AddressTile.InviteAddressType objects represeting // List of AddressTile.InviteAddressType objects representing
// the list of addresses we're going to invite // the list of addresses we're going to invite
inviteList: [], inviteList: [],
// List of AddressTile.InviteAddressType objects represeting // Whether a search is ongoing
// the set of autocompletion results for the current search busy: false,
// An error message generated during the user directory search
searchError: null,
// Whether the server supports the user_directory API
serverSupportsUserDirectory: true,
// The query being searched for
query: "",
// List of AddressTile.InviteAddressType objects representing
// the set of auto-completion results for the current search
// query. // query.
queryList: [], queryList: [],
}; };
@ -77,20 +80,6 @@ module.exports = React.createClass({
// Set the cursor at the end of the text input // Set the cursor at the end of the text input
this.refs.textinput.value = this.props.value; this.refs.textinput.value = this.props.value;
} }
// Create a Fuse instance for fuzzy searching this._userList
this._fuse = new Fuse(
// Use an empty list at first that will later be populated
// (see this._updateUserList)
[],
{
shouldSort: true,
location: 0, // The index of the query in the test string
distance: 5, // The distance away from location the query can be
// 0.0 = exact match, 1.0 = match anything
threshold: 0.3,
}
);
this._updateUserList();
}, },
onButtonClick: function() { onButtonClick: function() {
@ -112,17 +101,28 @@ module.exports = React.createClass({
// A Direct Message room already exists for this user, so select a // A Direct Message room already exists for this user, so select a
// room from a list that is similar to the one in MemberInfo panel // room from a list that is similar to the one in MemberInfo panel
const ChatCreateOrReuseDialog = sdk.getComponent( const ChatCreateOrReuseDialog = sdk.getComponent(
"views.dialogs.ChatCreateOrReuseDialog" "views.dialogs.ChatCreateOrReuseDialog",
); );
Modal.createDialog(ChatCreateOrReuseDialog, { const close = Modal.createDialog(ChatCreateOrReuseDialog, {
userId: userId, userId: userId,
onFinished: (success) => { onFinished: (success) => {
if (success) { this.props.onFinished(success);
this.props.onFinished(true, inviteList[0]); },
} onNewDMClick: () => {
// else show this ChatInviteDialog again dis.dispatch({
} action: 'start_chat',
user_id: userId,
}); });
close(true);
},
onExistingRoomSelected: (roomId) => {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
close(true);
},
}).close;
} else { } else {
this._startChat(inviteList); this._startChat(inviteList);
} }
@ -148,15 +148,15 @@ module.exports = React.createClass({
} else if (e.keyCode === 38) { // up arrow } else if (e.keyCode === 38) { // up arrow
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.addressSelector.moveSelectionUp(); if (this.addressSelector) this.addressSelector.moveSelectionUp();
} else if (e.keyCode === 40) { // down arrow } else if (e.keyCode === 40) { // down arrow
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.addressSelector.moveSelectionDown(); if (this.addressSelector) this.addressSelector.moveSelectionDown();
} else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab } else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.addressSelector.chooseSelection(); if (this.addressSelector) this.addressSelector.chooseSelection();
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace } else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -179,71 +179,36 @@ module.exports = React.createClass({
onQueryChanged: function(ev) { onQueryChanged: function(ev) {
const query = ev.target.value; const query = ev.target.value;
let queryList = [];
if (query.length < 2) {
return;
}
if (this.queryChangedDebouncer) { if (this.queryChangedDebouncer) {
clearTimeout(this.queryChangedDebouncer); clearTimeout(this.queryChangedDebouncer);
} }
this.queryChangedDebouncer = setTimeout(() => {
// Only do search if there is something to search // Only do search if there is something to search
if (query.length > 0 && query != '@') { if (query.length > 0 && query != '@' && query.length >= 2) {
// Weighted keys prefer to match userIds when first char is @ this.queryChangedDebouncer = setTimeout(() => {
this._fuse.options.keys = [{ if (this.state.serverSupportsUserDirectory) {
name: 'displayName', this._doUserDirectorySearch(query);
weight: query[0] === '@' ? 0.1 : 0.9, } else {
},{ this._doLocalSearch(query);
name: 'userId',
weight: query[0] === '@' ? 0.9 : 0.1,
}];
queryList = this._fuse.search(query).map((user) => {
// Return objects, structure of which is defined
// by InviteAddressType
return {
addressType: 'mx',
address: user.userId,
displayName: user.displayName,
avatarMxc: user.avatarUrl,
isKnown: true,
}
});
// If the query is a valid address, add an entry for that
// This is important, otherwise there's no way to invite
// a perfectly valid address if there are close matches.
const addrType = getAddressType(query);
if (addrType !== null) {
queryList.unshift({
addressType: addrType,
address: query,
isKnown: false,
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (addrType == 'email') {
this._lookupThreepid(addrType, query).done();
}
}
} }
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
} else {
this.setState({ this.setState({
queryList: queryList, queryList: [],
error: false, query: "",
}, () => { searchError: null,
this.addressSelector.moveSelectionTop();
}); });
}, 200); }
}, },
onDismissed: function(index) { onDismissed: function(index) {
var self = this; var self = this;
return function() { return () => {
var inviteList = self.state.inviteList.slice(); var inviteList = self.state.inviteList.slice();
inviteList.splice(index, 1); inviteList.splice(index, 1);
self.setState({ self.setState({
inviteList: inviteList, inviteList: inviteList,
queryList: [], queryList: [],
query: "",
}); });
if (this._cancelThreepidLookup) this._cancelThreepidLookup(); if (this._cancelThreepidLookup) this._cancelThreepidLookup();
}; };
@ -262,10 +227,109 @@ module.exports = React.createClass({
this.setState({ this.setState({
inviteList: inviteList, inviteList: inviteList,
queryList: [], queryList: [],
query: "",
}); });
if (this._cancelThreepidLookup) this._cancelThreepidLookup(); if (this._cancelThreepidLookup) this._cancelThreepidLookup();
}, },
_doUserDirectorySearch: function(query) {
this.setState({
busy: true,
query,
searchError: null,
});
MatrixClientPeg.get().searchUserDirectory({
term: query,
}).then((resp) => {
// The query might have changed since we sent the request, so ignore
// responses for anything other than the latest query.
if (this.state.query !== query) {
return;
}
this._processResults(resp.results, query);
}).catch((err) => {
console.error('Error whilst searching user directory: ', err);
this.setState({
searchError: err.errcode ? err.message : _t('Something went wrong!'),
});
if (err.errcode === 'M_UNRECOGNIZED') {
this.setState({
serverSupportsUserDirectory: false,
});
// Do a local search immediately
this._doLocalSearch(query);
}
}).done(() => {
this.setState({
busy: false,
});
});
},
_doLocalSearch: function(query) {
this.setState({
query,
searchError: null,
});
const queryLowercase = query.toLowerCase();
const results = [];
MatrixClientPeg.get().getUsers().forEach((user) => {
if (user.userId.toLowerCase().indexOf(queryLowercase) === -1 &&
user.displayName.toLowerCase().indexOf(queryLowercase) === -1
) {
return;
}
// Put results in the format of the new API
results.push({
user_id: user.userId,
display_name: user.displayName,
avatar_url: user.avatarUrl,
});
});
this._processResults(results, query);
},
_processResults: function(results, query) {
const queryList = [];
results.forEach((user) => {
if (user.user_id === MatrixClientPeg.get().credentials.userId) {
return;
}
// Return objects, structure of which is defined
// by InviteAddressType
queryList.push({
addressType: 'mx',
address: user.user_id,
displayName: user.display_name,
avatarMxc: user.avatar_url,
isKnown: true,
});
});
// If the query is a valid address, add an entry for that
// This is important, otherwise there's no way to invite
// a perfectly valid address if there are close matches.
const addrType = getAddressType(query);
if (addrType !== null) {
queryList.unshift({
addressType: addrType,
address: query,
isKnown: false,
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (addrType == 'email') {
this._lookupThreepid(addrType, query).done();
}
}
this.setState({
queryList,
error: false,
}, () => {
if (this.addressSelector) this.addressSelector.moveSelectionTop();
});
},
_getDirectMessageRooms: function(addr) { _getDirectMessageRooms: function(addr) {
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr); const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
@ -284,11 +348,7 @@ module.exports = React.createClass({
_startChat: function(addrs) { _startChat: function(addrs) {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); dis.dispatch({action: 'view_set_mxid'});
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't invite users. Please register."
});
return; return;
} }
@ -308,8 +368,8 @@ module.exports = React.createClass({
console.error(err.stack); console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to invite", title: _t("Failed to invite"),
description: ((err && err.message) ? err.message : "Operation failed"), description: ((err && err.message) ? err.message : _t("Operation failed")),
}); });
return null; return null;
}) })
@ -321,8 +381,8 @@ module.exports = React.createClass({
console.error(err.stack); console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to invite user", title: _t("Failed to invite user"),
description: ((err && err.message) ? err.message : "Operation failed"), description: ((err && err.message) ? err.message : _t("Operation failed")),
}); });
return null; return null;
}) })
@ -342,8 +402,8 @@ module.exports = React.createClass({
console.error(err.stack); console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to invite", title: _t("Failed to invite"),
description: ((err && err.message) ? err.message : "Operation failed"), description: ((err && err.message) ? err.message : _t("Operation failed")),
}); });
return null; return null;
}) })
@ -354,18 +414,6 @@ module.exports = React.createClass({
this.props.onFinished(true, addrTexts); this.props.onFinished(true, addrTexts);
}, },
_updateUserList: new rate_limited_func(function() {
// Get all the users
this._userList = MatrixClientPeg.get().getUsers();
// Remove current user
const meIx = this._userList.findIndex((u) => {
return u.userId === MatrixClientPeg.get().credentials.userId;
});
this._userList.splice(meIx, 1);
this._fuse.set(this._userList);
}, 500),
_isOnInviteList: function(uid) { _isOnInviteList: function(uid) {
for (let i = 0; i < this.state.inviteList.length; i++) { for (let i = 0; i < this.state.inviteList.length; i++) {
if ( if (
@ -401,7 +449,7 @@ module.exports = React.createClass({
if (errorList.length > 0) { if (errorList.length > 0) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to invite the following users to the " + room.name + " room:", title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
description: errorList.join(", "), description: errorList.join(", "),
}); });
} }
@ -433,6 +481,7 @@ module.exports = React.createClass({
this.setState({ this.setState({
inviteList: inviteList, inviteList: inviteList,
queryList: [], queryList: [],
query: "",
}); });
if (this._cancelThreepidLookup) this._cancelThreepidLookup(); if (this._cancelThreepidLookup) this._cancelThreepidLookup();
return inviteList; return inviteList;
@ -468,7 +517,7 @@ module.exports = React.createClass({
displayName: res.displayname, displayName: res.displayname,
avatarMxc: res.avatar_url, avatarMxc: res.avatar_url,
isKnown: true, isKnown: true,
}] }],
}); });
}); });
}, },
@ -500,23 +549,27 @@ module.exports = React.createClass({
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
defaultValue={this.props.value} defaultValue={this.props.value}
autoFocus={this.props.focus}> autoFocus={this.props.focus}>
</textarea> </textarea>,
); );
var error; let error;
var addressSelector; let addressSelector;
if (this.state.error) { if (this.state.error) {
error = <div className="mx_ChatInviteDialog_error">You have entered an invalid contact. Try using their Matrix ID or email address.</div>; error = <div className="mx_ChatInviteDialog_error">{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}</div>;
} else if (this.state.searchError) {
error = <div className="mx_ChatInviteDialog_error">{this.state.searchError}</div>;
} else if (
this.state.query.length > 0 &&
this.state.queryList.length === 0 &&
!this.state.busy
) {
error = <div className="mx_ChatInviteDialog_error">{_t("No results")}</div>;
} else { } else {
const addressSelectorHeader = <div className="mx_ChatInviteDialog_addressSelectHeader">
Searching known users
</div>;
addressSelector = ( addressSelector = (
<AddressSelector ref={(ref) => {this.addressSelector = ref;}} <AddressSelector ref={(ref) => {this.addressSelector = ref;}}
addressList={ this.state.queryList } addressList={ this.state.queryList }
onSelected={ this.onSelected } onSelected={ this.onSelected }
truncateAt={ TRUNCATE_QUERY_LIST } truncateAt={ TRUNCATE_QUERY_LIST }
header={ addressSelectorHeader }
/> />
); );
} }

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import classnames from 'classnames'; import classnames from 'classnames';
import { _t } from '../../../languageHandler';
/* /*
* A dialog for confirming a redaction. * A dialog for confirming a redaction.
@ -42,7 +43,7 @@ export default React.createClass({
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const title = "Confirm Redaction"; const title = _t("Confirm Removal");
const confirmButtonClass = classnames({ const confirmButtonClass = classnames({
'mx_Dialog_primary': true, 'mx_Dialog_primary': true,
@ -55,16 +56,16 @@ export default React.createClass({
title={title} title={title}
> >
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
Are you sure you wish to redact (delete) this event? {_t("Are you sure you wish to remove (delete) this event? " +
Note that if you redact a room name or topic change, it could undo the change. "Note that if you delete a room name or topic change, it could undo the change.")}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className={confirmButtonClass} onClick={this.onOk}> <button className={confirmButtonClass} onClick={this.onOk}>
Redact {_t("Remove")}
</button> </button>
<button onClick={this.onCancel}> <button onClick={this.onCancel}>
Cancel {_t("Cancel")}
</button> </button>
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classnames from 'classnames'; import classnames from 'classnames';
/* /*
@ -69,7 +70,7 @@ export default React.createClass({
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const title = this.props.action + " this person?"; const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
const confirmButtonClass = classnames({ const confirmButtonClass = classnames({
'mx_Dialog_primary': true, 'mx_Dialog_primary': true,
'danger': this.props.danger, 'danger': this.props.danger,
@ -82,7 +83,7 @@ export default React.createClass({
<form onSubmit={this.onOk}> <form onSubmit={this.onOk}>
<input className="mx_ConfirmUserActionDialog_reasonField" <input className="mx_ConfirmUserActionDialog_reasonField"
ref={this._collectReasonField} ref={this._collectReasonField}
placeholder="Reason" placeholder={ _t("Reason") }
autoFocus={true} autoFocus={true}
/> />
</form> </form>
@ -111,7 +112,7 @@ export default React.createClass({
</button> </button>
<button onClick={this.onCancel}> <button onClick={this.onCancel}>
Cancel { _t("Cancel") }
</button> </button>
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -20,6 +20,7 @@ import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import * as Lifecycle from '../../../Lifecycle'; import * as Lifecycle from '../../../Lifecycle';
import Velocity from 'velocity-vector'; import Velocity from 'velocity-vector';
import { _t } from '../../../languageHandler';
export default class DeactivateAccountDialog extends React.Component { export default class DeactivateAccountDialog extends React.Component {
constructor(props, context) { constructor(props, context) {
@ -56,10 +57,10 @@ export default class DeactivateAccountDialog extends React.Component {
Lifecycle.onLoggedOut(); Lifecycle.onLoggedOut();
this.props.onFinished(false); this.props.onFinished(false);
}, (err) => { }, (err) => {
let errStr = 'Unknown error'; let errStr = _t('Unknown error');
// https://matrix.org/jira/browse/SYN-744 // https://matrix.org/jira/browse/SYN-744
if (err.httpStatus == 401 || err.httpStatus == 403) { if (err.httpStatus == 401 || err.httpStatus == 403) {
errStr = 'Incorrect password'; errStr = _t('Incorrect password');
Velocity(this._passwordField, "callout.shake", 300); Velocity(this._passwordField, "callout.shake", 300);
} }
this.setState({ this.setState({
@ -85,29 +86,29 @@ export default class DeactivateAccountDialog extends React.Component {
passwordBoxClass = 'error'; passwordBoxClass = 'error';
} }
const okLabel = this.state.busy ? <Loader /> : 'Deactivate Account'; const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
const okEnabled = this.state.confirmButtonEnabled && !this.state.busy; const okEnabled = this.state.confirmButtonEnabled && !this.state.busy;
let cancelButton = null; let cancelButton = null;
if (!this.state.busy) { if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel} autoFocus={true}> cancelButton = <button onClick={this._onCancel} autoFocus={true}>
Cancel {_t("Cancel")}
</button>; </button>;
} }
return ( return (
<div className="mx_DeactivateAccountDialog"> <div className="mx_DeactivateAccountDialog">
<div className="mx_Dialog_title danger"> <div className="mx_Dialog_title danger">
Deactivate Account {_t("Deactivate Account")}
</div> </div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<p>This will make your account permanently unusable. You will not be able to re-register the same user ID.</p> <p>{_t("This will make your account permanently unusable. You will not be able to re-register the same user ID.")}</p>
<p>This action is irreversible.</p> <p>{_t("This action is irreversible.")}</p>
<p>To continue, please enter your password.</p> <p>{_t("To continue, please enter your password.")}</p>
<p>Password:</p> <p>{_t("Password")}:</p>
<input <input
type="password" type="password"
onChange={this._onPasswordFieldChange} onChange={this._onPasswordFieldChange}

View file

@ -0,0 +1,77 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
export default function DeviceVerifyDialog(props) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const key = FormattingUtils.formatCryptoKey(props.device.getFingerprint());
const body = (
<div>
<p>
{_t("To verify that this device can be trusted, please contact its " +
"owner using some other means (e.g. in person or a phone call) " +
"and ask them whether the key they see in their User Settings " +
"for this device matches the key below:")}
</p>
<div className="mx_UserSettings_cryptoSection">
<ul>
<li><label>{_t("Device name")}:</label> <span>{ props.device.getDisplayName() }</span></li>
<li><label>{_t("Device ID")}:</label> <span><code>{ props.device.deviceId}</code></span></li>
<li><label>{_t("Device key")}:</label> <span><code><b>{ key }</b></code></span></li>
</ul>
</div>
<p>
{_t("If it matches, press the verify button below. " +
"If it doesn't, then someone else is intercepting this device " +
"and you probably want to press the blacklist button instead.")}
</p>
<p>
{_t("In future this verification process will be more sophisticated.")}
</p>
</div>
);
function onFinished(confirm) {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
props.userId, props.device.deviceId, true,
);
}
props.onFinished(confirm);
}
return (
<QuestionDialog
title={_t("Verify device")}
description={body}
button={_t("I verify that the keys match")}
onFinished={onFinished}
/>
);
}
DeviceVerifyDialog.propTypes = {
userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func.isRequired,
};

View file

@ -27,6 +27,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
displayName: 'ErrorDialog', displayName: 'ErrorDialog',
@ -43,10 +44,10 @@ export default React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
title: "Error",
description: "An error has occurred.",
button: "OK",
focus: true, focus: true,
title: null,
description: null,
button: null,
}; };
}, },
@ -60,13 +61,13 @@ export default React.createClass({
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={this.props.title}> title={this.props.title || _t('Error')}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
{this.props.description} {this.props.description || _t('An error has occurred.')}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}> <button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
{this.props.button} {this.props.button || _t('OK')}
</button> </button>
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -15,11 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import Matrix from 'matrix-js-sdk';
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
@ -46,12 +45,6 @@ export default React.createClass({
title: React.PropTypes.string, title: React.PropTypes.string,
}, },
getDefaultProps: function() {
return {
title: "Authentication",
};
},
getInitialState: function() { getInitialState: function() {
return { return {
authError: null, authError: null,
@ -85,7 +78,7 @@ export default React.createClass({
<AccessibleButton onClick={this._onDismissClick} <AccessibleButton onClick={this._onDismissClick}
className="mx_UserSettings_button" className="mx_UserSettings_button"
> >
Dismiss {_t("Dismiss")}
</AccessibleButton> </AccessibleButton>
</div> </div>
); );
@ -105,7 +98,7 @@ export default React.createClass({
return ( return (
<BaseDialog className="mx_InteractiveAuthDialog" <BaseDialog className="mx_InteractiveAuthDialog"
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={this.state.authError ? 'Error' : this.props.title} title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
> >
{content} {content}
</BaseDialog> </BaseDialog>

View file

@ -0,0 +1,172 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import Modal from '../../../Modal';
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
/**
* Dialog which asks the user whether they want to share their keys with
* an unverified device.
*
* onFinished is called with `true` if the key should be shared, `false` if it
* should not, and `undefined` if the dialog is cancelled. (In other words:
* truthy: do the key share. falsy: don't share the keys).
*/
export default React.createClass({
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
userId: React.PropTypes.string.isRequired,
deviceId: React.PropTypes.string.isRequired,
onFinished: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
deviceInfo: null,
wasNewDevice: false,
};
},
componentDidMount: function() {
this._unmounted = false;
const userId = this.props.userId;
const deviceId = this.props.deviceId;
// give the client a chance to refresh the device list
this.props.matrixClient.downloadKeys([userId], false).then((r) => {
if (this._unmounted) { return; }
const deviceInfo = r[userId][deviceId];
if(!deviceInfo) {
console.warn(`No details found for device ${userId}:${deviceId}`);
this.props.onFinished(false);
return;
}
const wasNewDevice = !deviceInfo.isKnown();
this.setState({
deviceInfo: deviceInfo,
wasNewDevice: wasNewDevice,
});
// if the device was new before, it's not any more.
if (wasNewDevice) {
this.props.matrixClient.setDeviceKnown(
userId,
deviceId,
true,
);
}
}).done();
},
componentWillUnmount: function() {
this._unmounted = true;
},
_onVerifyClicked: function() {
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
console.log("KeyShareDialog: Starting verify dialog");
Modal.createDialog(DeviceVerifyDialog, {
userId: this.props.userId,
device: this.state.deviceInfo,
onFinished: (verified) => {
if (verified) {
// can automatically share the keys now.
this.props.onFinished(true);
}
},
});
},
_onShareClicked: function() {
console.log("KeyShareDialog: User clicked 'share'");
this.props.onFinished(true);
},
_onIgnoreClicked: function() {
console.log("KeyShareDialog: User clicked 'ignore'");
this.props.onFinished(false);
},
_renderContent: function() {
const displayName = this.state.deviceInfo.getDisplayName() ||
this.state.deviceInfo.deviceId;
let text;
if (this.state.wasNewDevice) {
text = "You added a new device '%(displayName)s', which is"
+ " requesting encryption keys.";
} else {
text = "Your unverified device '%(displayName)s' is requesting"
+ " encryption keys.";
}
text = _t(text, {displayName: displayName});
return (
<div>
<p>{text}</p>
<div className="mx_Dialog_buttons">
<button onClick={this._onVerifyClicked}>
{_t('Start verification')}
</button>
<button onClick={this._onShareClicked}>
{_t('Share without verifying')}
</button>
<button onClick={this._onIgnoreClicked}>
{_t('Ignore request')}
</button>
</div>
</div>
);
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('views.elements.Spinner');
let content;
if (this.state.deviceInfo) {
content = this._renderContent();
} else {
content = (
<div>
<p>{_t('Loading device info...')}</p>
<Spinner />
</div>
);
}
return (
<BaseDialog className='mx_KeyShareRequestDialog'
onFinished={this.props.onFinished}
title={_t('Encryption key request')}
>
{content}
</BaseDialog>
);
},
});

View file

@ -1,78 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
/*
* Usage:
* Modal.createDialog(NeedToRegisterDialog, {
* title: "some text", (default: "Registration required")
* description: "some more text",
* onFinished: someFunction,
* });
*/
import React from 'react';
import dis from '../../../dispatcher';
import sdk from '../../../index';
module.exports = React.createClass({
displayName: 'NeedToRegisterDialog',
propTypes: {
title: React.PropTypes.string,
description: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.string,
]),
onFinished: React.PropTypes.func.isRequired,
},
getDefaultProps: function() {
return {
title: "Registration required",
description: "A registered account is required for this action",
};
},
onRegisterClicked: function() {
dis.dispatch({
action: "start_upgrade_registration",
});
if (this.props.onFinished) {
this.props.onFinished();
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_NeedToRegisterDialog"
onFinished={this.props.onFinished}
title={this.props.title}
>
<div className="mx_Dialog_content">
{this.props.description}
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}>
Cancel
</button>
<button onClick={this.onRegisterClicked}>
Register
</button>
</div>
</BaseDialog>
);
},
});

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
displayName: 'QuestionDialog', displayName: 'QuestionDialog',
@ -33,7 +34,6 @@ export default React.createClass({
title: "", title: "",
description: "", description: "",
extraButtons: null, extraButtons: null,
button: "OK",
focus: true, focus: true,
hasCancelButton: true, hasCancelButton: true,
}; };
@ -47,17 +47,11 @@ export default React.createClass({
this.props.onFinished(false); this.props.onFinished(false);
}, },
componentDidMount: function() {
if (this.props.focus) {
this.refs.button.focus();
}
},
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const cancelButton = this.props.hasCancelButton ? ( const cancelButton = this.props.hasCancelButton ? (
<button onClick={this.onCancel}> <button onClick={this.onCancel}>
Cancel {_t("Cancel")}
</button> </button>
) : null; ) : null;
return ( return (
@ -69,8 +63,8 @@ export default React.createClass({
{this.props.description} {this.props.description}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button ref="button" className="mx_Dialog_primary" onClick={this.onOk}> <button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
{this.props.button} {this.props.button || _t('OK')}
</button> </button>
{this.props.extraButtons} {this.props.extraButtons}
{cancelButton} {cancelButton}

View file

@ -18,6 +18,7 @@ import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t, _tJsx } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
@ -43,29 +44,32 @@ export default React.createClass({
if (SdkConfig.get().bug_report_endpoint_url) { if (SdkConfig.get().bug_report_endpoint_url) {
bugreport = ( bugreport = (
<p>Otherwise, <a onClick={this._sendBugReport} href='#'> <p>
click here</a> to send a bug report. {_tJsx(
"Otherwise, <a>click here</a> to send a bug report.",
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{sub}</a>,
)}
</p> </p>
); );
} }
return ( return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title='Unable to restore session'> title={_t('Unable to restore session')}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<p>We encountered an error trying to restore your previous session. If <p>{_t("We encountered an error trying to restore your previous session. If " +
you continue, you will need to log in again, and encrypted chat "you continue, you will need to log in again, and encrypted chat " +
history will be unreadable.</p> "history will be unreadable.")}</p>
<p>If you have previously used a more recent version of Riot, your session <p>{_t("If you have previously used a more recent version of Riot, your session " +
may be incompatible with this version. Close this window and return "may be incompatible with this version. Close this window and return " +
to the more recent version.</p> "to the more recent version.")}</p>
{bugreport} {bugreport}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this._continueClicked}> <button className="mx_Dialog_primary" onClick={this._continueClicked}>
Continue anyway {_t("Continue anyway")}
</button> </button>
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -1,87 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
/**
* Prompt the user to set a display name.
*
* On success, `onFinished(true, newDisplayName)` is called.
*/
export default React.createClass({
displayName: 'SetDisplayNameDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
currentDisplayName: React.PropTypes.string,
},
getInitialState: function() {
if (this.props.currentDisplayName) {
return { value: this.props.currentDisplayName };
}
if (MatrixClientPeg.get().isGuest()) {
return { value : "Guest " + MatrixClientPeg.get().getUserIdLocalpart() };
}
else {
return { value : MatrixClientPeg.get().getUserIdLocalpart() };
}
},
componentDidMount: function() {
this.refs.input_value.select();
},
onValueChange: function(ev) {
this.setState({
value: ev.target.value
});
},
onFormSubmit: function(ev) {
ev.preventDefault();
this.props.onFinished(true, this.state.value);
return false;
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_SetDisplayNameDialog"
onFinished={this.props.onFinished}
title="Set a Display Name"
>
<div className="mx_Dialog_content">
Your display name is how you'll appear to others when you speak in rooms.<br/>
What would you like it to be?
</div>
<form onSubmit={this.onFormSubmit}>
<div className="mx_Dialog_content">
<input type="text" ref="input_value" value={this.state.value}
autoFocus={true} onChange={this.onValueChange} size="30"
className="mx_SetDisplayNameDialog_input"
/>
</div>
<div className="mx_Dialog_buttons">
<input className="mx_Dialog_primary" type="submit" value="Set" />
</div>
</form>
</BaseDialog>
);
},
});

View file

@ -0,0 +1,164 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import React from 'react';
import sdk from '../../../index';
import Email from '../../../email';
import AddThreepid from '../../../AddThreepid';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
/**
* Prompt the user to set an email address.
*
* On success, `onFinished(true)` is called.
*/
export default React.createClass({
displayName: 'SetEmailDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
emailAddress: null,
emailBusy: false,
};
},
componentDidMount: function() {
},
onEmailAddressChanged: function(value) {
this.setState({
emailAddress: value,
});
},
onSubmit: function() {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const emailAddress = this.state.emailAddress;
if (!Email.looksValid(emailAddress)) {
Modal.createDialog(ErrorDialog, {
title: _t("Invalid Email Address"),
description: _t("This doesn't appear to be a valid email address"),
});
return;
}
this._addThreepid = new AddThreepid();
// we always bind emails when registering, so let's do the
// same here.
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
Modal.createDialog(QuestionDialog, {
title: _t("Verification Pending"),
description: _t(
"Please check your email and click on the link it contains. Once this " +
"is done, click continue.",
),
button: _t('Continue'),
onFinished: this.onEmailDialogFinished,
});
}, (err) => {
this.setState({emailBusy: false});
console.error("Unable to add email address " + emailAddress + " " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to add email address"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
this.setState({emailBusy: true});
},
onCancelled: function() {
this.props.onFinished(false);
},
onEmailDialogFinished: function(ok) {
if (ok) {
this.verifyEmailAddress();
} else {
this.setState({emailBusy: false});
}
},
verifyEmailAddress: function() {
this._addThreepid.checkEmailLinkClicked().done(() => {
this.props.onFinished(true);
}, (err) => {
this.setState({emailBusy: false});
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const message = _t("Unable to verify email address.") + " " +
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
Modal.createDialog(QuestionDialog, {
title: _t("Verification Pending"),
description: message,
button: _t('Continue'),
onFinished: this.onEmailDialogFinished,
});
} else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify email address: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to verify email address."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
});
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('elements.Spinner');
const EditableText = sdk.getComponent('elements.EditableText');
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
className="mx_SetEmailDialog_email_input"
placeholder={ _t("Email address") }
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
blurToCancel={ false }
onValueChanged={ this.onEmailAddressChanged } />;
return (
<BaseDialog className="mx_SetEmailDialog"
onFinished={this.onCancelled}
title={this.props.title}
>
<div className="mx_Dialog_content">
<p>
{ _t('This will allow you to reset your password and receive notifications.') }
</p>
{ emailInput }
</div>
<div className="mx_Dialog_buttons">
<input className="mx_Dialog_primary"
type="submit"
value={_t("Continue")}
onClick={this.onSubmit}
/>
<input
type="submit"
value={_t("Skip")}
onClick={this.onCancelled}
/>
</div>
</BaseDialog>
);
},
});

View file

@ -0,0 +1,294 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import q from 'q';
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames';
import KeyCode from '../../../KeyCode';
import { _t, _tJsx } from '../../../languageHandler';
// The amount of time to wait for further changes to the input username before
// sending a request to the server
const USERNAME_CHECK_DEBOUNCE_MS = 250;
/**
* Prompt the user to set a display name.
*
* On success, `onFinished(true, newDisplayName)` is called.
*/
export default React.createClass({
displayName: 'SetMxIdDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
// Called when the user requests to register with a different homeserver
onDifferentServerClicked: React.PropTypes.func.isRequired,
// Called if the user wants to switch to login instead
onLoginClick: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
// The entered username
username: '',
// Indicate ongoing work on the username
usernameBusy: false,
// Indicate error with username
usernameError: '',
// Assume the homeserver supports username checking until "M_UNRECOGNIZED"
usernameCheckSupport: true,
// Whether the auth UI is currently being used
doingUIAuth: false,
// Indicate error with auth
authError: '',
};
},
componentDidMount: function() {
this.refs.input_value.select();
this._matrixClient = MatrixClientPeg.get();
},
onValueChange: function(ev) {
this.setState({
username: ev.target.value,
usernameBusy: true,
usernameError: '',
}, () => {
if (!this.state.username || !this.state.usernameCheckSupport) {
this.setState({
usernameBusy: false,
});
return;
}
// Debounce the username check to limit number of requests sent
if (this._usernameCheckTimeout) {
clearTimeout(this._usernameCheckTimeout);
}
this._usernameCheckTimeout = setTimeout(() => {
this._doUsernameCheck().finally(() => {
this.setState({
usernameBusy: false,
});
});
}, USERNAME_CHECK_DEBOUNCE_MS);
});
},
onKeyUp: function(ev) {
if (ev.keyCode === KeyCode.ENTER) {
this.onSubmit();
}
},
onSubmit: function(ev) {
this.setState({
doingUIAuth: true,
});
},
_doUsernameCheck: function() {
// Check if username is available
return this._matrixClient.isUsernameAvailable(this.state.username).then(
(isAvailable) => {
if (isAvailable) {
this.setState({usernameError: ''});
}
},
(err) => {
// Indicate whether the homeserver supports username checking
const newState = {
usernameCheckSupport: err.errcode !== "M_UNRECOGNIZED",
};
console.error('Error whilst checking username availability: ', err);
switch (err.errcode) {
case "M_USER_IN_USE":
newState.usernameError = _t('Username not available');
break;
case "M_INVALID_USERNAME":
newState.usernameError = _t(
'Username invalid: %(errMessage)s',
{ errMessage: err.message},
);
break;
case "M_UNRECOGNIZED":
// This homeserver doesn't support username checking, assume it's
// fine and rely on the error appearing in registration step.
newState.usernameError = '';
break;
case undefined:
newState.usernameError = _t('Something went wrong!');
break;
default:
newState.usernameError = _t(
'An error occurred: %(error_string)s',
{ error_string: err.message },
);
break;
}
this.setState(newState);
},
);
},
_generatePassword: function() {
return Math.random().toString(36).slice(2);
},
_makeRegisterRequest: function(auth) {
// Not upgrading - changing mxids
const guestAccessToken = null;
if (!this._generatedPassword) {
this._generatedPassword = this._generatePassword();
}
return this._matrixClient.register(
this.state.username,
this._generatedPassword,
undefined, // session id: included in the auth dict already
auth,
{},
guestAccessToken,
);
},
_onUIAuthFinished: function(success, response) {
this.setState({
doingUIAuth: false,
});
if (!success) {
this.setState({ authError: response.message });
return;
}
// XXX Implement RTS /register here
const teamToken = null;
this.props.onFinished(true, {
userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: this._matrixClient.getHomeserverUrl(),
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token,
password: this._generatedPassword,
teamToken: teamToken,
});
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
const Spinner = sdk.getComponent('elements.Spinner');
let auth;
if (this.state.doingUIAuth) {
auth = <InteractiveAuth
matrixClient={this._matrixClient}
makeRequest={this._makeRegisterRequest}
onAuthFinished={this._onUIAuthFinished}
inputs={{}}
poll={true}
/>;
}
const inputClasses = classnames({
"mx_SetMxIdDialog_input": true,
"error": Boolean(this.state.usernameError),
});
let usernameIndicator = null;
let usernameBusyIndicator = null;
if (this.state.usernameBusy) {
usernameBusyIndicator = <Spinner w="24" h="24"/>;
} else {
const usernameAvailable = this.state.username &&
this.state.usernameCheckSupport && !this.state.usernameError;
const usernameIndicatorClasses = classnames({
"error": Boolean(this.state.usernameError),
"success": usernameAvailable,
});
usernameIndicator = <div className={usernameIndicatorClasses}>
{ usernameAvailable ? _t('Username available') : this.state.usernameError }
</div>;
}
let authErrorIndicator = null;
if (this.state.authError) {
authErrorIndicator = <div className="error">
{ this.state.authError }
</div>;
}
const canContinue = this.state.username &&
!this.state.usernameError &&
!this.state.usernameBusy;
return (
<BaseDialog className="mx_SetMxIdDialog"
onFinished={this.props.onFinished}
title="To get started, please pick a username!"
>
<div className="mx_Dialog_content">
<div className="mx_SetMxIdDialog_input_group">
<input type="text" ref="input_value" value={this.state.username}
autoFocus={true}
onChange={this.onValueChange}
onKeyUp={this.onKeyUp}
size="30"
className={inputClasses}
/>
{ usernameBusyIndicator }
</div>
{ usernameIndicator }
<p>
{ _tJsx(
'This will be your account name on the <span></span> ' +
'homeserver, or you can pick a <a>different server</a>.',
[
/<span><\/span>/,
/<a>(.*?)<\/a>/,
],
[
(sub) => <span>{this.props.homeserverUrl}</span>,
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{sub}</a>,
],
)}
</p>
<p>
{ _tJsx(
'If you already have a Matrix account you can <a>log in</a> instead.',
/<a>(.*?)<\/a>/,
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{sub}</a>],
)}
</p>
{ auth }
{ authErrorIndicator }
</div>
<div className="mx_Dialog_buttons">
<input className="mx_Dialog_primary"
type="submit"
value={_t("Continue")}
onClick={this.onSubmit}
disabled={!canContinue}
/>
</div>
</BaseDialog>
);
},
});

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
displayName: 'TextInputDialog', displayName: 'TextInputDialog',
@ -36,7 +37,6 @@ export default React.createClass({
title: "", title: "",
value: "", value: "",
description: "", description: "",
button: "OK",
focus: true, focus: true,
}; };
}, },
@ -73,7 +73,7 @@ export default React.createClass({
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.onCancel}> <button onClick={this.onCancel}>
Cancel { _t("Cancel") }
</button> </button>
<button className="mx_Dialog_primary" onClick={this.onOk}> <button className="mx_Dialog_primary" onClick={this.onOk}>
{this.props.button} {this.props.button}

View file

@ -16,10 +16,10 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import Resend from '../../../Resend'; import Resend from '../../../Resend';
import { _t } from '../../../languageHandler';
function DeviceListEntry(props) { function DeviceListEntry(props) {
const {userId, device} = props; const {userId, device} = props;
@ -120,17 +120,17 @@ export default React.createClass({
if (blacklistUnverified) { if (blacklistUnverified) {
warning = ( warning = (
<h4> <h4>
You are currently blacklisting unverified devices; to send {_t("You are currently blacklisting unverified devices; to send " +
messages to these devices you must verify them. "messages to these devices you must verify them.")}
</h4> </h4>
); );
} else { } else {
warning = ( warning = (
<div> <div>
<p> <p>
We recommend you go through the verification process {_t("We recommend you go through the verification process " +
for each device to confirm they belong to their legitimate owner, "for each device to confirm they belong to their legitimate owner, " +
but you can resend the message without verifying if you prefer. "but you can resend the message without verifying if you prefer.")}
</p> </p>
</div> </div>
); );
@ -145,14 +145,14 @@ export default React.createClass({
console.log("UnknownDeviceDialog closed by escape"); console.log("UnknownDeviceDialog closed by escape");
this.props.onFinished(); this.props.onFinished();
}} }}
title='Room contains unknown devices' title={_t('Room contains unknown devices')}
> >
<GeminiScrollbar autoshow={false} className="mx_Dialog_content"> <GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<h4> <h4>
"{this.props.room.name}" contains devices that you haven't seen before. {_t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name})}
</h4> </h4>
{ warning } { warning }
Unknown devices: {_t("Unknown devices")}:
<UnknownDeviceList devices={this.props.devices} /> <UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar> </GeminiScrollbar>
@ -162,7 +162,7 @@ export default React.createClass({
this.props.onFinished(); this.props.onFinished();
Resend.resendUnsentEvents(this.props.room); Resend.resendUnsentEvents(this.props.room);
}}> }}>
Send anyway {_t("Send anyway")}
</button> </button>
<button className="mx_Dialog_primary" autoFocus={ true } <button className="mx_Dialog_primary" autoFocus={ true }
onClick={() => { onClick={() => {

View file

@ -27,6 +27,7 @@ export default React.createClass({
size: PropTypes.string, size: PropTypes.string,
tooltip: PropTypes.bool, tooltip: PropTypes.bool,
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
mouseOverAction: PropTypes.string,
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
iconPath: PropTypes.string.isRequired, iconPath: PropTypes.string.isRequired,
}, },
@ -51,6 +52,9 @@ export default React.createClass({
_onMouseEnter: function() { _onMouseEnter: function() {
if (this.props.tooltip) this.setState({showTooltip: true}); if (this.props.tooltip) this.setState({showTooltip: true});
if (this.props.mouseOverAction) {
dis.dispatch({action: this.props.mouseOverAction});
}
}, },
_onMouseLeave: function() { _onMouseLeave: function() {

View file

@ -16,12 +16,11 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); import React from 'react';
var classNames = require('classnames'); import classNames from 'classnames';
var sdk = require("../../../index"); import sdk from "../../../index";
var Invite = require("../../../Invite"); import MatrixClientPeg from "../../../MatrixClientPeg";
var MatrixClientPeg = require("../../../MatrixClientPeg"); import { _t } from '../../../languageHandler';
var Avatar = require('../../../Avatar');
// React PropType definition for an object describing // React PropType definition for an object describing
// an address that can be invited to a room (which // an address that can be invited to a room (which
@ -142,7 +141,7 @@ export default React.createClass({
}); });
info = ( info = (
<div className={unknownClasses}>Unknown Address</div> <div className={unknownClasses}>{_t("Unknown Address")}</div>
); );
} }

View file

@ -17,12 +17,14 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const CreateRoomButton = function(props) { const CreateRoomButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton'); const ActionButton = sdk.getComponent('elements.ActionButton');
return ( return (
<ActionButton action="view_create_chat" <ActionButton action="view_create_room"
label="Create new room" mouseOverAction={props.callout ? "callout_create_room" : null}
label={ _t("Create new room") }
iconPath="img/icons-create-room.svg" iconPath="img/icons-create-room.svg"
size={props.size} size={props.size}
tooltip={props.tooltip} tooltip={props.tooltip}

View file

@ -18,6 +18,7 @@ import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index'; import sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
displayName: 'DeviceVerifyButtons', displayName: 'DeviceVerifyButtons',
@ -50,42 +51,10 @@ export default React.createClass({
}, },
onVerifyClick: function() { onVerifyClick: function() {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
Modal.createDialog(QuestionDialog, { Modal.createDialog(DeviceVerifyDialog, {
title: "Verify device", userId: this.props.userId,
description: ( device: this.state.device,
<div>
<p>
To verify that this device can be trusted, please contact its
owner using some other means (e.g. in person or a phone call)
and ask them whether the key they see in their User Settings
for this device matches the key below:
</p>
<div className="mx_UserSettings_cryptoSection">
<ul>
<li><label>Device name:</label> <span>{ this.state.device.getDisplayName() }</span></li>
<li><label>Device ID:</label> <span><code>{ this.state.device.deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{ this.state.device.getFingerprint() }</b></code></span></li>
</ul>
</div>
<p>
If it matches, press the verify button below.
If it doesnt, then someone else is intercepting this device
and you probably want to press the blacklist button instead.
</p>
<p>
In future this verification process will be more sophisticated.
</p>
</div>
),
button: "I verify that the keys match",
onFinished: confirm=>{
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.state.device.deviceId, true
);
}
},
}); });
}, },
@ -114,14 +83,14 @@ export default React.createClass({
blacklistButton = ( blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
onClick={this.onUnblacklistClick}> onClick={this.onUnblacklistClick}>
Unblacklist {_t("Unblacklist")}
</button> </button>
); );
} else { } else {
blacklistButton = ( blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
onClick={this.onBlacklistClick}> onClick={this.onBlacklistClick}>
Blacklist {_t("Blacklist")}
</button> </button>
); );
} }
@ -130,14 +99,14 @@ export default React.createClass({
verifyButton = ( verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}> onClick={this.onUnverifyClick}>
Unverify {_t("Unverify")}
</button> </button>
); );
} else { } else {
verifyButton = ( verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
onClick={this.onVerifyClick}> onClick={this.onVerifyClick}>
Verify... {_t("Verify...")}
</button> </button>
); );
} }

Some files were not shown because too many files have changed in this diff Show more