Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into set_default_federate_by_settings

This commit is contained in:
Michael Telatynski 2017-08-02 12:15:00 +01:00
commit fd454b476a
No known key found for this signature in database
GPG key ID: 0435A1D4BBD34D64
268 changed files with 27696 additions and 5893 deletions

View file

@ -1,4 +1,4 @@
{
"presets": ["react", "es2015", "es2016"],
"plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-generator", "transform-runtime", "add-module-exports"]
"plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-bluebird", "transform-runtime", "add-module-exports"]
}

161
.eslintignore.errorfiles Normal file
View file

@ -0,0 +1,161 @@
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
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/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/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/ContentMessages.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/PlatformPeg.js
src/Presence.js
src/ratelimitedfunc.js
src/RichText.js
src/Roles.js
src/Rooms.js
src/ScalarAuthClient.js
src/ScalarMessaging.js
src/TabComplete.js
src/TabCompleteEntries.js
src/TextForEvent.js
src/Tinter.js
src/UiEffects.js
src/Unread.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

View file

@ -64,7 +64,7 @@ module.exports = {
// to JSX.
ignorePattern: '^\\s*<',
ignoreComments: true,
code: 90,
code: 120,
}],
"valid-jsdoc": ["warn"],
"new-cap": ["warn"],

6
.flowconfig Normal file
View file

@ -0,0 +1,6 @@
[include]
src/**/*.js
test/**/*.js
[ignore]
node_modules/

5
.gitignore vendored
View file

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

View file

@ -9,16 +9,24 @@ set -ev
RIOT_WEB_DIR=riot-web
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"
cd "$RIOT_WEB_DIR"
git checkout "$curbranch" || git checkout develop
mkdir node_modules
npm install
(cd node_modules/matrix-js-sdk && npm install)
# use the version of js-sdk we just used in the react-sdk tests
rm -r node_modules/matrix-js-sdk
ln -s "$REACT_SDK_DIR/node_modules/matrix-js-sdk" node_modules/matrix-js-sdk
# ... and, of course, the version of react-sdk we just built
rm -r node_modules/matrix-react-sdk
ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk

View file

@ -1,9 +1,17 @@
# we need trusty for the chrome addon
dist: trusty
# we don't need sudo, so can run in a container, which makes startup much
# quicker.
sudo: false
language: node_js
node_js:
- node # Latest stable version of nodejs.
addons:
chrome: stable
install:
- npm install
- (cd node_modules/matrix-js-sdk && npm install)
script:
- npm run test
- ./.travis-test-riot.sh
./scripts/travis.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)
===================================================================================================
[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
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
===============
@ -190,4 +194,3 @@ Alternative instructions:
* 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
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
}
```
- 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
are simple and closely related. If you put the next declaration on a new line,
treat yourself to another `var`:
```javascript
var key = "foo",
const key = "foo",
comparator = function(x, y) {
return x - y;
}; // Bad
var key = "foo";
var comparator = function(x, y) {
const key = "foo";
const comparator = function(x, y) {
return x - y;
}; // Good
var x = 0, y = 0; // Fine
let x = 0, y = 0; // Fine
var x = 0;
var y = 0; // Also fine
let x = 0;
let y = 0; // Also fine
```
- 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 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.

View file

@ -2,7 +2,6 @@
set -e
export KARMAFLAGS="--no-colors"
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 4
@ -16,11 +15,16 @@ npm install
(cd node_modules/matrix-js-sdk && npm install)
# run the mocha tests
npm run test
npm run test -- --no-colors
# run eslint
npm run lintall -- -f checkstyle -o eslint.xml || true
# re-run the linter, excluding any files known to have errors or warnings.
./node_modules/.bin/eslint --max-warnings 0 \
--ignore-path .eslintignore.errorfiles \
src test
# delete the old tarball, if it exists
rm -f matrix-react-sdk-*.tgz

View file

@ -55,11 +55,18 @@ module.exports = function (config) {
// some images to reduce noise from the tests
{pattern: 'test/img/*', watched: false, included: 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: {
// redirect img links to the karma server
"/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
@ -86,7 +93,18 @@ module.exports = function (config) {
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress', 'junit'],
reporters: ['logcapture', 'spec', 'junit', 'summary'],
specReporter: {
suppressErrorSummary: false, // do print error summary
suppressFailed: false, // do print information about failed tests
suppressPassed: false, // do print information about passed tests
showSpecTiming: true, // print the time elapsed for each spec
},
client: {
captureLogs: true,
},
// web server port
port: 9876,
@ -97,7 +115,10 @@ module.exports = function (config) {
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR ||
// config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
//
// This is strictly for logs that would be generated by the browser itself and we
// don't want to log about missing images, which are emitted on LOG_WARN.
logLevel: config.LOG_ERROR,
// enable / disable watching file and executing tests whenever any file
// changes
@ -109,11 +130,25 @@ module.exports = function (config) {
browsers: [
'Chrome',
//'PhantomJS',
//'ChromeHeadless',
],
customLaunchers: {
'ChromeHeadless': {
base: 'Chrome',
flags: [
// See https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md
'--headless',
'--disable-gpu',
// Without a remote debugging port, Google Chrome exits immediately.
'--remote-debugging-port=9222',
],
}
},
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// singleRun: false,
// Concurrency level
// how many browser should be started simultaneous
@ -135,17 +170,24 @@ module.exports = function (config) {
},
],
noParse: [
// for cross platform compatibility use [\\\/] as the path separator
// this ensures that the regex trips on both Windows and *nix
// don't parse the languages within highlight.js. They
// cause stack overflows
// (https://github.com/webpack/webpack/issues/1721), and
// there is no need for webpack to parse them - they can
// just be included as-is.
/highlight\.js\/lib\/languages/,
/highlight\.js[\\\/]lib[\\\/]languages/,
// olm takes ages for webpack to process, and it's already heavily
// optimised, so there is little to gain by us uglifying it.
/olm[\\\/](javascript[\\\/])?olm\.js$/,
// also disable parsing for sinon, because it
// tries to do voodoo with 'require' which upsets
// webpack (https://github.com/webpack/webpack/issues/304)
/sinon\/pkg\/sinon\.js$/,
/sinon[\\\/]pkg[\\\/]sinon\.js$/,
],
},
resolve: {
@ -159,11 +201,15 @@ module.exports = function (config) {
'sinon': 'sinon/pkg/sinon.js',
},
root: [
path.resolve('./src'),
path.resolve('./test'),
],
},
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: {

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "0.8.8",
"version": "0.9.7",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -31,45 +31,51 @@
"reskindex": "scripts/reskindex.js"
},
"scripts": {
"reskindex": "scripts/reskindex.js -h header",
"build": "node scripts/babelcheck.js && babel src -d lib --source-maps",
"start": "node scripts/babelcheck.js && babel src -w -d lib --source-maps",
"reskindex": "node scripts/reskindex.js -h header",
"reskindex:watch": "node scripts/reskindex.js -h header -w",
"build": "npm run reskindex && babel src -d lib --source-maps --copy-files",
"build:watch": "babel src -w -d lib --source-maps --copy-files",
"emoji-data-strip": "node scripts/emoji-data-strip.js",
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
"lint": "eslint src/",
"lintall": "eslint src/ test/",
"clean": "rimraf lib",
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt",
"test": "karma start $KARMAFLAGS --browsers PhantomJS",
"test-multi": "karma start $KARMAFLAGS --single-run=false"
"test": "karma start --single-run=true --browsers ChromeHeadless",
"test-multi": "karma start"
},
"dependencies": {
"babel-runtime": "^6.11.6",
"bluebird": "^3.5.0",
"blueimp-canvas-to-blob": "^3.5.0",
"browser-encrypt-attachment": "^0.3.0",
"browser-request": "^0.3.3",
"classnames": "^2.1.2",
"commonmark": "^0.27.0",
"draft-js": "^0.8.1",
"counterpart": "^0.18.0",
"draft-js": "^0.10.1",
"draft-js-export-html": "^0.5.0",
"draft-js-export-markdown": "^0.2.0",
"emojione": "2.2.3",
"emojione": "2.2.7",
"file-saver": "^1.3.3",
"filesize": "^3.1.2",
"flux": "^2.0.3",
"filesize": "3.5.6",
"flux": "2.1.1",
"fuse.js": "^2.2.0",
"glob": "^5.0.14",
"highlight.js": "^8.9.1",
"isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.3",
"lodash": "^4.13.1",
"matrix-js-sdk": "0.7.7",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"optimist": "^0.6.1",
"q": "^1.4.1",
"prop-types": "^15.5.8",
"react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2",
"react-dom": "^15.4.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"sanitize-html": "^1.11.1",
"sanitize-html": "^1.14.1",
"text-encoding-utf-8": "^1.0.1",
"url": "^0.11.0",
"velocity-vector": "vector-im/velocity#059e3b2",
"whatwg-fetch": "^1.0.0"
},
@ -79,7 +85,7 @@
"babel-eslint": "^6.1.2",
"babel-loader": "^6.2.5",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-async-to-generator": "^6.16.0",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
"babel-plugin-transform-class-properties": "^6.16.0",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-plugin-transform-runtime": "^6.15.0",
@ -88,6 +94,7 @@
"babel-preset-es2016": "^6.11.3",
"babel-preset-es2017": "^6.14.0",
"babel-preset-react": "^6.11.1",
"chokidar": "^1.6.1",
"eslint": "^3.13.1",
"eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^4.0.1",
@ -95,16 +102,19 @@
"eslint-plugin-react": "^6.9.0",
"expect": "^1.16.0",
"json-loader": "^0.5.3",
"karma": "^0.13.22",
"karma": "^1.7.0",
"karma-chrome-launcher": "^0.2.3",
"karma-cli": "^0.1.2",
"karma-junit-reporter": "^0.4.1",
"karma-logcapture-reporter": "0.0.1",
"karma-mocha": "^0.2.2",
"karma-phantomjs-launcher": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "^0.0.31",
"karma-summary-reporter": "^1.3.3",
"karma-webpack": "^1.7.0",
"matrix-react-test-utils": "^0.1.1",
"mocha": "^2.4.5",
"phantomjs-prebuilt": "^2.1.7",
"parallelshell": "^1.2.0",
"react-addons-test-utils": "^15.4.0",
"require-json": "0.0.1",
"rimraf": "^2.4.3",

View file

@ -1,22 +0,0 @@
#!/usr/bin/env node
var exec = require('child_process').exec;
// Makes sure the babel executable in the path is babel 6 (or greater), not
// babel 5, which it is if you upgrade from an older version of react-sdk and
// run 'npm install' since the package has changed to babel-cli, so 'babel'
// remains installed and the executable in node_modules/.bin remains as babel
// 5.
exec("babel -V", function (error, stdout, stderr) {
if ((error && error.code) || parseInt(stdout.substr(0,1), 10) < 6) {
console.log("\033[31m\033[1m"+
'*****************************************\n'+
'* matrix-react-sdk has moved to babel 6 *\n'+
'* Please "rm -rf node_modules && npm i" *\n'+
'* then restore links as appropriate *\n'+
'*****************************************\n'+
"\033[91m");
process.exit(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)

View file

@ -0,0 +1,26 @@
#!/usr/bin/env node
const EMOJI_DATA = require('emojione/emoji.json');
const EMOJI_SUPPORTED = Object.keys(require('emojione').emojioneList);
const fs = require('fs');
const output = Object.keys(EMOJI_DATA).map(
(key) => {
const datum = EMOJI_DATA[key];
const newDatum = {
name: datum.name,
shortname: datum.shortname,
category: datum.category,
emoji_order: datum.emoji_order,
};
if (datum.aliases_ascii.length > 0) {
newDatum.aliases_ascii = datum.aliases_ascii;
}
return newDatum;
}
).filter((datum) => {
return EMOJI_SUPPORTED.includes(datum.shortname);
});
// Write to a file in src. Changes should be checked into git. This file is copied by
// babel using --copy-files
fs.writeFileSync('./src/stripped-emoji.json', JSON.stringify(output));

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,53 +1,99 @@
#!/usr/bin/env node
var fs = require('fs');
var path = require('path');
var glob = require('glob');
var args = require('optimist').argv;
var header = args.h || args.header;
var componentsDir = path.join('src', 'components');
var chokidar = require('chokidar');
var componentIndex = path.join('src', 'component-index.js');
var componentIndexTmp = componentIndex+".tmp";
var componentsDir = path.join('src', 'components');
var componentGlob = '**/*.js';
var prevFiles = [];
var packageJson = JSON.parse(fs.readFileSync('./package.json'));
function reskindex() {
var files = glob.sync(componentGlob, {cwd: componentsDir}).sort();
if (!filesHaveChanged(files, prevFiles)) {
return;
}
prevFiles = files;
var strm = fs.createWriteStream(componentIndex);
var header = args.h || args.header;
var packageJson = JSON.parse(fs.readFileSync('./package.json'));
if (header) {
strm.write(fs.readFileSync(header));
strm.write('\n');
var strm = fs.createWriteStream(componentIndexTmp);
if (header) {
strm.write(fs.readFileSync(header));
strm.write('\n');
}
strm.write("/*\n");
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(" * so you'd just be trying to swim upstream like a salmon.\n");
strm.write(" * You are not a salmon.\n");
strm.write(" */\n\n");
if (packageJson['matrix-react-parent']) {
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 {
strm.write("let components = {};\n");
}
for (var i = 0; i < files.length; ++i) {
var file = files[i].replace('.js', '');
var moduleName = (file.replace(/\//g, '.'));
var importName = moduleName.replace(/\./g, "$");
strm.write("import " + importName + " from './components/" + file + "';\n");
strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");");
strm.write('\n');
strm.uncork();
}
strm.write("export {components};\n");
strm.end();
fs.rename(componentIndexTmp, componentIndex, function(err) {
if(err) {
console.error("Error moving new index into place: " + err);
} else {
console.log('Reskindex: completed');
}
});
}
strm.write("/*\n");
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(" * so you'd just be trying to swim upstream like 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");
if (packageJson['matrix-react-parent']) {
strm.write("module.exports.components = require('"+packageJson['matrix-react-parent']+"/lib/component-index').components;\n\n");
} else {
strm.write("module.exports.components = {};\n");
// 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;
}
var files = glob.sync('**/*.js', {cwd: componentsDir}).sort();
for (var i = 0; i < files.length; ++i) {
var file = files[i].replace('.js', '');
var moduleName = (file.replace(/\//g, '.'));
var importName = moduleName.replace(/\./g, "$");
strm.write("import " + importName + " from './components/" + file + "';\n");
strm.write(importName + " && (module.exports.components['"+moduleName+"'] = " + importName + ");");
strm.write('\n');
strm.uncork();
// -w indicates watch mode where any FS events will trigger reskindex
if (!args.w) {
reskindex();
return;
}
strm.end();
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

@ -15,7 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var MatrixClientPeg = require("./MatrixClientPeg");
import MatrixClientPeg from './MatrixClientPeg';
import { _t } from './languageHandler';
/**
* Allows a user to add a third party identifier to their Home Server and,
@ -43,8 +44,8 @@ class AddThreepid {
this.sessionId = res.sid;
return res;
}, function(err) {
if (err.errcode == 'M_THREEPID_IN_USE') {
err.message = "This email address is already in use";
if (err.errcode === 'M_THREEPID_IN_USE') {
err.message = _t('This email address is already in use');
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
@ -68,8 +69,8 @@ class AddThreepid {
this.sessionId = res.sid;
return res;
}, function(err) {
if (err.errcode == 'M_THREEPID_IN_USE') {
err.message = "This phone number is already in use";
if (err.errcode === 'M_THREEPID_IN_USE') {
err.message = _t('This phone number is already in use');
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
@ -84,16 +85,15 @@ class AddThreepid {
* the request failed.
*/
checkEmailLinkClicked() {
var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
return MatrixClientPeg.get().addThreePid({
sid: this.sessionId,
client_secret: this.clientSecret,
id_server: identityServerDomain
id_server: identityServerDomain,
}, this.bind).catch(function(err) {
if (err.httpStatus === 401) {
err.message = "Failed to verify email address: make sure you clicked the link in the email";
}
else if (err.httpStatus) {
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
} else if (err.httpStatus) {
err.message += ` (Status ${err.httpStatus})`;
}
throw err;
@ -103,6 +103,7 @@ class AddThreepid {
/**
* Takes a phone number verification code as entered by the user and validates
* it with the ID server, then if successful, adds the phone number.
* @param {string} token phone number verification code as entered by the user
* @return {Promise} Resolves if the phone number was added. Rejects with an object
* with a "message" property which contains a human-readable message detailing why
* the request failed.
@ -118,7 +119,7 @@ class AddThreepid {
return MatrixClientPeg.get().addThreePid({
sid: this.sessionId,
client_secret: this.clientSecret,
id_server: identityServerDomain
id_server: identityServerDomain,
}, this.bind);
});
}

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

@ -15,18 +15,18 @@ limitations under the License.
*/
'use strict';
var ContentRepo = require("matrix-js-sdk").ContentRepo;
var MatrixClientPeg = require('./MatrixClientPeg');
import {ContentRepo} from 'matrix-js-sdk';
import MatrixClientPeg from './MatrixClientPeg';
module.exports = {
avatarUrlForMember: function(member, width, height, resizeMethod) {
var url = member.getAvatarUrl(
let url = member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
width,
height,
Math.floor(width * window.devicePixelRatio),
Math.floor(height * window.devicePixelRatio),
resizeMethod,
false,
false
false,
);
if (!url) {
// member can be null here currently since on invites, the JS SDK
@ -38,9 +38,11 @@ module.exports = {
},
avatarUrlForUser: function(user, width, height, resizeMethod) {
var url = ContentRepo.getHttpUriForMxc(
const url = ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
width, height, resizeMethod
Math.floor(width * window.devicePixelRatio),
Math.floor(height * window.devicePixelRatio),
resizeMethod,
);
if (!url || url.length === 0) {
return null;
@ -49,12 +51,11 @@ module.exports = {
},
defaultAvatarUrlForString: function(s) {
var images = ['76cfa6', '50e2c2', 'f4c371'];
var total = 0;
for (var i = 0; i < s.length; ++i) {
const images = ['76cfa6', '50e2c2', 'f4c371'];
let total = 0;
for (let i = 0; i < s.length; ++i) {
total += s.charCodeAt(i);
}
return 'img/' + images[total % images.length] + '.png';
}
},
};

View file

@ -17,6 +17,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import dis from './dispatcher';
/**
* Base class for classes that provide platform-specific functionality
* eg. Setting an application badge or displaying notifications
@ -27,6 +29,21 @@ export default class BasePlatform {
constructor() {
this.notificationCount = 0;
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) {
@ -40,6 +57,7 @@ export default class BasePlatform {
/**
* Returns true if the platform supports displaying
* notifications, otherwise false.
* @returns {boolean} whether the platform supports displaying notifications
*/
supportsNotifications(): boolean {
return false;
@ -48,6 +66,7 @@ export default class BasePlatform {
/**
* Returns true if the application currently has permission
* to display notifications. Otherwise false.
* @returns {boolean} whether the application has permission to display notifications
*/
maySendNotifications(): boolean {
return false;
@ -66,11 +85,14 @@ export default class BasePlatform {
displayNotification(title: string, msg: string, avatarUrl: string, room: Object) {
}
loudNotification(ev: Event, room: Object) {
}
/**
* Returns a promise that resolves to a string representing
* the current version of the application.
*/
getAppVersion() {
getAppVersion(): Promise<string> {
throw new Error("getAppVersion not implemented!");
}
@ -79,10 +101,12 @@ export default class BasePlatform {
* with getUserMedia, return a string explaining why not.
* Otherwise, return null.
*/
screenCaptureErrorString() {
screenCaptureErrorString(): string {
return "Not implemented";
}
isElectron(): boolean { return false; }
/**
* Restarts the application, without neccessarily reloading
* any application code

View file

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

@ -0,0 +1,82 @@
//@flow
/*
Copyright 2017 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 {ContentState} from 'draft-js';
import * as RichText from './RichText';
import Markdown from './Markdown';
import _flow from 'lodash/flow';
import _clamp from 'lodash/clamp';
type MessageFormat = 'html' | 'markdown';
class HistoryItem {
message: string = '';
format: MessageFormat = 'html';
constructor(message: string, format: MessageFormat) {
this.message = message;
this.format = format;
}
toContentState(format: MessageFormat): ContentState {
let {message} = this;
if (format === 'markdown') {
if (this.format === 'html') {
message = _flow([RichText.htmlToContentState, RichText.stateToMarkdown])(message);
}
return ContentState.createFromText(message);
} else {
if (this.format === 'markdown') {
message = new Markdown(message).toHTML();
}
return RichText.htmlToContentState(message);
}
}
}
export default class ComposerHistoryManager {
history: Array<HistoryItem> = [];
prefix: string;
lastIndex: number = 0;
currentIndex: number = 0;
constructor(roomId: string, prefix: string = 'mx_composer_history_') {
this.prefix = prefix + roomId;
// TODO: Performance issues?
let item;
for(; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
this.history.push(
Object.assign(new HistoryItem(), JSON.parse(item)),
);
}
this.lastIndex = this.currentIndex;
}
addItem(message: string, format: MessageFormat) {
const item = new HistoryItem(message, format);
this.history.push(item);
this.currentIndex = this.lastIndex + 1;
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item));
}
getItem(offset: number, format: MessageFormat): ?ContentState {
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.lastIndex - 1);
const item = this.history[this.currentIndex];
return item ? item.toContentState(format) : null;
}
}

View file

@ -16,11 +16,12 @@ limitations under the License.
'use strict';
var q = require('q');
import Promise from 'bluebird';
var extend = require('./extend');
var dis = require('./dispatcher');
var MatrixClientPeg = require('./MatrixClientPeg');
var sdk = require('./index');
import { _t } from './languageHandler';
var Modal = require('./Modal');
var encrypt = require("browser-encrypt-attachment");
@ -51,7 +52,7 @@ const MAX_HEIGHT = 600;
* and a thumbnail key.
*/
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
const deferred = q.defer();
const deferred = Promise.defer();
var targetWidth = inputWidth;
var targetHeight = inputHeight;
@ -94,7 +95,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
* @return {Promise} A promise that resolves with the html image element.
*/
function loadImageElement(imageFile) {
const deferred = q.defer();
const deferred = Promise.defer();
// Load the file into an html element
const img = document.createElement("img");
@ -153,7 +154,7 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
* @return {Promise} A promise that resolves with the video image element.
*/
function loadVideoElement(videoFile) {
const deferred = q.defer();
const deferred = Promise.defer();
// Load the file into an html element
const video = document.createElement("video");
@ -209,7 +210,7 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
* is read.
*/
function readFileAsArrayBuffer(file) {
const deferred = q.defer();
const deferred = Promise.defer();
const reader = new FileReader();
reader.onload = function(e) {
deferred.resolve(e.target.result);
@ -228,11 +229,13 @@ function readFileAsArrayBuffer(file) {
* @param {MatrixClient} matrixClient The matrix client to upload the file with.
* @param {String} roomId The ID of the room being uploaded to.
* @param {File} file The file to upload.
* @param {Function?} progressHandler optional callback to be called when a chunk of
* data is uploaded.
* @return {Promise} A promise that resolves with an object.
* If the file is unencrypted then the object will have a "url" key.
* If the file is encrypted then the object will have a "file" key.
*/
function uploadFile(matrixClient, roomId, file) {
function uploadFile(matrixClient, roomId, file, progressHandler) {
if (matrixClient.isRoomEncrypted(roomId)) {
// If the room is encrypted then encrypt the file before uploading it.
// First read the file into memory.
@ -244,7 +247,9 @@ function uploadFile(matrixClient, roomId, file) {
const encryptInfo = encryptResult.info;
// Pass the encrypted data as a Blob to the uploader.
const blob = new Blob([encryptResult.data]);
return matrixClient.uploadContent(blob).then(function(url) {
return matrixClient.uploadContent(blob, {
progressHandler: progressHandler,
}).then(function(url) {
// If the attachment is encrypted then bundle the URL along
// with the information needed to decrypt the attachment and
// add it under a file key.
@ -256,7 +261,9 @@ function uploadFile(matrixClient, roomId, file) {
});
});
} else {
const basePromise = matrixClient.uploadContent(file);
const basePromise = matrixClient.uploadContent(file, {
progressHandler: progressHandler,
});
const promise1 = basePromise.then(function(url) {
// If the attachment isn't encrypted then include the URL directly.
return {"url": url};
@ -287,7 +294,7 @@ class ContentMessages {
content.info.mimetype = file.type;
}
const def = q.defer();
const def = Promise.defer();
if (file.type.indexOf('image/') == 0) {
content.msgtype = 'm.image';
infoForImageFile(matrixClient, roomId, file).then(imageInfo=>{
@ -325,36 +332,37 @@ class ContentMessages {
dis.dispatch({action: 'upload_started'});
var error;
function onProgress(ev) {
upload.total = ev.total;
upload.loaded = ev.loaded;
dis.dispatch({action: 'upload_progress', upload: upload});
}
return def.promise.then(function() {
// XXX: upload.promise must be the promise that
// is returned by uploadFile as it has an abort()
// method hacked onto it.
upload.promise = uploadFile(
matrixClient, roomId, file
matrixClient, roomId, file, onProgress,
);
return upload.promise.then(function(result) {
content.file = result.file;
content.url = result.url;
});
}).progress(function(ev) {
if (ev) {
upload.total = ev.total;
upload.loaded = ev.loaded;
dis.dispatch({action: 'upload_progress', upload: upload});
}
}).then(function(url) {
return matrixClient.sendMessage(roomId, content);
}, function(err) {
error = err;
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) {
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");
Modal.createDialog(ErrorDialog, {
title: "Upload Failed",
description: desc
title: _t('Upload Failed'),
description: desc,
});
}
}).finally(() => {

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 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.
@ -15,38 +16,90 @@ limitations under the License.
*/
'use strict';
import { _t } from './languageHandler';
var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
function getDaysArray() {
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'),
];
}
function pad(n) {
return (n < 10 ? '0' : '') + n;
}
function twelveHourTime(date) {
let hours = date.getHours() % 12;
const minutes = pad(date.getMinutes());
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
hours = hours ? hours : 12; // convert 0 -> 12
return `${hours}:${minutes}${ampm}`;
}
module.exports = {
formatDate: function(date) {
// date.toLocaleTimeString is completely system dependent.
// just go 24h for now
function pad(n) {
return (n < 10 ? '0' : '') + n;
}
var now = new Date();
formatDate: function(date, showTwelveHour=false) {
const now = new Date();
const days = getDaysArray();
const months = getMonthsArray();
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) {
// 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()) {
// 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),
});
}
else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
}
else /* if (now.getFullYear() === date.getFullYear()) */ {
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
}
/*
else {
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
}
*/
return this.formatFullDate(date, showTwelveHour);
},
formatTime: function(date) {
//return pad(date.getHours()) + ':' + pad(date.getMinutes());
return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2);
}
};
formatFullDate: function(date, showTwelveHour=false) {
const days = getDaysArray();
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

@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var sdk = require('./index');
import sdk from './index';
function isMatch(query, name, uid) {
query = query.toLowerCase();
@ -33,8 +32,8 @@ function isMatch(query, name, uid) {
}
// split spaces in name and try matching constituent parts
var parts = name.split(" ");
for (var i = 0; i < parts.length; i++) {
const parts = name.split(" ");
for (let i = 0; i < parts.length; i++) {
if (parts[i].indexOf(query) === 0) {
return true;
}
@ -67,7 +66,7 @@ class Entity {
class MemberEntity extends Entity {
getJsx() {
var MemberTile = sdk.getComponent("rooms.MemberTile");
const MemberTile = sdk.getComponent("rooms.MemberTile");
return (
<MemberTile key={this.model.userId} member={this.model} />
);
@ -84,6 +83,7 @@ class UserEntity extends Entity {
super(model);
this.showInviteButton = Boolean(showInviteButton);
this.inviteFn = inviteFn;
this.onClick = this.onClick.bind(this);
}
onClick() {
@ -93,15 +93,15 @@ class UserEntity extends Entity {
}
getJsx() {
var UserTile = sdk.getComponent("rooms.UserTile");
const UserTile = sdk.getComponent("rooms.UserTile");
return (
<UserTile key={this.model.userId} user={this.model}
showInviteButton={this.showInviteButton} onClick={this.onClick.bind(this)} />
showInviteButton={this.showInviteButton} onClick={this.onClick} />
);
}
matches(queryString) {
var name = this.model.displayName || this.model.userId;
const name = this.model.displayName || this.model.userId;
return isMatch(queryString, name, this.model.userId);
}
}
@ -109,7 +109,7 @@ class UserEntity extends Entity {
module.exports = {
newEntity: function(jsx, matchFn) {
var entity = new Entity();
const entity = new Entity();
entity.getJsx = function() {
return jsx;
};
@ -137,5 +137,5 @@ module.exports = {
return users.map(function(u) {
return new UserEntity(u, showInviteButton, inviteFn);
});
}
},
};

View file

@ -23,8 +23,12 @@ var linkifyMatrix = require('./linkify-matrix');
import escape from 'lodash/escape';
import emojione from 'emojione';
import classNames from 'classnames';
import MatrixClientPeg from './MatrixClientPeg';
emojione.imagePathSVG = 'emojione/svg/';
// Store PNG path for displaying many flags at once (for increased performance over SVG)
emojione.imagePathPNG = 'emojione/png/';
// Use SVGs for emojis
emojione.imageType = 'svg';
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
@ -34,7 +38,7 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
* because we want to include emoji shortnames in title text
*/
export function unicodeToImage(str) {
let replaceWith, unicode, alt;
let replaceWith, unicode, alt, short, fname;
const mappedUnicode = emojione.mapUnicodeToShort();
str = str.replace(emojione.regUnicode, function(unicodeChar) {
@ -46,11 +50,14 @@ export function unicodeToImage(str) {
// get the unicode codepoint from the actual char
unicode = emojione.jsEscapeMap[unicodeChar];
short = mappedUnicode[unicode];
fname = emojione.emojioneList[short].fname;
// depending on the settings, we'll either add the native unicode as the alt tag, otherwise the shortname
alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
const title = mappedUnicode[unicode];
replaceWith = `<img class="mx_emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${unicode}.svg${emojione.cacheBustParam}"/>`;
replaceWith = `<img class="mx_emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
return replaceWith;
}
});
@ -64,17 +71,24 @@ export function unicodeToImage(str) {
* emoji.
*
* @param alt {string} String to use for the image alt text
* @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used.
* @param unicode {integer} One or more integers representing unicode characters
* @returns A img node with the corresponding emoji
*/
export function charactersToImageNode(alt, ...unicode) {
export function charactersToImageNode(alt, useSvg, ...unicode) {
const fileName = unicode.map((u) => {
return u.toString(16);
}).join('-');
return <img alt={alt} src={`${emojione.imagePathSVG}${fileName}.svg${emojione.cacheBustParam}`}/>;
const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG;
const fileType = useSvg ? 'svg' : 'png';
return <img
alt={alt}
src={`${path}${fileName}.${fileType}${emojione.cacheBustParam}`}
/>;
}
export function stripParagraphs(html: string): string {
export function processHtmlForSending(html: string): string {
const contentDiv = document.createElement('div');
contentDiv.innerHTML = html;
@ -83,10 +97,21 @@ export function stripParagraphs(html: string): string {
}
let contentHTML = "";
for (let i=0; i<contentDiv.children.length; i++) {
for (let i=0; i < contentDiv.children.length; i++) {
const element = contentDiv.children[i];
if (element.tagName.toLowerCase() === 'p') {
contentHTML += element.innerHTML + '<br />';
contentHTML += element.innerHTML;
// Don't add a <br /> for the last <p>
if (i !== contentDiv.children.length - 1) {
contentHTML += '<br />';
}
} else if (element.tagName.toLowerCase() === 'pre') {
// Replace "<br>\n" with "\n" within `<pre>` tags because the <br> is
// redundant. This is a workaround for a bug in draft-js-export-html:
// https://github.com/sstur/draft-js-export-html/issues/62
contentHTML += '<pre>' +
element.innerHTML.replace(/<br>\n/g, '\n').trim() +
'</pre>';
} else {
const temp = document.createElement('div');
temp.appendChild(element.cloneNode(true));
@ -97,34 +122,39 @@ export function stripParagraphs(html: string): string {
return contentHTML;
}
var sanitizeHtmlParams = {
/*
* Given an untrusted HTML string, return a React node with an sanitized version
* of that HTML.
*/
export function sanitizedHtmlNode(insaneHtml) {
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
}
const sanitizeHtmlParams = {
allowedTags: [
'font', // custom to matrix for IRC-style font coloring
'del', // for markdown
// deliberately no h1/h2 to stop people shouting.
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
],
allowedAttributes: {
// custom ones first:
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
// We don't currently allow img itself by default, but this
// would make sense if we did
img: ['src'],
img: ['src', 'width', 'height', 'alt', 'title'],
ol: ['start'],
code: ['class'], // We don't actually allow all classes, we filter them in transformTags
},
// Lots of these won't come up by default because we don't allow them
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
// URL schemes we permit
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
// DO NOT USE. sanitize-html allows all URL starting with '//'
// so this will always allow links to whatever scheme the
// host page is served over.
allowedSchemesByTag: {},
allowProtocolRelative: false,
transformTags: { // custom to matrix
// add blank targets to all hyperlinks except vector URLs
@ -139,22 +169,50 @@ var sanitizeHtmlParams = {
attribs.href = m[1];
delete attribs.target;
}
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
if (m) {
var entity = m[1];
if (entity[0] === '@') {
attribs.href = '#/user/' + entity;
else {
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
if (m) {
var entity = m[1];
if (entity[0] === '@') {
attribs.href = '#/user/' + entity;
}
else if (entity[0] === '#' || entity[0] === '!') {
attribs.href = '#/room/' + entity;
}
delete attribs.target;
}
else if (entity[0] === '#' || entity[0] === '!') {
attribs.href = '#/room/' + entity;
}
delete attribs.target;
}
}
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
return { tagName: tagName, attribs : attribs };
},
'img': function(tagName, attribs) {
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
// because transformTags is used _before_ we filter by allowedSchemesByTag and
// we don't want to allow images with `https?` `src`s.
if (!attribs.src.startsWith('mxc://')) {
return { tagName, attribs: {}};
}
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
attribs.src,
attribs.width || 800,
attribs.height || 600,
);
return { tagName: tagName, attribs: attribs };
},
'code': function(tagName, attribs) {
if (typeof attribs.class !== 'undefined') {
// Filter out all classes other than ones starting with language- for syntax highlighting.
let classes = attribs.class.split(/\s+/).filter(function(cl) {
return cl.startsWith('language-');
});
attribs.class = classes.join(' ');
}
return {
tagName: tagName,
attribs: attribs,
};
},
'*': function(tagName, attribs) {
// Delete any style previously assigned, style is an allowedTag for font and span
// because attributes are stripped after transforming
@ -335,6 +393,7 @@ export function bodyToHtml(content, highlights, opts) {
}
safeBody = sanitizeHtml(body, sanitizeHtmlParams);
safeBody = unicodeToImage(safeBody);
safeBody = addCodeCopyButton(safeBody);
}
finally {
delete sanitizeHtmlParams.textFilter;
@ -350,7 +409,24 @@ export function bodyToHtml(content, highlights, opts) {
'mx_EventTile_bigEmoji': emojiBody,
'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) {

View file

@ -21,6 +21,7 @@ module.exports = {
ENTER: 13,
SHIFT: 16,
ESCAPE: 27,
SPACE: 32,
PAGE_UP: 33,
PAGE_DOWN: 34,
END: 35,
@ -30,6 +31,30 @@ module.exports = {
RIGHT: 39,
DOWN: 40,
DELETE: 46,
KEY_A: 65,
KEY_B: 66,
KEY_C: 67,
KEY_D: 68,
KEY_E: 69,
KEY_F: 70,
KEY_G: 71,
KEY_H: 72,
KEY_I: 73,
KEY_J: 74,
KEY_K: 75,
KEY_L: 76,
KEY_M: 77,
KEY_N: 78,
KEY_O: 79,
KEY_P: 80,
KEY_Q: 81,
KEY_R: 82,
KEY_S: 83,
KEY_T: 84,
KEY_U: 85,
KEY_V: 86,
KEY_W: 87,
KEY_X: 88,
KEY_Y: 89,
KEY_Z: 90,
};

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

@ -15,10 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import q from 'q';
import Promise from 'bluebird';
import Matrix from 'matrix-js-sdk';
import MatrixClientPeg from './MatrixClientPeg';
import createMatrixClient from './utils/createMatrixClient';
import Analytics from './Analytics';
import Notifier from './Notifier';
import UserActivity from './UserActivity';
import Presence from './Presence';
@ -32,28 +34,19 @@ import sdk from './index';
* Called at startup, to attempt to build a logged-in Matrix session. It tries
* a number of things:
*
* 0. if it looks like we are in the middle of a registration process, it does
* nothing.
*
* 1. if we have a loginToken in the (real) query params, it uses that to log
* in.
*
* 2. if we have a guest access token in the fragment query params, it uses
* 1. if we have a guest access token in the fragment query params, it uses
* that.
*
* 3. if an access token is stored in local storage (from a previous session),
* 2. if an access token is stored in local storage (from a previous session),
* it uses that.
*
* 4. it attempts to auto-register as a guest user.
* 3. it attempts to auto-register as a guest user.
*
* If any of steps 1-4 are successful, it will call {setLoggedIn}, which in
* If any of steps 1-4 are successful, it will call {_doSetLoggedIn}, which in
* turn will raise on_logged_in and will_start_client events.
*
* It returns a promise which resolves when the above process completes.
*
* @param {object} opts.realQueryParams: string->string map of the
* query-parameters extracted from the real query-string of the starting
* URI.
* @param {object} opts
*
* @param {object} opts.fragmentQueryParams: string->string map of the
* query-parameters extracted from the #-fragment of the starting URI.
@ -67,54 +60,39 @@ import sdk from './index';
* @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is
* true; defines the IS to use.
*
* @returns {Promise} a promise which resolves when the above process completes.
* Resolves to `true` if we ended up starting a session, or `false` if we
* failed.
*/
export function loadSession(opts) {
const realQueryParams = opts.realQueryParams || {};
const fragmentQueryParams = opts.fragmentQueryParams || {};
let enableGuest = opts.enableGuest || false;
const guestHsUrl = opts.guestHsUrl;
const guestIsUrl = opts.guestIsUrl;
const defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
if (fragmentQueryParams.client_secret && fragmentQueryParams.sid) {
// this happens during email validation: the email contains a link to the
// IS, which in turn redirects back to vector. We let MatrixChat create a
// Registration component which completes the next stage of registration.
console.log("Not registering as guest: registration already in progress.");
return q();
}
if (!guestHsUrl) {
console.warn("Cannot enable guest access: can't determine HS URL to use");
enableGuest = false;
}
if (realQueryParams.loginToken) {
if (!realQueryParams.homeserver) {
console.warn("Cannot log in with token: can't determine HS URL to use");
} else {
return _loginWithToken(realQueryParams, defaultDeviceDisplayName);
}
}
if (enableGuest &&
fragmentQueryParams.guest_user_id &&
fragmentQueryParams.guest_access_token
) {
console.log("Using guest access credentials");
setLoggedIn({
return _doSetLoggedIn({
userId: fragmentQueryParams.guest_user_id,
accessToken: fragmentQueryParams.guest_access_token,
homeserverUrl: guestHsUrl,
identityServerUrl: guestIsUrl,
guest: true,
});
return q();
}, true).then(() => true);
}
return _restoreFromLocalStorage().then((success) => {
if (success) {
return;
return true;
}
if (enableGuest) {
@ -122,12 +100,32 @@ export function loadSession(opts) {
}
// fall back to login screen
return false;
});
}
function _loginWithToken(queryParams, defaultDeviceDisplayName) {
/**
* @param {Object} queryParams string->string map of the
* query-parameters extracted from the real query-string of the starting
* URI.
*
* @param {String} defaultDeviceDisplayName
*
* @returns {Promise} promise which resolves to true if we completed the token
* login, else false
*/
export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
if (!queryParams.loginToken) {
return Promise.resolve(false);
}
if (!queryParams.homeserver) {
console.warn("Cannot log in with token: can't determine HS URL to use");
return Promise.resolve(false);
}
// create a temporary MatrixClient to do the login
var client = Matrix.createClient({
const client = Matrix.createClient({
baseUrl: queryParams.homeserver,
});
@ -138,28 +136,32 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
},
).then(function(data) {
console.log("Logged in with token");
setLoggedIn({
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token,
homeserverUrl: queryParams.homeserver,
identityServerUrl: queryParams.identityServer,
guest: false,
return _clearStorage().then(() => {
_persistCredentialsToLocalStorage({
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token,
homeserverUrl: queryParams.homeserver,
identityServerUrl: queryParams.identityServer,
guest: false,
});
return true;
});
}, (err) => {
}).catch((err) => {
console.error("Failed to log in with login token: " + err + " " +
err.data);
return false;
});
}
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.
// Not really sure where the right home for it is.
// create a temporary MatrixClient to do the login
var client = Matrix.createClient({
const client = Matrix.createClient({
baseUrl: hsUrl,
});
@ -168,83 +170,78 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
initial_device_display_name: defaultDeviceDisplayName,
},
}).then((creds) => {
console.log("Registered as guest: %s", creds.user_id);
setLoggedIn({
console.log(`Registered as guest: ${creds.user_id}`);
return _doSetLoggedIn({
userId: creds.user_id,
deviceId: creds.device_id,
accessToken: creds.access_token,
homeserverUrl: hsUrl,
identityServerUrl: isUrl,
guest: true,
});
}, true).then(() => true);
}, (err) => {
console.error("Failed to register as guest: " + err + " " + err.data);
return false;
});
}
// returns a promise which resolves to true if a session is found in
// 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() {
if (!localStorage) {
return q(false);
return Promise.resolve(false);
}
const hs_url = localStorage.getItem("mx_hs_url");
const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
const access_token = localStorage.getItem("mx_access_token");
const user_id = localStorage.getItem("mx_user_id");
const device_id = localStorage.getItem("mx_device_id");
const hsUrl = localStorage.getItem("mx_hs_url");
const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
const accessToken = localStorage.getItem("mx_access_token");
const userId = localStorage.getItem("mx_user_id");
const deviceId = localStorage.getItem("mx_device_id");
let is_guest;
let isGuest;
if (localStorage.getItem("mx_is_guest") !== null) {
is_guest = localStorage.getItem("mx_is_guest") === "true";
isGuest = localStorage.getItem("mx_is_guest") === "true";
} else {
// legacy key name
is_guest = localStorage.getItem("matrix-is-guest") === "true";
isGuest = localStorage.getItem("matrix-is-guest") === "true";
}
if (access_token && user_id && hs_url) {
console.log("Restoring session for %s", user_id);
if (accessToken && userId && hsUrl) {
console.log(`Restoring session for ${userId}`);
try {
setLoggedIn({
userId: user_id,
deviceId: device_id,
accessToken: access_token,
homeserverUrl: hs_url,
identityServerUrl: is_url,
guest: is_guest,
});
return q(true);
return _doSetLoggedIn({
userId: userId,
deviceId: deviceId,
accessToken: accessToken,
homeserverUrl: hsUrl,
identityServerUrl: isUrl,
guest: isGuest,
}, false).then(() => true);
} catch (e) {
return _handleRestoreFailure(e);
}
} else {
console.log("No previous session found.");
return q(false);
return Promise.resolve(false);
}
}
function _handleRestoreFailure(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 = Promise.defer();
const SessionRestoreErrorDialog =
sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
Modal.createDialog(SessionRestoreErrorDialog, {
error: msg,
error: e.message,
onFinished: (success) => {
def.resolve(success);
},
@ -253,7 +250,7 @@ function _handleRestoreFailure(e) {
return def.promise.then((success) => {
if (success) {
// user clicked continue.
_clearLocalStorage();
_clearStorage();
return false;
}
@ -264,46 +261,82 @@ function _handleRestoreFailure(e) {
let rtsClient = null;
export function initRtsClient(url) {
rtsClient = new RtsClient(url);
if (url) {
rtsClient = new RtsClient(url);
} else {
rtsClient = null;
}
}
/**
* Transitions to a logged-in state using the given credentials
* Transitions to a logged-in state using the given credentials.
*
* Starts the matrix client and all other react-sdk services that
* listen for events while a session is logged in.
*
* Also stops the old MatrixClient and clears old credentials/etc out of
* storage before starting the new client.
*
* @param {MatrixClientCreds} credentials The credentials to use
*
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
*/
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);
console.log("setLoggedIn => %s (guest=%s) hs=%s",
credentials.userId, credentials.guest,
credentials.homeserverUrl);
console.log(
"setLoggedIn: mxid: " + credentials.userId +
" deviceId: " + credentials.deviceId +
" guest: " + credentials.guest +
" hs: " + credentials.homeserverUrl,
);
// This is dispatched to indicate that the user is still in the process of logging in
// because `teamPromise` may take some time to resolve, breaking the assumption that
// `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms
// later than MatrixChat might assume.
dis.dispatch({action: 'on_logging_in'});
//
// we fire it *synchronously* to make sure it fires before on_logged_in.
// (dis.dispatch uses `setTimeout`, which does not guarantee ordering.)
dis.dispatch({action: 'on_logging_in'}, true);
if (clearStorage) {
await _clearStorage();
}
Analytics.setGuest(credentials.guest);
// Resolves by default
let teamPromise = Promise.resolve(null);
// persist the session
if (localStorage) {
try {
localStorage.setItem("mx_hs_url", credentials.homeserverUrl);
localStorage.setItem("mx_is_url", credentials.identityServerUrl);
localStorage.setItem("mx_user_id", credentials.userId);
localStorage.setItem("mx_access_token", credentials.accessToken);
localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
_persistCredentialsToLocalStorage(credentials);
// 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);
// The user registered as a PWLU (PassWord-Less User), the generated password
// is cached here such that the user can change it at a later time.
if (credentials.password) {
// Update SessionStore
dis.dispatch({
action: 'cached_password',
cachedPassword: credentials.password,
});
}
console.log("Session persisted for %s", credentials.userId);
} catch (e) {
console.warn("Error using local storage: can't persist session!", e);
}
@ -314,25 +347,42 @@ export function setLoggedIn(credentials) {
localStorage.setItem("mx_team_token", body.team_token);
}
return body.team_token;
}, (err) => {
console.warn(`Failed to get team token on login: ${err}` );
return null;
});
}
} else {
console.warn("No local storage available: can't persist session!");
}
// stop any running clients before we create a new one with these new credentials
stopMatrixClient();
MatrixClientPeg.replaceUsingCreds(credentials);
teamPromise.then((teamToken) => {
dis.dispatch({action: 'on_logged_in', teamToken: teamToken});
}, (err) => {
console.warn("Failed to get team token on login", err);
dis.dispatch({action: 'on_logged_in', teamToken: null});
});
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}`);
}
/**
@ -352,7 +402,7 @@ export function logout() {
return;
}
return MatrixClientPeg.get().logout().then(onLoggedOut,
MatrixClientPeg.get().logout().then(onLoggedOut,
(err) => {
// Just throwing an error here is going to be very unhelpful
// if you're trying to log out because your server's down and
@ -363,15 +413,17 @@ export function logout() {
// change your password).
console.log("Failed to call logout API: token will not be invalidated");
onLoggedOut();
}
);
},
).done();
}
/**
* Starts the matrix client and all other react-sdk services that
* listen for events while a session is logged in.
*/
export function startMatrixClient() {
function startMatrixClient() {
console.log(`Lifecycle: Starting MatrixClient`);
// dispatch this before starting the matrix client: it's used
// to add listeners for the 'sync' event so otherwise we'd have
// a race condition (and we need to dispatch synchronously for this
@ -387,44 +439,54 @@ export function startMatrixClient() {
}
/*
* Stops a running client and all related services, used after
* a session has been logged out / ended.
* Stops a running client and all related services, and clears persistent
* storage. Used after a session has been logged out.
*/
export function onLoggedOut() {
_clearLocalStorage();
stopMatrixClient();
_clearStorage().done();
dis.dispatch({action: 'on_logged_out'});
}
function _clearLocalStorage() {
if (!window.localStorage) {
return;
}
const hsUrl = window.localStorage.getItem("mx_hs_url");
const isUrl = window.localStorage.getItem("mx_is_url");
window.localStorage.clear();
/**
* @returns {Promise} promise which resolves once the stores have been cleared
*/
function _clearStorage() {
Analytics.logout();
// preserve our HS & IS URLs for convenience
// N.B. we cache them in hsUrl/isUrl and can't really inline them
// as getCurrentHsUrl() may call through to localStorage.
// NB. We do clear the device ID (as well as all the settings)
if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl);
if (isUrl) window.localStorage.setItem("mx_is_url", isUrl);
if (window.localStorage) {
const hsUrl = window.localStorage.getItem("mx_hs_url");
const isUrl = window.localStorage.getItem("mx_is_url");
window.localStorage.clear();
// preserve our HS & IS URLs for convenience
// N.B. we cache them in hsUrl/isUrl and can't really inline them
// as getCurrentHsUrl() may call through to localStorage.
// NB. We do clear the device ID (as well as all the settings)
if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl);
if (isUrl) window.localStorage.setItem("mx_is_url", isUrl);
}
// create a temporary client to clear out the persistent stores.
const cli = createMatrixClient({
// we'll never make any requests, so can pass a bogus HS URL
baseUrl: "",
});
return cli.clearStores();
}
/**
* Stop all the background processes related to the current client
* Stop all the background processes related to the current client.
*/
export function stopMatrixClient() {
Notifier.stop();
UserActivity.stop();
Presence.stop();
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
if (cli) {
cli.stopClient();
cli.removeAllListeners();
cli.store.deleteAllData();
MatrixClientPeg.unset();
}
}

View file

@ -16,8 +16,9 @@ limitations under the License.
*/
import Matrix from "matrix-js-sdk";
import { _t } from "./languageHandler";
import q from 'q';
import Promise from 'bluebird';
import url from 'url';
export default class Login {
@ -96,11 +97,6 @@ export default class Login {
guest: true
};
}, (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;
});
}
@ -148,7 +144,7 @@ export default class Login {
const client = this._createTemporaryClient();
return client.login('m.login.password', loginParams).then(function(data) {
return q({
return Promise.resolve({
homeserverUrl: self._hsUrl,
identityServerUrl: self._isUrl,
userId: data.user_id,
@ -156,15 +152,7 @@ export default class Login {
accessToken: data.access_token
});
}, function(error) {
if (error.httpStatus == 400 && loginParams.medium) {
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 (error.httpStatus === 403) {
if (self._fallbackHsUrl) {
var fbClient = Matrix.createClient({
baseUrl: self._fallbackHsUrl,
@ -172,7 +160,7 @@ export default class Login {
});
return fbClient.login('m.login.password', loginParams).then(function(data) {
return q({
return Promise.resolve({
homeserverUrl: self._fallbackHsUrl,
identityServerUrl: self._isUrl,
userId: data.user_id,
@ -185,21 +173,23 @@ export default class Login {
});
}
}
else {
error.friendlyText = (
'There was a problem logging in. (HTTP ' + error.httpStatus + ")"
);
}
throw error;
});
}
redirectToCas() {
var client = this._createTemporaryClient();
var parsedUrl = url.parse(window.location.href, true);
const client = this._createTemporaryClient();
const parsedUrl = url.parse(window.location.href, true);
// XXX: at this point, the fragment will always be #/login, which is no
// use to anyone. Ideally, we would get the intended fragment from
// MatrixChat.screenAfterLogin so that you could follow #/room links etc
// through a CAS login.
parsedUrl.hash = "";
parsedUrl.query["homeserver"] = client.getHomeserverUrl();
parsedUrl.query["identityServer"] = client.getIdentityServerUrl();
var casUrl = client.getCasLoginUrl(url.format(parsedUrl));
const casUrl = client.getCasLoginUrl(url.format(parsedUrl));
window.location.href = casUrl;
}
}

View file

@ -17,7 +17,7 @@ limitations under the License.
import commonmark from 'commonmark';
import escape from 'lodash/escape';
const ALLOWED_HTML_TAGS = ['del'];
const ALLOWED_HTML_TAGS = ['del', 'u'];
// These types of node are definitely text
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 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.
@ -16,13 +17,10 @@ limitations under the License.
'use strict';
import q from "q";
import Matrix from 'matrix-js-sdk';
import utils from 'matrix-js-sdk/lib/utils';
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
const localStorage = window.localStorage;
import createMatrixClient from './utils/createMatrixClient';
interface MatrixClientCreds {
homeserverUrl: string,
@ -50,7 +48,6 @@ class MatrixClientPeg {
this.opts = {
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
*/
setIndexedDbWorkerScript(script) {
this.indexedDbWorkerScript = script;
createMatrixClient.indexedDbWorkerScript = script;
}
get(): MatrixClient {
@ -80,20 +77,38 @@ class MatrixClientPeg {
this._createClient(creds);
}
start() {
async start() {
// try to initialise e2e on the new client
try {
// check that we have a version of the js-sdk which includes initCrypto
if (this.matrixClient.initCrypto) {
await this.matrixClient.initCrypto();
}
} catch(e) {
// this can happen for a number of reasons, the most likely being
// that the olm library was missing. It's not fatal.
console.warn("Unable to initialise e2e: " + e);
}
const opts = utils.deepCopy(this.opts);
// the react sdk doesn't work without this, so don't allow
opts.pendingEventOrdering = "detached";
let promise = this.matrixClient.store.startup();
// log any errors when starting up the database (if one exists)
promise.catch((err) => { console.error(err); });
try {
let promise = this.matrixClient.store.startup();
console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
await promise;
} catch(err) {
// log any errors when starting up the database (if one exists)
console.error(`Error starting matrixclient store: ${err}`);
}
// regardless of errors, start the client. If we did error out, we'll
// just end up doing a full initial /sync.
promise.finally(() => {
this.get().startClient(opts);
});
console.log(`MatrixClientPeg: really starting MatrixClient`);
this.get().startClient(opts);
console.log(`MatrixClientPeg: MatrixClient started`);
}
getCredentials(): MatrixClientCreds {
@ -130,22 +145,7 @@ class MatrixClientPeg {
timelineSupport: true,
};
if (localStorage) {
opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
}
if (window.indexedDB && localStorage) {
// FIXME: bodge to remove old database. Remove this after a few weeks.
window.indexedDB.deleteDatabase("matrix-js-sdk:default");
opts.store = new Matrix.IndexedDBStore({
indexedDB: window.indexedDB,
dbName: "riot-web-sync",
localStorage: localStorage,
workerScript: this.indexedDbWorkerScript,
});
}
this.matrixClient = Matrix.createClient(opts);
this.matrixClient = createMatrixClient(opts, this.indexedDbWorkerScript);
// we're going to add eventlisteners for each matrix event tile, so the
// potential number of event listeners is quite high.

View file

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

@ -15,11 +15,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var MatrixClientPeg = require("./MatrixClientPeg");
var PlatformPeg = require("./PlatformPeg");
var TextForEvent = require('./TextForEvent');
var Avatar = require('./Avatar');
var dis = require("./dispatcher");
import MatrixClientPeg from './MatrixClientPeg';
import PlatformPeg from './PlatformPeg';
import TextForEvent from './TextForEvent';
import Analytics from './Analytics';
import Avatar from './Avatar';
import dis from './dispatcher';
import sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal';
/*
* Dispatches:
@ -29,7 +33,7 @@ var dis = require("./dispatcher");
* }
*/
var Notifier = {
const Notifier = {
notifsByRoom: {},
notificationMessageForEvent: function(ev) {
@ -48,16 +52,16 @@ var Notifier = {
return;
}
var msg = this.notificationMessageForEvent(ev);
let msg = this.notificationMessageForEvent(ev);
if (!msg) return;
var title;
if (!ev.sender || room.name == ev.sender.name) {
let title;
if (!ev.sender || room.name === ev.sender.name) {
title = room.name;
// notificationMessageForEvent includes sender,
// but we already have the sender here
if (ev.getContent().body) msg = ev.getContent().body;
} else if (ev.getType() == 'm.room.member') {
} else if (ev.getType() === 'm.room.member') {
// context is all in the message here, we don't need
// to display sender info
title = room.name;
@ -68,7 +72,7 @@ var Notifier = {
if (ev.getContent().body) msg = ev.getContent().body;
}
var avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
ev.sender, 40, 40, 'crop'
) : null;
@ -83,7 +87,7 @@ var Notifier = {
},
_playAudioNotification: function(ev, room) {
var e = document.getElementById("messageAudio");
const e = document.getElementById("messageAudio");
if (e) {
e.load();
e.play();
@ -95,7 +99,7 @@ var Notifier = {
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt);
MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt);
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
this.toolbarHidden = false;
this.isSyncing = false;
@ -104,7 +108,7 @@ var Notifier = {
stop: function() {
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt);
MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt);
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
}
this.isSyncing = false;
@ -118,10 +122,13 @@ var Notifier = {
setEnabled: function(enable, callback) {
const plaf = PlatformPeg.get();
if (!plaf) return;
Analytics.trackEvent('Notifier', 'Set Enabled', enable);
// make sure that we persist the current setting audio_enabled setting
// before changing anything
if (global.localStorage) {
if(global.localStorage.getItem('audio_notifications_enabled') == null) {
if (global.localStorage.getItem('audio_notifications_enabled') === null) {
this.setAudioEnabled(this.isEnabled());
}
}
@ -131,6 +138,14 @@ var Notifier = {
plaf.requestNotificationPermission().done((result) => {
if (result !== 'granted') {
// The permission request was dismissed or denied
const description = result === 'denied'
? _t('Riot does not have permission to send you notifications - please check your browser settings')
: _t('Riot was not given permission to send notifications - please try again');
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createDialog(ErrorDialog, {
title: _t('Unable to enable Notifications'),
description,
});
return;
}
@ -141,7 +156,7 @@ var Notifier = {
if (callback) callback();
dis.dispatch({
action: "notifier_enabled",
value: true
value: true,
});
});
// clear the notifications_hidden flag, so that if notifications are
@ -152,7 +167,7 @@ var Notifier = {
global.localStorage.setItem('notifications_enabled', 'false');
dis.dispatch({
action: "notifier_enabled",
value: false
value: false,
});
}
},
@ -165,7 +180,7 @@ var Notifier = {
if (!global.localStorage) return true;
var enabled = global.localStorage.getItem('notifications_enabled');
const enabled = global.localStorage.getItem('notifications_enabled');
if (enabled === null) return true;
return enabled === 'true';
},
@ -173,12 +188,12 @@ var Notifier = {
setAudioEnabled: function(enable) {
if (!global.localStorage) return;
global.localStorage.setItem('audio_notifications_enabled',
enable ? 'true' : 'false');
enable ? 'true' : 'false');
},
isAudioEnabled: function(enable) {
if (!global.localStorage) return true;
var enabled = global.localStorage.getItem(
const enabled = global.localStorage.getItem(
'audio_notifications_enabled');
// default to true if the popups are enabled
if (enabled === null) return this.isEnabled();
@ -188,11 +203,13 @@ var Notifier = {
setToolbarHidden: function(hidden, persistent = true) {
this.toolbarHidden = hidden;
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
// XXX: why are we dispatching this here?
// this is nothing to do with notifier_enabled
dis.dispatch({
action: "notifier_enabled",
value: this.isEnabled()
value: this.isEnabled(),
});
// update the info to localStorage for persistent settings
@ -215,8 +232,7 @@ var Notifier = {
onSyncStateChange: function(state) {
if (state === "SYNCING") {
this.isSyncing = true;
}
else if (state === "STOPPED" || state === "ERROR") {
} else if (state === "STOPPED" || state === "ERROR") {
this.isSyncing = false;
}
},
@ -225,22 +241,23 @@ var Notifier = {
if (toStartOfTimeline) return;
if (!room) return;
if (!this.isSyncing) return; // don't alert for any messages initially
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) {
if (this.isEnabled()) {
this._displayPopupNotification(ev, room);
}
if (actions.tweaks.sound && this.isAudioEnabled()) {
PlatformPeg.get().loudNotification(ev, room);
this._playAudioNotification(ev, room);
}
}
},
onRoomReceipt: function(ev, room) {
if (room.getUnreadNotificationCount() == 0) {
if (room.getUnreadNotificationCount() === 0) {
// ideally we would clear each notification when it was read,
// but we have no way, given a read receipt, to know whether
// the receipt comes before or after an event, so we can't
@ -255,7 +272,7 @@ var Notifier = {
}
delete this.notifsByRoom[room.roomId];
}
}
},
};
if (!global.mxNotifier) {

View file

@ -23,8 +23,8 @@ limitations under the License.
* { key: $KEY, val: $VALUE, place: "add|del" }
*/
module.exports.getKeyValueArrayDiffs = function(before, after) {
var results = [];
var delta = {};
const results = [];
const delta = {};
Object.keys(before).forEach(function(beforeKey) {
delta[beforeKey] = delta[beforeKey] || 0; // init to 0 initially
delta[beforeKey]--; // keys present in the past have -ve values
@ -46,9 +46,9 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
results.push({ place: "del", key: muxedKey, val: beforeVal });
});
break;
case 0: // A mix of added/removed keys
case 0: {// A mix of added/removed keys
// compare old & new vals
var itemDelta = {};
const itemDelta = {};
before[muxedKey].forEach(function(beforeVal) {
itemDelta[beforeVal] = itemDelta[beforeVal] || 0;
itemDelta[beforeVal]--;
@ -68,9 +68,9 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
}
});
break;
}
default:
console.error("Calculated key delta of " + delta[muxedKey] +
" - this should never happen!");
console.error("Calculated key delta of " + delta[muxedKey] + " - this should never happen!");
break;
}
});
@ -79,8 +79,10 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
};
/**
* Shallow-compare two objects for equality: each key and value must be
* identical
* Shallow-compare two objects for equality: each key and value must be identical
* @param {Object} objA First object to compare against the second
* @param {Object} objB Second object to compare against the first
* @return {boolean} whether the two objects have same key=values
*/
module.exports.shallowEqual = function(objA, objB) {
if (objA === objB) {
@ -92,15 +94,15 @@ module.exports.shallowEqual = function(objA, objB) {
return false;
}
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
for (var i = 0; i < keysA.length; i++) {
var key = keysA[i];
for (let i = 0; i < keysA.length; i++) {
const key = keysA[i];
if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
return false;
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 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.
@ -22,4 +23,6 @@ export default {
CreateRoom: "create_room",
RoomDirectory: "room_directory",
UserView: "user_view",
GroupView: "group_view",
MyGroups: "my_groups",
};

View file

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var Matrix = require("matrix-js-sdk");
import * as Matrix from 'matrix-js-sdk';
import { _t } from './languageHandler';
/**
* Allows a user to reset their password on a homeserver.
@ -33,7 +34,7 @@ class PasswordReset {
constructor(homeserverUrl, identityUrl) {
this.client = Matrix.createClient({
baseUrl: homeserverUrl,
idBaseUrl: identityUrl
idBaseUrl: identityUrl,
});
this.clientSecret = this.client.generateClientSecret();
this.identityServerDomain = identityUrl.split("://")[1];
@ -52,8 +53,8 @@ class PasswordReset {
this.sessionId = res.sid;
return res;
}, function(err) {
if (err.errcode == 'M_THREEPID_NOT_FOUND') {
err.message = "This email address was not found";
if (err.errcode === 'M_THREEPID_NOT_FOUND') {
err.message = _t('This email address was not found');
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
@ -74,16 +75,15 @@ class PasswordReset {
threepid_creds: {
sid: this.sessionId,
client_secret: this.clientSecret,
id_server: this.identityServerDomain
}
id_server: this.identityServerDomain,
},
}, this.password).catch(function(err) {
if (err.httpStatus === 401) {
err.message = "Failed to verify email address: make sure you clicked the link in the email";
}
else if (err.httpStatus === 404) {
err.message = "Your email address does not appear to be associated with a Matrix ID on this Homeserver.";
}
else if (err.httpStatus) {
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
} else if (err.httpStatus === 404) {
err.message =
_t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.');
} else if (err.httpStatus) {
err.message += ` (Status ${err.httpStatus})`;
}
throw err;

View file

@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var MatrixClientPeg = require('./MatrixClientPeg');
var dis = require('./dispatcher');
var sdk = require('./index');
var Modal = require('./Modal');
import MatrixClientPeg from './MatrixClientPeg';
import dis from './dispatcher';
import { EventStatus } from 'matrix-js-sdk';
module.exports = {
@ -37,12 +35,10 @@ module.exports = {
},
resend: function(event) {
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
MatrixClientPeg.get().resendEvent(
event, room
).done(function(res) {
MatrixClientPeg.get().resendEvent(event, room).done(function(res) {
dis.dispatch({
action: 'message_sent',
event: event
event: event,
});
}, function(err) {
// XXX: temporary logging to try to diagnose
@ -58,7 +54,7 @@ module.exports = {
dis.dispatch({
action: 'message_send_failed',
event: event
event: event,
});
});
},
@ -66,7 +62,7 @@ module.exports = {
MatrixClientPeg.get().cancelPendingEvent(event);
dis.dispatch({
action: 'message_send_cancelled',
event: event
event: event,
});
},
};

View file

@ -16,6 +16,7 @@ import * as sdk from './index';
import * as emojione from 'emojione';
import {stateToHTML} from 'draft-js-export-html';
import {SelectionRange} from "./autocomplete/Autocompleter";
import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown';
const MARKDOWN_REGEX = {
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
@ -30,9 +31,26 @@ const USERNAME_REGEX = /@\S+:\S+/g;
const ROOM_REGEX = /#\S+:\S+/g;
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g');
export const contentStateToHTML = stateToHTML;
const ZWS_CODE = 8203;
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
export function stateToMarkdown(state) {
return __stateToMarkdown(state)
.replace(
ZWS, // draft-js-export-markdown adds these
''); // this is *not* a zero width space, trust me :)
}
export function HTMLtoContentState(html: string): ContentState {
export const contentStateToHTML = (contentState: ContentState) => {
return stateToHTML(contentState, {
inlineStyles: {
UNDERLINE: {
element: 'u'
}
}
});
};
export function htmlToContentState(html: string): ContentState {
return ContentState.createFromBlockArray(convertFromHTML(html));
}
@ -95,31 +113,6 @@ let emojiDecorator = {
* Returns a composite decorator which has access to provided scope.
*/
export function getScopedRTDecorators(scope: any): CompositeDecorator {
let MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
let usernameDecorator = {
strategy: (contentBlock, callback) => {
findWithRegex(USERNAME_REGEX, contentBlock, callback);
},
component: (props) => {
let member = scope.room.getMember(props.children[0].props.text);
// unused until we make these decorators immutable (autocomplete needed)
let name = member ? member.name : null;
let avatar = member ? <MemberAvatar member={member} width={16} height={16}/> : null;
return <span className="mx_UserPill">{avatar}{props.children}</span>;
}
};
let roomDecorator = {
strategy: (contentBlock, callback) => {
findWithRegex(ROOM_REGEX, contentBlock, callback);
},
component: (props) => {
return <span className="mx_RoomPill">{props.children}</span>;
}
};
// TODO Re-enable usernameDecorator and roomDecorator
return [emojiDecorator];
}
@ -146,9 +139,9 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
</a>
)
});
markdownDecorators.push(emojiDecorator);
return markdownDecorators;
// markdownDecorators.push(emojiDecorator);
// TODO Consider renabling "syntax highlighting" when we can do it properly
return [emojiDecorator];
}
/**
@ -286,3 +279,14 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor
return editorState;
}
export function hasMultiLineSelection(editorState: EditorState): boolean {
const selectionState = editorState.getSelection();
const anchorKey = selectionState.getAnchorKey();
const currentContent = editorState.getCurrentContent();
const currentContentBlock = currentContent.getBlockForKey(anchorKey);
const start = selectionState.getStartOffset();
const end = selectionState.getEndOffset();
const selectedText = currentContentBlock.getText().slice(start, end);
return selectedText.includes('\n');
}

34
src/Roles.js Normal file
View file

@ -0,0 +1,34 @@
/*
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 { _t } from './languageHandler';
export function levelRoleMap() {
return {
undefined: _t('Default'),
0: _t('User'),
50: _t('Moderator'),
100: _t('Admin'),
};
}
export function textualPowerLevel(level, userDefault) {
const LEVEL_ROLE_MAP = this.levelRoleMap();
if (LEVEL_ROLE_MAP[level]) {
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`);
} else {
return level;
}
}

View file

@ -19,8 +19,7 @@ limitations under the License.
function tsOfNewestEvent(room) {
if (room.timeline.length) {
return room.timeline[room.timeline.length - 1].getTs();
}
else {
} else {
return Number.MAX_SAFE_INTEGER;
}
}
@ -32,5 +31,5 @@ function mostRecentActivityFirst(roomList) {
}
module.exports = {
mostRecentActivityFirst: mostRecentActivityFirst
mostRecentActivityFirst,
};

View file

@ -16,7 +16,7 @@ limitations under the License.
import MatrixClientPeg from './MatrixClientPeg';
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
import q from 'q';
import Promise from 'bluebird';
export const ALL_MESSAGES_LOUD = 'all_messages_loud';
export const ALL_MESSAGES = 'all_messages';
@ -52,7 +52,7 @@ export function getRoomNotifsState(roomId) {
}
export function setRoomNotifsState(roomId, newState) {
if (newState == MUTE) {
if (newState === MUTE) {
return setRoomNotifsStateMuted(roomId);
} else {
return setRoomNotifsStateUnmuted(roomId, newState);
@ -80,14 +80,14 @@ function setRoomNotifsStateMuted(roomId) {
kind: 'event_match',
key: 'room_id',
pattern: roomId,
}
},
],
actions: [
'dont_notify',
]
],
}));
return q.all(promises);
return Promise.all(promises);
}
function setRoomNotifsStateUnmuted(roomId, newState) {
@ -99,16 +99,16 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
promises.push(cli.deletePushRule('global', 'override', overrideMuteRule.rule_id));
}
if (newState == 'all_messages') {
if (newState === 'all_messages') {
const roomRule = cli.getRoomPushRule('global', roomId);
if (roomRule) {
promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id));
}
} else if (newState == 'mentions_only') {
} else if (newState === 'mentions_only') {
promises.push(cli.addPushRule('global', 'room', roomId, {
actions: [
'dont_notify',
]
],
}));
// https://matrix.org/jira/browse/SPEC-400
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
@ -119,14 +119,14 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
{
set_tweak: 'sound',
value: 'default',
}
]
},
],
}));
// https://matrix.org/jira/browse/SPEC-400
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
}
return q.all(promises);
return Promise.all(promises);
}
function findOverrideMuteRule(roomId) {
@ -145,20 +145,10 @@ function isRuleForRoom(roomId, rule) {
return false;
}
const cond = rule.conditions[0];
if (
cond.kind == 'event_match' &&
cond.key == 'room_id' &&
cond.pattern == roomId
) {
return true;
}
return false;
return (cond.kind === 'event_match' && cond.key === 'room_id' && cond.pattern === roomId);
}
function isMuteRule(rule) {
return (
rule.actions.length == 1 &&
rule.actions[0] == 'dont_notify'
);
return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify');
}

View file

@ -15,8 +15,7 @@ limitations under the License.
*/
import MatrixClientPeg from './MatrixClientPeg';
import DMRoomMap from './utils/DMRoomMap';
import q from 'q';
import Promise from 'bluebird';
/**
* Given a room object, return the alias we should use for it,
@ -103,7 +102,7 @@ export function guessAndSetDMRoom(room, isDirect) {
*/
export function setDMRoom(roomId, userId) {
if (MatrixClientPeg.get().isGuest()) {
return q();
return Promise.resolve();
}
const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct');
@ -145,7 +144,18 @@ export function guessDMRoomTarget(room, me) {
let oldestTs;
let oldestUser;
// Pick the user who's been here longest (and isn't us)
// Pick the joined user who's been here longest (and isn't us),
for (const user of room.getJoinedMembers()) {
if (user.userId == me.userId) continue;
if (oldestTs === undefined || user.events.member.getTs() < oldestTs) {
oldestUser = user;
oldestTs = user.events.member.getTs();
}
}
if (oldestUser) return oldestUser;
// if there are no joined members other than us, use the oldest member
for (const user of room.currentState.getMembers()) {
if (user.userId == me.userId) continue;

View file

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

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var q = require("q");
import Promise from 'bluebird';
var request = require('browser-request');
var SdkConfig = require('./SdkConfig');
@ -39,7 +39,7 @@ class ScalarAuthClient {
// Returns a scalar_token string
getScalarToken() {
var tok = window.localStorage.getItem("mx_scalar_token");
if (tok) return q(tok);
if (tok) return Promise.resolve(tok);
// No saved token, so do the dance to get one. First, we
// need an openid bearer token from the HS.
@ -53,7 +53,7 @@ class ScalarAuthClient {
}
exchangeForScalarToken(openid_token_object) {
var defer = q.defer();
var defer = Promise.defer();
var scalar_rest_url = SdkConfig.get().integrations_rest_url;
request({
@ -76,10 +76,13 @@ class ScalarAuthClient {
return defer.promise;
}
getScalarInterfaceUrlForRoom(roomId) {
getScalarInterfaceUrlForRoom(roomId, screen) {
var url = SdkConfig.get().integrations_ui_url;
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
url += "&room_id=" + encodeURIComponent(roomId);
if (screen) {
url += '&screen=' + encodeURIComponent(screen);
}
return url;
}
@ -89,4 +92,3 @@ class ScalarAuthClient {
}
module.exports = ScalarAuthClient;

View file

@ -1,5 +1,6 @@
/*
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.
@ -17,7 +18,7 @@ limitations under the License.
/*
Listens for incoming postMessage requests from the integrations UI URL. The following API is exposed:
{
action: "invite" | "membership_state" | "bot_options" | "set_bot_options",
action: "invite" | "membership_state" | "bot_options" | "set_bot_options" | etc... ,
room_id: $ROOM_ID,
user_id: $USER_ID
// additional request fields
@ -94,6 +95,115 @@ 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
}
can_send_event
--------------
Check if the client can send the given event into the given room. If the client
is unable to do this, an error response is returned instead of 'response: false'.
Request:
- room_id is the room to do the check in.
- event_type is the event type which will be sent.
- is_state is true if the event to be sent is a state event.
Response:
true
Example:
{
action: "can_send_event",
is_state: false,
event_type: "m.room.message",
room_id: "!foo:bar",
response: true
}
set_widget
----------
Set a new widget in the room. Clobbers based on the ID.
Request:
- `room_id` (String) is the room to set the widget in.
- `widget_id` (String) is the ID of the widget to add (or replace if it already exists).
It can be an arbitrary UTF8 string and is purely for distinguishing between widgets.
- `url` (String) is the URL that clients should load in an iframe to run the widget.
All widgets must have a valid URL. If the URL is `null` (not `undefined`), the
widget will be removed from the room.
- `type` (String) is the type of widget, which is provided as a hint for matrix clients so they
can configure/lay out the widget in different ways. All widgets must have a type.
- `name` (String) is an optional human-readable string about the widget.
- `data` (Object) is some optional data about the widget, and can contain arbitrary key/value pairs.
Response:
{
success: true
}
Example:
{
action: "set_widget",
room_id: "!foo:bar",
widget_id: "abc123",
url: "http://widget.url",
type: "example",
response: {
success: true
}
}
get_widgets
-----------
Get a list of all widgets in the room. The response is an array
of state events.
Request:
- `room_id` (String) is the room to get the widgets in.
Response:
[
{
type: "im.vector.modular.widgets",
state_key: "wid1",
content: {
type: "grafana",
url: "https://grafanaurl",
name: "dashboard",
data: {key: "val"}
}
room_id: !foo:bar,
sender: "@alice:localhost"
}
]
Example:
{
action: "get_widgets",
room_id: "!foo:bar",
response: [
{
type: "im.vector.modular.widgets",
state_key: "wid1",
content: {
type: "grafana",
url: "https://grafanaurl",
name: "dashboard",
data: {key: "val"}
}
room_id: !foo:bar,
sender: "@alice:localhost"
}
]
}
membership_state AND bot_options
--------------------------------
Get the content of the "m.room.member" or "m.room.bot.options" state event respectively.
@ -125,6 +235,7 @@ const SdkConfig = require('./SdkConfig');
const MatrixClientPeg = require("./MatrixClientPeg");
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
const dis = require("./dispatcher");
import { _t } from './languageHandler';
function sendResponse(event, res) {
const data = JSON.parse(JSON.stringify(event.data));
@ -150,7 +261,7 @@ function inviteUser(event, roomId, userId) {
console.log(`Received request to invite ${userId} into room ${roomId}`);
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, "You need to be logged in.");
sendError(event, _t('You need to be logged in.'));
return;
}
const room = client.getRoom(roomId);
@ -170,10 +281,91 @@ function inviteUser(event, roomId, userId) {
success: true,
});
}, 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);
});
}
function setWidget(event, roomId) {
const widgetId = event.data.widget_id;
const widgetType = event.data.type;
const widgetUrl = event.data.url;
const widgetName = event.data.name; // optional
const widgetData = event.data.data; // optional
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
return;
}
// both adding/removing widgets need these checks
if (!widgetId || widgetUrl === undefined) {
sendError(event, _t("Unable to create widget."), new Error("Missing required widget fields."));
return;
}
if (widgetUrl !== null) { // if url is null it is being deleted, don't need to check name/type/etc
// check types of fields
if (widgetName !== undefined && typeof widgetName !== 'string') {
sendError(event, _t("Unable to create widget."), new Error("Optional field 'name' must be a string."));
return;
}
if (widgetData !== undefined && !(widgetData instanceof Object)) {
sendError(event, _t("Unable to create widget."), new Error("Optional field 'data' must be an Object."));
return;
}
if (typeof widgetType !== 'string') {
sendError(event, _t("Unable to create widget."), new Error("Field 'type' must be a string."));
return;
}
if (typeof widgetUrl !== 'string') {
sendError(event, _t("Unable to create widget."), new Error("Field 'url' must be a string or null."));
return;
}
}
let content = {
type: widgetType,
url: widgetUrl,
name: widgetName,
data: widgetData,
};
if (widgetUrl === null) { // widget is being deleted
content = {};
}
client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => {
sendResponse(event, {
success: true,
});
}, (err) => {
sendError(event, _t('Failed to send request.'), err);
});
}
function getWidgets(event, roomId) {
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;
}
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
// Only return widgets which have required fields
let widgetStateEvents = [];
stateEvents.forEach((ev) => {
if (ev.getContent().type && ev.getContent().url) {
widgetStateEvents.push(ev.event); // return the raw event
}
})
sendResponse(event, widgetStateEvents);
}
function setPlumbingState(event, roomId, status) {
if (typeof status !== 'string') {
throw new Error('Plumbing state status should be a string');
@ -181,7 +373,7 @@ function setPlumbingState(event, roomId, status) {
console.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`);
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, "You need to be logged in.");
sendError(event, _t('You need to be logged in.'));
return;
}
client.sendStateEvent(roomId, "m.room.plumbing", { status : status }).done(() => {
@ -189,7 +381,7 @@ function setPlumbingState(event, roomId, status) {
success: true,
});
}, (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 +389,7 @@ function setBotOptions(event, roomId, userId) {
console.log(`Received request to set options for bot ${userId} in room ${roomId}`);
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, "You need to be logged in.");
sendError(event, _t('You need to be logged in.'));
return;
}
client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => {
@ -205,20 +397,20 @@ function setBotOptions(event, roomId, userId) {
success: true,
});
}, (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) {
if (!(Number.isInteger(level) && level >= 0)) {
sendError(event, "Power level must be positive integer.");
sendError(event, _t('Power level must be positive integer.'));
return;
}
console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`);
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, "You need to be logged in.");
sendError(event, _t('You need to be logged in.'));
return;
}
@ -235,7 +427,7 @@ function setBotPower(event, roomId, userId, level) {
success: true,
});
}, (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 +447,66 @@ function botOptions(event, roomId, userId) {
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
}
function returnStateEvent(event, roomId, eventType, stateKey) {
function getMembershipCount(event, roomId) {
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, "You need to be logged in.");
sendError(event, _t('You need to be logged in.'));
return;
}
const room = client.getRoom(roomId);
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 canSendEvent(event, roomId) {
const evType = "" + event.data.event_type; // force stringify
const isState = Boolean(event.data.is_state);
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;
}
const me = client.credentials.userId;
const member = room.getMember(me);
if (!member || member.membership !== "join") {
sendError(event, _t('You are not in this room.'));
return;
}
let canSend = false;
if (isState) {
canSend = room.currentState.maySendStateEvent(evType, me);
}
else {
canSend = room.currentState.maySendEvent(evType, me);
}
if (!canSend) {
sendError(event, _t('You do not have permission to do that in this room.'));
return;
}
sendResponse(event, true);
}
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;
}
const stateEvent = room.currentState.getStateEvents(eventType, stateKey);
@ -300,7 +543,7 @@ const onMessage = function(event) {
// All strings start with the empty string, so for sanity return if the length
// of the event origin is 0.
let url = SdkConfig.get().integrations_ui_url;
if (event.origin.length === 0 || !url.startsWith(event.origin)) {
if (event.origin.length === 0 || !url.startsWith(event.origin) || !event.data.action) {
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
}
@ -313,13 +556,13 @@ const onMessage = function(event) {
const roomId = event.data.room_id;
const userId = event.data.user_id;
if (!roomId) {
sendError(event, "Missing room_id in request");
sendError(event, _t('Missing room_id in request'));
return;
}
let promise = Promise.resolve(currentRoomId);
if (!currentRoomId) {
if (!currentRoomAlias) {
sendError(event, "Must be viewing a room");
sendError(event, _t('Must be viewing a room'));
return;
}
// no room ID but there is an alias, look it up.
@ -331,21 +574,33 @@ const onMessage = function(event) {
promise.then((viewingRoomId) => {
if (roomId !== viewingRoomId) {
sendError(event, "Room " + roomId + " not visible");
sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
return;
}
// Getting join rules does not require userId
// These APIs don't require userId
if (event.data.action === "join_rules_state") {
getJoinRules(event, roomId);
return;
} else if (event.data.action === "set_plumbing_state") {
setPlumbingState(event, roomId, event.data.status);
return;
} else if (event.data.action === "get_membership_count") {
getMembershipCount(event, roomId);
return;
} else if (event.data.action === "set_widget") {
setWidget(event, roomId);
return;
} else if (event.data.action === "get_widgets") {
getWidgets(event, roomId);
return;
} else if (event.data.action === "can_send_event") {
canSendEvent(event, roomId);
return;
}
if (!userId) {
sendError(event, "Missing user_id in request");
sendError(event, _t('Missing user_id in request'));
return;
}
switch (event.data.action) {
@ -370,16 +625,31 @@ const onMessage = function(event) {
}
}, (err) => {
console.error(err);
sendError(event, "Failed to lookup current room.");
sendError(event, _t('Failed to lookup current room') + '.');
});
};
let listenerCount = 0;
module.exports = {
startListening: function() {
window.addEventListener("message", onMessage, false);
if (listenerCount === 0) {
window.addEventListener("message", onMessage, false);
}
listenerCount += 1;
},
stopListening: function() {
window.removeEventListener("message", onMessage);
listenerCount -= 1;
if (listenerCount === 0) {
window.removeEventListener("message", onMessage);
}
if (listenerCount < 0) {
// Make an error so we get a stack trace
const e = new Error(
"ScalarMessaging: mismatched startListening / stopListening detected." +
" Negative count"
);
console.error(e);
}
},
};

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var DEFAULTS = {
const DEFAULTS = {
// URL to a page we show in an iframe to configure integrations
integrations_ui_url: "https://scalar.vector.im/",
// Base URL to the REST interface of the integrations server
@ -30,8 +30,8 @@ class SdkConfig {
}
static put(cfg) {
var defaultKeys = Object.keys(DEFAULTS);
for (var i = 0; i < defaultKeys.length; ++i) {
const defaultKeys = Object.keys(DEFAULTS);
for (let i = 0; i < defaultKeys.length; ++i) {
if (cfg[defaultKeys[i]] === undefined) {
cfg[defaultKeys[i]] = DEFAULTS[defaultKeys[i]];
}

View file

@ -23,41 +23,46 @@ class Skinner {
if (this.components === null) {
throw new Error(
"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"+
" 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];
if (comp) {
return comp;
}
let comp = this.components[name];
// XXX: Temporarily also try 'views.' as we're currently
// leaving the 'views.' off views.
var comp = this.components['views.'+name];
if (comp) {
return comp;
if (!comp) {
comp = this.components['views.'+name];
}
throw new Error("No such component: "+name);
if (!comp) {
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) {
if (this.components !== null) {
throw new Error(
"Attempted to load a skin while a skin is already loaded"+
"If you want to change the active skin, call resetSkin first"
);
"If you want to change the active skin, call resetSkin first");
}
this.components = {};
var compKeys = Object.keys(skinObject.components);
for (var i = 0; i < compKeys.length; ++i) {
var comp = skinObject.components[compKeys[i]];
const compKeys = Object.keys(skinObject.components);
for (let i = 0; i < compKeys.length; ++i) {
const comp = skinObject.components[compKeys[i]];
this.addComponent(compKeys[i], comp);
}
}
addComponent(name, comp) {
var slot = name;
let slot = name;
if (comp.replaces !== undefined) {
if (comp.replaces.indexOf('.') > -1) {
slot = comp.replaces;

View file

@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var MatrixClientPeg = require("./MatrixClientPeg");
var dis = require("./dispatcher");
var Tinter = require("./Tinter");
import MatrixClientPeg from "./MatrixClientPeg";
import dis from "./dispatcher";
import Tinter from "./Tinter";
import sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal';
@ -41,58 +42,64 @@ class Command {
}
getUsage() {
return "Usage: " + this.getCommandWithArgs();
return _t('Usage') + ': ' + this.getCommandWithArgs();
}
}
var reject = function(msg) {
function reject(msg) {
return {
error: msg
error: msg,
};
};
}
var success = function(promise) {
function success(promise) {
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) {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
// TODO Don't explain this away, actually show a search UI here.
Modal.createDialog(ErrorDialog, {
title: "/ddg is not a command",
description: "To use it, just wait for autocomplete results to load and tab through them.",
title: _t('/ddg is not a command'),
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
});
return success();
}),
// Change your nickname
nick: new Command("nick", "<display_name>", function(room_id, args) {
nick: new Command("nick", "<display_name>", function(roomId, args) {
if (args) {
return success(
MatrixClientPeg.get().setDisplayName(args)
MatrixClientPeg.get().setDisplayName(args),
);
}
return reject(this.getUsage());
}),
// 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) {
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) {
Tinter.tint(matches[1], matches[4]);
var colorScheme = {};
const colorScheme = {};
colorScheme.primary_color = matches[1];
if (matches[4]) {
colorScheme.secondary_color = matches[4];
}
return success(
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
topic: new Command("topic", "<topic>", function(room_id, args) {
topic: new Command("topic", "<topic>", function(roomId, args) {
if (args) {
return success(
MatrixClientPeg.get().setRoomTopic(room_id, args)
MatrixClientPeg.get().setRoomTopic(roomId, args),
);
}
return reject(this.getUsage());
}),
// Invite a user
invite: new Command("invite", "<userId>", function(room_id, args) {
invite: new Command("invite", "<userId>", function(roomId, args) {
if (args) {
var matches = args.match(/^(\S+)$/);
const matches = args.match(/^(\S+)$/);
if (matches) {
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: new Command("join", "#alias:domain", function(room_id, args) {
join: new Command("join", "#alias:domain", function(roomId, args) {
if (args) {
var matches = args.match(/^(\S+)$/);
const matches = args.match(/^(\S+)$/);
if (matches) {
var room_alias = matches[1];
if (room_alias[0] !== '#') {
let roomAlias = matches[1];
if (roomAlias[0] !== '#') {
return reject(this.getUsage());
}
if (!room_alias.match(/:/)) {
room_alias += ':' + MatrixClientPeg.get().getDomain();
if (!roomAlias.match(/:/)) {
roomAlias += ':' + MatrixClientPeg.get().getDomain();
}
dis.dispatch({
action: 'view_room',
room_alias: room_alias,
room_alias: roomAlias,
auto_join: true,
});
@ -147,29 +154,29 @@ var commands = {
return reject(this.getUsage());
}),
part: new Command("part", "[#alias:domain]", function(room_id, args) {
var targetRoomId;
part: new Command("part", "[#alias:domain]", function(roomId, args) {
let targetRoomId;
if (args) {
var matches = args.match(/^(\S+)$/);
const matches = args.match(/^(\S+)$/);
if (matches) {
var room_alias = matches[1];
if (room_alias[0] !== '#') {
let roomAlias = matches[1];
if (roomAlias[0] !== '#') {
return reject(this.getUsage());
}
if (!room_alias.match(/:/)) {
room_alias += ':' + MatrixClientPeg.get().getDomain();
if (!roomAlias.match(/:/)) {
roomAlias += ':' + MatrixClientPeg.get().getDomain();
}
// Try to find a room with this alias
var rooms = MatrixClientPeg.get().getRooms();
for (var i = 0; i < rooms.length; i++) {
var aliasEvents = rooms[i].currentState.getStateEvents(
"m.room.aliases"
const rooms = MatrixClientPeg.get().getRooms();
for (let i = 0; i < rooms.length; i++) {
const aliasEvents = rooms[i].currentState.getStateEvents(
"m.room.aliases",
);
for (var j = 0; j < aliasEvents.length; j++) {
var aliases = aliasEvents[j].getContent().aliases || [];
for (var k = 0; k < aliases.length; k++) {
if (aliases[k] === room_alias) {
for (let j = 0; j < aliasEvents.length; j++) {
const aliases = aliasEvents[j].getContent().aliases || [];
for (let k = 0; k < aliases.length; k++) {
if (aliases[k] === roomAlias) {
targetRoomId = rooms[i].roomId;
break;
}
@ -178,27 +185,28 @@ var commands = {
}
if (targetRoomId) { break; }
}
}
if (!targetRoomId) {
return reject("Unrecognised room alias: " + room_alias);
if (!targetRoomId) {
return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
}
}
}
if (!targetRoomId) targetRoomId = room_id;
if (!targetRoomId) targetRoomId = roomId;
return success(
MatrixClientPeg.get().leave(targetRoomId).then(
function() {
dis.dispatch({action: 'view_next_room'});
})
function() {
dis.dispatch({action: 'view_next_room'});
},
),
);
}),
// 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) {
var matches = args.match(/^(\S+?)( +(.*))?$/);
const matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) {
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: new Command("ban", "<userId> [<reason>]", function(room_id, args) {
ban: new Command("ban", "<userId> [<reason>]", function(roomId, args) {
if (args) {
var matches = args.match(/^(\S+?)( +(.*))?$/);
const matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) {
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: new Command("unban", "<userId>", function(room_id, args) {
unban: new Command("unban", "<userId>", function(roomId, args) {
if (args) {
var matches = args.match(/^(\S+)$/);
const matches = args.match(/^(\S+)$/);
if (matches) {
// Reset the user membership to "leave" to unban him
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
op: new Command("op", "<userId> [<power level>]", function(room_id, args) {
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
if (args) {
var matches = args.match(/^(\S+?)( +(\d+))?$/);
var powerLevel = 50; // default power level for op
const matches = args.match(/^(\S+?)( +(\d+))?$/);
let powerLevel = 50; // default power level for op
if (matches) {
var user_id = matches[1];
const userId = matches[1];
if (matches.length === 4 && undefined !== matches[3]) {
powerLevel = parseInt(matches[3]);
}
if (powerLevel !== NaN) {
var room = MatrixClientPeg.get().getRoom(room_id);
if (!isNaN(powerLevel)) {
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) {
return reject("Bad room ID: " + room_id);
return reject("Bad room ID: " + roomId);
}
var powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", ""
const powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "",
);
return success(
MatrixClientPeg.get().setPowerLevel(
room_id, user_id, powerLevel, powerLevelEvent
)
roomId, userId, powerLevel, powerLevelEvent,
),
);
}
}
@ -262,32 +270,96 @@ var commands = {
}),
// 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) {
var matches = args.match(/^(\S+)$/);
const matches = args.match(/^(\S+)$/);
if (matches) {
var room = MatrixClientPeg.get().getRoom(room_id);
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) {
return reject("Bad room ID: " + room_id);
return reject("Bad room ID: " + roomId);
}
var powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", ""
const powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "",
);
return success(
MatrixClientPeg.get().setPowerLevel(
room_id, args, undefined, powerLevelEvent
)
roomId, args, undefined, powerLevelEvent,
),
);
}
}
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];
return success(
// Promise.resolve to handle transition from static result to promise; can be removed
// in future
Promise.resolve(MatrixClientPeg.get().getStoredDevice(userId, deviceId)).then((device) => {
if (!device) {
throw new Error(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`);
}
if (device.isVerified()) {
if (device.getFingerprint() === fingerprint) {
throw new Error(_t(`Device already verified!`));
} else {
throw new Error(_t(`WARNING: Device already verified, but keys do NOT MATCH!`));
}
}
if (device.getFingerprint() !== fingerprint) {
const fprint = device.getFingerprint();
throw new Error(
_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 MatrixClientPeg.get().setDeviceVerified(
userId, deviceId, true,
);
}).then(() => {
// 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 reject(this.getUsage());
}),
};
/* eslint-enable babel/no-invalid-this */
// helpful aliases
var aliases = {
j: "join"
const aliases = {
j: "join",
};
module.exports = {
@ -304,13 +376,13 @@ module.exports = {
// IRC-style commands
input = input.replace(/\s+$/, "");
if (input[0] === "/" && input[1] !== "/") {
var bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
var cmd, args;
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
let cmd;
let args;
if (bits) {
cmd = bits[1].substring(1).toLowerCase();
args = bits[3];
}
else {
} else {
cmd = input;
}
if (cmd === "me") return null;
@ -319,9 +391,8 @@ module.exports = {
}
if (commands[cmd]) {
return commands[cmd].run(roomId, args);
}
else {
return reject("Unrecognised command: " + input);
} else {
return reject(_t("Unrecognised command:") + ' ' + input);
}
}
return null; // not a command
@ -329,12 +400,12 @@ module.exports = {
getCommandList: function() {
// 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];
});
cmds.push(new Command("me", "<action>", function() {}));
cmds.push(new Command("markdown", "<on|off>", function() {}));
return cmds;
}
},
};

View file

@ -1,391 +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.
*/
import { Entry, MemberEntry, CommandEntry } from './TabCompleteEntries';
import SlashCommands from './SlashCommands';
import MatrixClientPeg from './MatrixClientPeg';
const DELAY_TIME_MS = 1000;
const KEY_TAB = 9;
const KEY_SHIFT = 16;
const KEY_WINDOWS = 91;
// NB: DO NOT USE \b its "words" are roman alphabet only!
//
// Capturing group containing the start
// of line or a whitespace char
// \_______________ __________Capturing group of 0 or more non-whitespace chars
// _|__ _|_ followed by the end of line
// / \/ \
const MATCH_REGEX = /(^|\s)(\S*)$/;
class TabComplete {
constructor(opts) {
opts.allowLooping = opts.allowLooping || false;
opts.autoEnterTabComplete = opts.autoEnterTabComplete || false;
opts.onClickCompletes = opts.onClickCompletes || false;
this.opts = opts;
this.completing = false;
this.list = []; // full set of tab-completable things
this.matchedList = []; // subset of completable things to loop over
this.currentIndex = 0; // index in matchedList currently
this.originalText = null; // original input text when tab was first hit
this.textArea = opts.textArea; // DOMElement
this.isFirstWord = false; // true if you tab-complete on the first word
this.enterTabCompleteTimerId = null;
this.inPassiveMode = false;
// Map tracking ordering of the room members.
// userId: integer, highest comes first.
this.memberTabOrder = {};
// monotonically increasing counter used for tracking ordering of members
this.memberOrderSeq = 0;
}
/**
* Call this when a a UI element representing a tab complete entry has been clicked
* @param {entry} The entry that was clicked
*/
onEntryClick(entry) {
if (this.opts.onClickCompletes) {
this.completeTo(entry);
}
}
loadEntries(room) {
this._makeEntries(room);
this._initSorting(room);
this._sortEntries();
}
onMemberSpoke(member) {
if (this.memberTabOrder[member.userId] === undefined) {
this.list.push(new MemberEntry(member));
}
this.memberTabOrder[member.userId] = this.memberOrderSeq++;
this._sortEntries();
}
/**
* @param {DOMElement}
*/
setTextArea(textArea) {
this.textArea = textArea;
}
/**
* @return {Boolean}
*/
isTabCompleting() {
// actually have things to tab over
return this.completing && this.matchedList.length > 1;
}
stopTabCompleting() {
this.completing = false;
this.currentIndex = 0;
this._notifyStateChange();
}
startTabCompleting(passive) {
this.originalText = this.textArea.value; // cache starting text
// grab the partial word from the text which we'll be tab-completing
var res = MATCH_REGEX.exec(this.originalText);
if (!res) {
this.matchedList = [];
return;
}
// ES6 destructuring; ignore first element (the complete match)
var [, boundaryGroup, partialGroup] = res;
if (partialGroup.length === 0 && passive) {
return;
}
this.isFirstWord = partialGroup.length === this.originalText.length;
this.completing = true;
this.currentIndex = 0;
this.matchedList = [
new Entry(partialGroup) // first entry is always the original partial
];
// find matching entries in the set of entries given to us
this.list.forEach((entry) => {
if (entry.text.toLowerCase().indexOf(partialGroup.toLowerCase()) === 0) {
this.matchedList.push(entry);
}
});
// console.log("calculated completions => %s", JSON.stringify(this.matchedList));
}
/**
* Do an auto-complete with the given word. This terminates the tab-complete.
* @param {Entry} entry The tab-complete entry to complete to.
*/
completeTo(entry) {
this.textArea.value = this._replaceWith(
entry.getFillText(), true, entry.getSuffix(this.isFirstWord)
);
this.stopTabCompleting();
// keep focus on the text area
this.textArea.focus();
}
/**
* @param {Number} numAheadToPeek Return *up to* this many elements.
* @return {Entry[]}
*/
peek(numAheadToPeek) {
if (this.matchedList.length === 0) {
return [];
}
var peekList = [];
// return the current match item and then one with an index higher, and
// so on until we've reached the requested limit. If we hit the end of
// the list of options we're done.
for (var i = 0; i < numAheadToPeek; i++) {
var nextIndex;
if (this.opts.allowLooping) {
nextIndex = (this.currentIndex + i) % this.matchedList.length;
}
else {
nextIndex = this.currentIndex + i;
if (nextIndex === this.matchedList.length) {
break;
}
}
peekList.push(this.matchedList[nextIndex]);
}
// console.log("Peek list(%s): %s", numAheadToPeek, JSON.stringify(peekList));
return peekList;
}
handleTabPress(passive, shiftKey) {
var wasInPassiveMode = this.inPassiveMode && !passive;
this.inPassiveMode = passive;
if (!this.completing) {
this.startTabCompleting(passive);
}
if (shiftKey) {
this.nextMatchedEntry(-1);
}
else {
// if we were in passive mode we got out of sync by incrementing the
// index to show the peek view but not set the text area. Therefore,
// we want to set the *current* index rather than the *next* index.
this.nextMatchedEntry(wasInPassiveMode ? 0 : 1);
}
this._notifyStateChange();
}
/**
* @param {DOMEvent} e
*/
onKeyDown(ev) {
if (!this.textArea) {
console.error("onKeyDown called before a <textarea> was set!");
return;
}
if (ev.keyCode !== KEY_TAB) {
// pressing any key (except shift, windows, cmd (OSX) and ctrl/alt combinations)
// aborts the current tab completion
if (this.completing && ev.keyCode !== KEY_SHIFT &&
!ev.metaKey && !ev.ctrlKey && !ev.altKey && ev.keyCode !== KEY_WINDOWS) {
// they're resuming typing; reset tab complete state vars.
this.stopTabCompleting();
}
// explicitly pressing any key except tab removes passive mode. Tab doesn't remove
// passive mode because handleTabPress needs to know when passive mode is toggling
// off so it can resync the textarea/peek list. If tab did remove passive mode then
// handleTabPress would never be able to tell when passive mode toggled off.
this.inPassiveMode = false;
// pressing any key at all (except tab) restarts the automatic tab-complete timer
if (this.opts.autoEnterTabComplete) {
const cachedText = ev.target.value;
clearTimeout(this.enterTabCompleteTimerId);
this.enterTabCompleteTimerId = setTimeout(() => {
if (this.completing) {
// If you highlight text and CTRL+X it, tab-completing will not be reset.
// This check makes sure that if something like a cut operation has been
// done, that we correctly refresh the tab-complete list. Normal backspace
// operations get caught by the stopTabCompleting() section above, but
// because the CTRL key is held, this does not execute for CTRL+X.
if (cachedText !== this.textArea.value) {
this.stopTabCompleting();
}
}
if (!this.completing) {
this.handleTabPress(true, false);
}
}, DELAY_TIME_MS);
}
return;
}
// ctrl-tab/alt-tab etc shouldn't trigger a complete
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
// tab key has been pressed at this point
this.handleTabPress(false, ev.shiftKey);
// prevent the default TAB operation (typically focus shifting)
ev.preventDefault();
}
/**
* Set the textarea to the next value in the matched list.
* @param {Number} offset Offset to apply *before* setting the next value.
*/
nextMatchedEntry(offset) {
if (this.matchedList.length === 0) {
return;
}
// work out the new index, wrapping if necessary.
this.currentIndex += offset;
if (this.currentIndex >= this.matchedList.length) {
this.currentIndex = 0;
}
else if (this.currentIndex < 0) {
this.currentIndex = this.matchedList.length - 1;
}
var isTransitioningToOriginalText = (
// impossible to transition if they've never hit tab
!this.inPassiveMode && this.currentIndex === 0
);
if (!this.inPassiveMode) {
// set textarea to this new value
this.textArea.value = this._replaceWith(
this.matchedList[this.currentIndex].getFillText(),
this.currentIndex !== 0, // don't suffix the original text!
this.matchedList[this.currentIndex].getSuffix(this.isFirstWord)
);
}
// visual display to the user that we looped - TODO: This should be configurable
if (isTransitioningToOriginalText) {
this.textArea.style["background-color"] = "#faa";
setTimeout(() => { // yay for lexical 'this'!
this.textArea.style["background-color"] = "";
}, 150);
if (!this.opts.allowLooping) {
this.stopTabCompleting();
}
}
else {
this.textArea.style["background-color"] = ""; // cancel blinks TODO: required?
}
}
_replaceWith(newVal, includeSuffix, suffix) {
// The regex to replace the input matches a character of whitespace AND
// the partial word. If we just use string.replace() with the regex it will
// replace the partial word AND the character of whitespace. We want to
// preserve whatever that character is (\n, \t, etc) so find out what it is now.
var boundaryChar;
var res = MATCH_REGEX.exec(this.originalText);
if (res) {
boundaryChar = res[1]; // the first captured group
}
if (boundaryChar === undefined) {
console.warn("Failed to find boundary char on text: '%s'", this.originalText);
boundaryChar = "";
}
suffix = suffix || "";
if (!includeSuffix) {
suffix = "";
}
var replacementText = boundaryChar + newVal + suffix;
return this.originalText.replace(MATCH_REGEX, function() {
return replacementText; // function form to avoid `$` special-casing
});
}
_notifyStateChange() {
if (this.opts.onStateChange) {
this.opts.onStateChange(this.completing);
}
}
_sortEntries() {
// largest comes first
const KIND_ORDER = {
command: 1,
member: 2,
};
this.list.sort((a, b) => {
const kindOrderDifference = KIND_ORDER[b.kind] - KIND_ORDER[a.kind];
if (kindOrderDifference != 0) {
return kindOrderDifference;
}
if (a.kind == 'member') {
let orderA = this.memberTabOrder[a.member.userId];
let orderB = this.memberTabOrder[b.member.userId];
if (orderA === undefined) orderA = -1;
if (orderB === undefined) orderB = -1;
return orderB - orderA;
}
// anything else we have no ordering for
return 0;
});
}
_makeEntries(room) {
const myUserId = MatrixClientPeg.get().credentials.userId;
const members = room.getJoinedMembers().filter(function(member) {
if (member.userId !== myUserId) return true;
});
this.list = MemberEntry.fromMemberList(members).concat(
CommandEntry.fromCommands(SlashCommands.getCommandList())
);
}
_initSorting(room) {
this.memberTabOrder = {};
this.memberOrderSeq = 0;
for (const ev of room.getLiveTimeline().getEvents()) {
this.memberTabOrder[ev.getSender()] = this.memberOrderSeq++;
}
}
}
module.exports = TabComplete;

View file

@ -1,125 +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.
*/
var sdk = require("./index");
class Entry {
constructor(text) {
this.text = text;
}
/**
* @return {string} The text to display in this entry.
*/
getText() {
return this.text;
}
/**
* @return {string} The text to insert into the input box. Most of the time
* this is the same as getText().
*/
getFillText() {
return this.text;
}
/**
* @return {ReactClass} Raw JSX
*/
getImageJsx() {
return null;
}
/**
* @return {?string} The unique key= prop for React dedupe
*/
getKey() {
return null;
}
/**
* @return {?string} The suffix to append to the tab-complete, or null to
* not do this.
*/
getSuffix(isFirstWord) {
return null;
}
/**
* Called when this entry is clicked.
*/
onClick() {
// NOP
}
}
class CommandEntry extends Entry {
constructor(cmd, cmdWithArgs) {
super(cmdWithArgs);
this.kind = 'command';
this.cmd = cmd;
}
getFillText() {
return this.cmd;
}
getKey() {
return this.getFillText();
}
getSuffix(isFirstWord) {
return " "; // force a space after the command.
}
}
CommandEntry.fromCommands = function(commandArray) {
return commandArray.map(function(cmd) {
return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs());
});
};
class MemberEntry extends Entry {
constructor(member) {
super((member.name || member.userId).replace(' (IRC)', ''));
this.member = member;
this.kind = 'member';
}
getImageJsx() {
var MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
return (
<MemberAvatar member={this.member} width={24} height={24} />
);
}
getKey() {
return this.member.userId;
}
getSuffix(isFirstWord) {
return isFirstWord ? ": " : " ";
}
}
MemberEntry.fromMemberList = function(members) {
return members.map(function(m) {
return new MemberEntry(m);
});
};
module.exports.Entry = Entry;
module.exports.MemberEntry = MemberEntry;
module.exports.CommandEntry = CommandEntry;

View file

@ -13,9 +13,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var MatrixClientPeg = require("./MatrixClientPeg");
var CallHandler = require("./CallHandler");
import MatrixClientPeg from "./MatrixClientPeg";
import CallHandler from "./CallHandler";
import { _t } from './languageHandler';
import * as Roles from './Roles';
function textForMemberEvent(ev) {
// XXX: SYJS-16 "sender is sometimes null for join messages"
@ -23,95 +24,103 @@ function textForMemberEvent(ev) {
var targetName = ev.target ? ev.target.name : ev.getStateKey();
var ConferenceHandler = CallHandler.getConferenceHandler();
var reason = ev.getContent().reason ? (
" Reason: " + ev.getContent().reason
_t('Reason') + ': ' + ev.getContent().reason
) : "";
switch (ev.getContent().membership) {
case 'invite':
var threePidContent = ev.getContent().third_party_invite;
if (threePidContent) {
if (threePidContent.display_name) {
return targetName + " accepted the invitation for " +
threePidContent.display_name + ".";
return _t('%(targetName)s accepted the invitation for %(displayName)s.', {targetName: targetName, displayName: threePidContent.display_name});
} else {
return targetName + " accepted an invitation.";
return _t('%(targetName)s accepted an invitation.', {targetName: targetName});
}
}
else {
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
return senderName + " requested a VoIP conference";
return _t('%(senderName)s requested a VoIP conference.', {senderName: senderName});
}
else {
return senderName + " invited " + targetName + ".";
return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName});
}
}
case 'ban':
return senderName + " banned " + targetName + "." + reason;
return _t(
'%(senderName)s banned %(targetName)s.',
{senderName: senderName, targetName: targetName}
) + ' ' + reason;
case 'join':
if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') {
if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) {
return ev.getSender() + " changed their display name from " +
ev.getPrevContent().displayname + " to " +
ev.getContent().displayname;
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});
} 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) {
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) {
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) {
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) {
return senderName + " set a profile picture";
return _t('%(senderName)s set a profile picture.', {senderName: senderName});
} else {
// hacky hack for https://github.com/vector-im/vector-web/issues/2020
return senderName + " rejoined the room.";
// suppress null rejoins
return '';
}
} else {
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
return "VoIP conference started";
return _t('VoIP conference started.');
}
else {
return targetName + " joined the room.";
return _t('%(targetName)s joined the room.', {targetName: targetName});
}
}
case 'leave':
if (ev.getSender() === ev.getStateKey()) {
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
return "VoIP conference finished";
return _t('VoIP conference finished.');
}
else if (ev.getPrevContent().membership === "invite") {
return targetName + " rejected the invitation.";
return _t('%(targetName)s rejected the invitation.', {targetName: targetName});
}
else {
return targetName + " left the room.";
return _t('%(targetName)s left the room.', {targetName: targetName});
}
}
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") {
return senderName + " kicked " + targetName + "." + reason;
return _t(
'%(senderName)s kicked %(targetName)s.',
{senderName: senderName, targetName: targetName}
) + ' ' + reason;
}
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 {
return targetName + " left the room.";
return _t('%(targetName)s left the room.', {targetName: targetName});
}
}
}
function textForTopicEvent(ev) {
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return senderDisplayName + ' changed the topic to "' + ev.getContent().topic + '"';
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {senderDisplayName: senderDisplayName, topic: ev.getContent().topic});
}
function textForRoomNameEvent(ev) {
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) {
@ -120,66 +129,123 @@ function textForMessageEvent(ev) {
if (ev.getContent().msgtype === "m.emote") {
message = "* " + senderDisplayName + " " + message;
} else if (ev.getContent().msgtype === "m.image") {
message = senderDisplayName + " sent an image.";
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName: senderDisplayName});
}
return message;
}
function textForCallAnswerEvent(event) {
var senderName = event.sender ? event.sender.name : "Someone";
var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)";
return senderName + " answered the call." + supported;
var senderName = event.sender ? event.sender.name : _t('Someone');
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return _t('%(senderName)s answered the call.', {senderName: senderName}) + ' ' + supported;
}
function textForCallHangupEvent(event) {
var senderName = event.sender ? event.sender.name : "Someone";
var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)";
return senderName + " ended the call." + supported;
const senderName = event.sender ? event.sender.name : _t('Someone');
const eventContent = event.getContent();
let reason = "";
if(!MatrixClientPeg.get().supportsVoip()) {
reason = _t('(not supported by this browser)');
} else if(eventContent.reason) {
if (eventContent.reason === "ice_failed") {
reason = _t('(could not connect media)');
} else if (eventContent.reason === "invite_timeout") {
reason = _t('(no answer)');
} else {
reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
}
}
return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason;
}
function textForCallInviteEvent(event) {
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?
var type = "voice";
if (event.getContent().offer && event.getContent().offer.sdp &&
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
type = "video";
}
var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)";
return senderName + " placed a " + type + " call." + supported;
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return _t('%(senderName)s placed a %(callType)s call.', {senderName: senderName, callType: type}) + ' ' + supported;
}
function textForThreePidInviteEvent(event) {
var senderName = event.sender ? event.sender.name : event.getSender();
return senderName + " sent an invitation to " + event.getContent().display_name +
" to join the room.";
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {senderName: senderName, targetDisplayName: event.getContent().display_name});
}
function textForHistoryVisibilityEvent(event) {
var senderName = event.sender ? event.sender.name : event.getSender();
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") {
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") {
text += "all room members, from the point they joined.";
text += _t('all room members, from the point they joined') + '.';
}
else if (vis === "shared") {
text += "all room members.";
text += _t('all room members') + '.';
}
else if (vis === "world_readable") {
text += "anyone.";
text += _t('anyone') + '.';
}
else {
text += " unknown (" + vis + ")";
text += ' ' + _t('unknown') + ' (' + vis + ').';
}
return text;
}
function textForEncryptionEvent(event) {
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
function textForPowerEvent(event) {
const senderName = event.sender ? event.sender.name : event.getSender();
if (!event.getPrevContent() || !event.getPrevContent().users) {
return '';
}
const userDefault = event.getContent().users_default || 0;
// Construct set of userIds
let users = [];
Object.keys(event.getContent().users).forEach(
(userId) => {
if (users.indexOf(userId) === -1) users.push(userId);
}
);
Object.keys(event.getPrevContent().users).forEach(
(userId) => {
if (users.indexOf(userId) === -1) users.push(userId);
}
);
let diff = [];
// XXX: This is also surely broken for i18n
users.forEach((userId) => {
// Previous power level
const from = event.getPrevContent().users[userId];
// Current power level
const to = event.getContent().users[userId];
if (to !== from) {
diff.push(
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
userId: userId,
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
toPowerLevel: Roles.textualPowerLevel(to, userDefault)
})
);
}
});
if (!diff.length) {
return '';
}
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
senderName: senderName,
powerLevelDiffText: diff.join(", ")
});
}
var handlers = {
@ -193,6 +259,7 @@ var handlers = {
'm.room.third_party_invite': textForThreePidInviteEvent,
'm.room.history_visibility': textForHistoryVisibilityEvent,
'm.room.encryption': textForEncryptionEvent,
'm.room.power_levels': textForPowerEvent,
};
module.exports = {

View file

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

View file

@ -15,6 +15,8 @@ limitations under the License.
*/
var MatrixClientPeg = require('./MatrixClientPeg');
import UserSettingsStore from './UserSettingsStore';
import shouldHideEvent from './shouldHideEvent';
var sdk = require('./index');
module.exports = {
@ -25,7 +27,9 @@ module.exports = {
eventTriggersUnreadCount: function(ev) {
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
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;
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
return false;
@ -35,13 +39,33 @@ module.exports = {
},
doesRoomHaveUnreadMessages: function(room) {
var readUpToId = room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
var myUserId = MatrixClientPeg.get().credentials.userId;
// get the most recent read receipt sent by our account.
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
// despite the name of the method :((
var readUpToId = room.getEventReadUpTo(myUserId);
// as we don't send RRs for our own messages, make sure we special case that
// if *we* sent the last message into the room, we consider it not unread!
// Should fix: https://github.com/vector-im/riot-web/issues/3263
// https://github.com/vector-im/riot-web/issues/2427
// ...and possibly some of the others at
// https://github.com/vector-im/riot-web/issues/3363
if (room.timeline.length &&
room.timeline[room.timeline.length - 1].sender &&
room.timeline[room.timeline.length - 1].sender.userId === myUserId)
{
return false;
}
// this just looks at whatever history we have, which if we've only just started
// up probably won't be very much, so if the last couple of events are ones that
// don't count, we don't know if there are any events that do count between where
// we have and the read receipt. We could fetch more history to try & find out,
// but currently we just guess.
const syncedSettings = UserSettingsStore.getSyncedSettings();
// Loop through messages, starting with the most recent...
for (var i = room.timeline.length - 1; i >= 0; --i) {
var ev = room.timeline[i];
@ -51,7 +75,7 @@ module.exports = {
// that counts and we can stop looking because the user's read
// this and everything before.
return false;
} else if (this.eventTriggersUnreadCount(ev)) {
} else if (!shouldHideEvent(ev, syncedSettings) && this.eventTriggersUnreadCount(ev)) {
// We've found a message that counts before we hit
// the read marker, so this room is definitely unread.
return true;

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var dis = require("./dispatcher");
import dis from './dispatcher';
var MIN_DISPATCH_INTERVAL_MS = 500;
var CURRENTLY_ACTIVE_THRESHOLD_MS = 2000;
const MIN_DISPATCH_INTERVAL_MS = 500;
const CURRENTLY_ACTIVE_THRESHOLD_MS = 2000;
/**
* This class watches for user activity (moving the mouse or pressing a key)
@ -32,7 +32,7 @@ class UserActivity {
start() {
document.onmousedown = this._onUserActivity.bind(this);
document.onmousemove = this._onUserActivity.bind(this);
document.onkeypress = this._onUserActivity.bind(this);
document.onkeydown = this._onUserActivity.bind(this);
// can't use document.scroll here because that's only the document
// itself being scrolled. Need to use addEventListener's useCapture.
// also this needs to be the wheel event, not scroll, as scroll is
@ -50,7 +50,7 @@ class UserActivity {
stop() {
document.onmousedown = undefined;
document.onmousemove = undefined;
document.onkeypress = undefined;
document.onkeydown = undefined;
window.removeEventListener('wheel', this._onUserActivity.bind(this),
{ passive: true, capture: true });
}
@ -58,16 +58,15 @@ class UserActivity {
/**
* Return true if there has been user activity very recently
* (ie. within a few seconds)
* @returns {boolean} true if user is currently/very recently active
*/
userCurrentlyActive() {
return this.lastActivityAtTs > new Date().getTime() - CURRENTLY_ACTIVE_THRESHOLD_MS;
}
_onUserActivity(event) {
if (event.screenX && event.type == "mousemove") {
if (event.screenX === this.lastScreenX &&
event.screenY === this.lastScreenY)
{
if (event.screenX && event.type === "mousemove") {
if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) {
// mouse hasn't actually moved
return;
}
@ -79,28 +78,24 @@ class UserActivity {
if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL_MS) {
this.lastDispatchAtTs = this.lastActivityAtTs;
dis.dispatch({
action: 'user_activity'
action: 'user_activity',
});
if (!this.activityEndTimer) {
this.activityEndTimer = setTimeout(
this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS
);
this.activityEndTimer = setTimeout(this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS);
}
}
}
_onActivityEndTimer() {
var now = new Date().getTime();
var targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS;
const now = new Date().getTime();
const targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS;
if (now >= targetTime) {
dis.dispatch({
action: 'user_activity_end'
action: 'user_activity_end',
});
this.activityEndTimer = undefined;
} else {
this.activityEndTimer = setTimeout(
this._onActivityEndTimer.bind(this), targetTime - now
);
this.activityEndTimer = setTimeout(this._onActivityEndTimer.bind(this), targetTime - now);
}
}
}

View file

@ -14,26 +14,31 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var q = require("q");
var MatrixClientPeg = require("./MatrixClientPeg");
var Notifier = require("./Notifier");
import Promise from 'bluebird';
import MatrixClientPeg from './MatrixClientPeg';
import Notifier from './Notifier';
import { _t } from './languageHandler';
/*
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
*/
module.exports = {
export default {
LABS_FEATURES: [
{
name: 'New Composer & Autocomplete',
id: 'rich_text_editor',
name: "-",
id: 'matrix_apps',
default: false,
},
],
// horrible but it works. The locality makes this somewhat more palatable.
doTranslations: function() {
this.LABS_FEATURES[0].name = _t("Matrix Apps");
},
loadProfileInfo: function() {
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
return cli.getProfileInfo(cli.credentials.userId);
},
@ -43,8 +48,8 @@ module.exports = {
loadThreePids: function() {
if (MatrixClientPeg.get().isGuest()) {
return q({
threepids: []
return Promise.resolve({
threepids: [],
}); // guests can't poke 3pid endpoint
}
return MatrixClientPeg.get().getThreePids();
@ -73,19 +78,19 @@ module.exports = {
Notifier.setAudioEnabled(enable);
},
changePassword: function(old_password, new_password) {
var cli = MatrixClientPeg.get();
changePassword: function(oldPassword, newPassword) {
const cli = MatrixClientPeg.get();
var authDict = {
const authDict = {
type: 'm.login.password',
user: cli.credentials.userId,
password: old_password
password: oldPassword,
};
return cli.setPassword(authDict, new_password);
return cli.setPassword(authDict, newPassword);
},
/**
/*
* Returns the email pusher (pusher of type 'email') for a given
* email address. Email pushers all have the same app ID, so since
* pushers are unique over (app ID, pushkey), there will be at most
@ -95,8 +100,8 @@ module.exports = {
if (pushers === undefined) {
return undefined;
}
for (var i = 0; i < pushers.length; ++i) {
if (pushers[i].kind == 'email' && pushers[i].pushkey == address) {
for (let i = 0; i < pushers.length; ++i) {
if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
return pushers[i];
}
}
@ -110,7 +115,7 @@ module.exports = {
addEmailPusher: function(address, data) {
return MatrixClientPeg.get().setPusher({
kind: 'email',
app_id: "m.email",
app_id: 'm.email',
pushkey: address,
app_display_name: 'Email Notifications',
device_display_name: address,
@ -121,46 +126,46 @@ module.exports = {
},
getUrlPreviewsDisabled: function() {
var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls");
const event = MatrixClientPeg.get().getAccountData('org.matrix.preview_urls');
return (event && event.getContent().disable);
},
setUrlPreviewsDisabled: function(disabled) {
// FIXME: handle errors
return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", {
disable: disabled
return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', {
disable: disabled,
});
},
getSyncedSettings: function() {
var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings");
const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings');
return event ? event.getContent() : {};
},
getSyncedSetting: function(type, defaultValue = null) {
var settings = this.getSyncedSettings();
return settings.hasOwnProperty(type) ? settings[type] : null;
const settings = this.getSyncedSettings();
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
},
setSyncedSetting: function(type, value) {
var settings = this.getSyncedSettings();
const settings = this.getSyncedSettings();
settings[type] = value;
// FIXME: handle errors
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings);
},
getLocalSettings: function() {
var localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
const localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
return JSON.parse(localSettingsString);
},
getLocalSetting: function(type, defaultValue = null) {
var settings = this.getLocalSettings();
return settings.hasOwnProperty(type) ? settings[type] : null;
const settings = this.getLocalSettings();
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
},
setLocalSetting: function(type, value) {
var settings = this.getLocalSettings();
const settings = this.getLocalSettings();
settings[type] = value;
// FIXME: handle errors
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
@ -171,8 +176,8 @@ module.exports = {
if (MatrixClientPeg.get().isGuest()) return false;
if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) {
for (var i = 0; i < this.LABS_FEATURES.length; i++) {
var f = this.LABS_FEATURES[i];
for (let i = 0; i < this.LABS_FEATURES.length; i++) {
const f = this.LABS_FEATURES[i];
if (f.id === feature) {
return f.default;
}
@ -183,5 +188,5 @@ module.exports = {
setFeatureEnabled: function(feature: string, enabled: boolean) {
localStorage.setItem(`mx_labs_feature_${feature}`, enabled);
}
},
};

View file

@ -64,7 +64,7 @@ module.exports = React.createClass({
});
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
}
if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
if (oldNode && oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
oldNode.style.visibility = c.props.style.visibility;
}
self.children[c.key] = old;

View file

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

58
src/WidgetUtils.js Normal file
View file

@ -0,0 +1,58 @@
/*
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 MatrixClientPeg from './MatrixClientPeg';
export default class WidgetUtils {
/* Returns true if user is able to send state events to modify widgets in this room
* @param roomId -- The ID of the room to check
* @return Boolean -- true if the user can modify widgets in this room
* @throws Error -- specifies the error reason
*/
static canUserModifyWidgets(roomId) {
if (!roomId) {
console.warn('No room ID specified');
return false;
}
const client = MatrixClientPeg.get();
if (!client) {
console.warn('User must be be logged in');
return false;
}
const room = client.getRoom(roomId);
if (!room) {
console.warn(`Room ID ${roomId} is not recognised`);
return false;
}
const me = client.credentials.userId;
if (!me) {
console.warn('Failed to get user ID');
return false;
}
const member = room.getMember(me);
if (!member || member.membership !== "join") {
console.warn(`User ${me} is not in room ${roomId}`);
return false;
}
return room.currentState.maySendStateEvent('im.vector.modular.widgets', me);
}
}

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
var React = require("react");
import { _t } from '../../../languageHandler';
var sdk = require('../../../index');
var MatrixClientPeg = require("../../../MatrixClientPeg");
@ -27,23 +28,31 @@ module.exports = React.createClass({
},
getInitialState: function() {
return { device: this.refreshDevice() };
return { device: null };
},
componentWillMount: function() {
this._unmounted = false;
var client = MatrixClientPeg.get();
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
// no need to redownload keys if we already have the device
if (this.state.device) {
return;
}
client.downloadKeys([this.props.event.getSender()], true).done(()=>{
// first try to load the device from our store.
//
this.refreshDevice().then((dev) => {
if (dev) {
return dev;
}
// tell the client to try to refresh the device list for this user
return client.downloadKeys([this.props.event.getSender()], true).then(() => {
return this.refreshDevice();
});
}).then((dev) => {
if (this._unmounted) {
return;
}
this.setState({ device: this.refreshDevice() });
this.setState({ device: dev });
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
}, (err)=>{
console.log("Error downloading devices", err);
});
@ -58,12 +67,16 @@ module.exports = React.createClass({
},
refreshDevice: function() {
return MatrixClientPeg.get().getEventSenderDeviceInfo(this.props.event);
// Promise.resolve to handle transition from static result to promise; can be removed
// in future
return Promise.resolve(MatrixClientPeg.get().getEventSenderDeviceInfo(this.props.event));
},
onDeviceVerificationChanged: function(userId, device) {
if (userId == this.props.event.getSender()) {
this.setState({ device: this.refreshDevice() });
this.refreshDevice().then((dev) => {
this.setState({ device: dev });
});
}
},
@ -78,33 +91,33 @@ module.exports = React.createClass({
_renderDeviceInfo: function() {
var device = this.state.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()) {
verificationStatus = (<b>Blacklisted</b>);
verificationStatus = (<b>{ _t('Blacklisted') }</b>);
} else if (device.isVerified()) {
verificationStatus = "verified";
verificationStatus = _t('verified');
}
return (
<table>
<tbody>
<tr>
<td>Name</td>
<td>{ _t('Name') }</td>
<td>{ device.getDisplayName() }</td>
</tr>
<tr>
<td>Device ID</td>
<td>{ _t('Device ID') }</td>
<td><code>{ device.deviceId }</code></td>
</tr>
<tr>
<td>Verification</td>
<td>{ _t('Verification') }</td>
<td>{ verificationStatus }</td>
</tr>
<tr>
<td>Ed25519 fingerprint</td>
<td>{ _t('Ed25519 fingerprint') }</td>
<td><code>{device.getFingerprint()}</code></td>
</tr>
</tbody>
@ -119,32 +132,32 @@ module.exports = React.createClass({
<table>
<tbody>
<tr>
<td>User ID</td>
<td>{ _t('User ID') }</td>
<td>{ event.getSender() }</td>
</tr>
<tr>
<td>Curve25519 identity key</td>
<td><code>{ event.getSenderKey() || <i>none</i> }</code></td>
<td>{ _t('Curve25519 identity key') }</td>
<td><code>{ event.getSenderKey() || <i>{ _t('none') }</i> }</code></td>
</tr>
<tr>
<td>Claimed Ed25519 fingerprint key</td>
<td><code>{ event.getKeysClaimed().ed25519 || <i>none</i> }</code></td>
<td>{ _t('Claimed Ed25519 fingerprint key') }</td>
<td><code>{ event.getKeysClaimed().ed25519 || <i>{ _t('none') }</i> }</code></td>
</tr>
<tr>
<td>Algorithm</td>
<td>{ event.getWireContent().algorithm || <i>unencrypted</i> }</td>
<td>{ _t('Algorithm') }</td>
<td>{ event.getWireContent().algorithm || <i>{ _t('unencrypted') }</i> }</td>
</tr>
{
event.getContent().msgtype === 'm.bad.encrypted' ? (
<tr>
<td>Decryption error</td>
<td>{ _t('Decryption error') }</td>
<td>{ event.getContent().body }</td>
</tr>
) : null
}
<tr>
<td>Session ID</td>
<td><code>{ event.getWireContent().session_id || <i>none</i> }</code></td>
<td>{ _t('Session ID') }</td>
<td><code>{ event.getWireContent().session_id || <i>{ _t('none') }</i> }</code></td>
</tr>
</tbody>
</table>
@ -166,18 +179,18 @@ module.exports = React.createClass({
return (
<div className="mx_EncryptedEventDialog" onKeyDown={ this.onKeyDown }>
<div className="mx_Dialog_title">
End-to-end encryption information
{ _t('End-to-end encryption information') }
</div>
<div className="mx_Dialog_content">
<h4>Event information</h4>
<h4>{ _t('Event information') }</h4>
{this._renderEventInfo()}
<h4>Sender device information</h4>
<h4>{ _t('Sender device information') }</h4>
{this._renderDeviceInfo()}
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={ this.props.onFinished } autoFocus={ true }>
OK
{ _t('OK') }
</button>
{buttons}
</div>

View file

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

View file

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

View file

@ -1,8 +1,25 @@
/*
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 type {Completion, SelectionRange} from './Autocompleter';
export default class AutocompleteProvider {
constructor(commandRegex?: RegExp, fuseOpts?: any) {
constructor(commandRegex?: RegExp) {
if (commandRegex) {
if (!commandRegex.global) {
throw new Error('commandRegex must have global flag set');

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
import type {Component} from 'react';
@ -6,7 +22,7 @@ import DuckDuckGoProvider from './DuckDuckGoProvider';
import RoomProvider from './RoomProvider';
import UserProvider from './UserProvider';
import EmojiProvider from './EmojiProvider';
import Q from 'q';
import Promise from 'bluebird';
export type SelectionRange = {
start: number,
@ -18,6 +34,9 @@ export type Completion = {
component: ?Component,
range: SelectionRange,
command: ?string,
// If provided, apply a LINK entity to the completion with the
// data = { url: href }.
href: ?string,
};
const PROVIDERS = [
@ -36,28 +55,31 @@ export async function getCompletions(query: string, selection: SelectionRange, f
otherwise, we run into a condition where new completions are displayed
while the user is interacting with the list, which makes it difficult
to predict whether an action will actually do what is intended
It ends up containing a list of Q promise states, which are objects with
state (== "fulfilled" || "rejected") and value. */
const completionsList = await Q.allSettled(
PROVIDERS.map(provider => {
return Q(provider.getCompletions(query, selection, force))
.timeout(PROVIDER_COMPLETION_TIMEOUT);
})
*/
const completionsList = await Promise.all(
// Array of inspections of promises that might timeout. Instead of allowing a
// single timeout to reject the Promise.all, reflect each one and once they've all
// settled, filter for the fulfilled ones
PROVIDERS.map((provider) => {
return provider
.getCompletions(query, selection, force)
.timeout(PROVIDER_COMPLETION_TIMEOUT)
.reflect();
}),
);
return completionsList
.filter(completion => completion.state === "fulfilled")
.map((completionsState, i) => {
return {
completions: completionsState.value,
provider: PROVIDERS[i],
return completionsList.filter(
(inspection) => inspection.isFulfilled(),
).map((completionsState, i) => {
return {
completions: completionsState.value(),
provider: PROVIDERS[i],
/* the currently matched "command" the completer tried to complete
* we pass this through so that Autocomplete can figure out when to
* re-show itself once hidden.
*/
command: PROVIDERS[i].getCurrentCommand(query, selection, force),
};
});
/* the currently matched "command" the completer tried to complete
* we pass this through so that Autocomplete can figure out when to
* re-show itself once hidden.
*/
command: PROVIDERS[i].getCurrentCommand(query, selection, force),
};
});
}

View file

@ -1,8 +1,28 @@
/*
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 { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import Fuse from 'fuse.js';
import FuzzyMatcher from './FuzzyMatcher';
import {TextualCompletion} from './Components';
// TODO merge this with the factory mechanics of SlashCommands?
// 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 = [
{
command: '/me',
@ -14,6 +34,16 @@ const COMMANDS = [
args: '<user-id> [reason]',
description: 'Bans user with given id',
},
{
command: '/unban',
args: '<user-id>',
description: 'Unbans user with given id',
},
{
command: '/op',
args: '<user-id> [<power-level>]',
description: 'Define the power level of a user',
},
{
command: '/deop',
args: '<user-id>',
@ -29,6 +59,16 @@ const COMMANDS = [
args: '<room-alias>',
description: 'Joins room with given alias',
},
{
command: '/part',
args: '[<room-alias>]',
description: 'Leave room',
},
{
command: '/topic',
args: '<topic>',
description: 'Sets the room topic',
},
{
command: '/kick',
args: '<user-id> [reason]',
@ -43,32 +83,43 @@ const COMMANDS = [
command: '/ddg',
args: '<query>',
description: 'Searches DuckDuckGo for results',
}
},
{
command: '/tint',
args: '<color1> [<color2>]',
description: 'Changes colour scheme of current room',
},
{
command: '/verify',
args: '<user-id> <device-id> <device-signing-key>',
description: 'Verifies a user, device, and pubkey tuple',
},
// Omitting `/markdown` as it only seems to apply to OldComposer
];
let COMMAND_RE = /(^\/\w*)/g;
const COMMAND_RE = /(^\/\w*)/g;
let instance = null;
export default class CommandProvider extends AutocompleteProvider {
constructor() {
super(COMMAND_RE);
this.fuse = new Fuse(COMMANDS, {
this.matcher = new FuzzyMatcher(COMMANDS, {
keys: ['command', 'args', 'description'],
});
}
async getCompletions(query: string, selection: {start: number, end: number}) {
let completions = [];
let {command, range} = this.getCurrentCommand(query, selection);
const {command, range} = this.getCurrentCommand(query, selection);
if (command) {
completions = this.fuse.search(command[0]).map(result => {
completions = this.matcher.match(command[0]).map((result) => {
return {
completion: result.command + ' ',
component: (<TextualCompletion
title={result.command}
subtitle={result.args}
description={result.description}
description={ _t(result.description) }
/>),
range,
};
@ -78,12 +129,11 @@ export default class CommandProvider extends AutocompleteProvider {
}
getName() {
return '*️⃣ Commands';
return '*️⃣ ' + _t('Commands');
}
static getInstance(): CommandProvider {
if (instance == null)
{instance = new CommandProvider();}
if (instance === null) instance = new CommandProvider();
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 ReactDOM from 'react-dom';
import classNames from 'classnames';
/* 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 { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import 'whatwg-fetch';
@ -75,7 +93,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
}
getName() {
return '🔍 Results from DuckDuckGo';
return '🔍 ' + _t('Results from DuckDuckGo');
}
static getInstance(): DuckDuckGoProvider {

View file

@ -1,30 +1,134 @@
/*
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 { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
import Fuse from 'fuse.js';
import {emojioneList, shortnameToImage, shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
import FuzzyMatcher from './FuzzyMatcher';
import sdk from '../index';
import {PillCompletion} from './Components';
import type {SelectionRange, Completion} from './Autocompleter';
import _uniq from 'lodash/uniq';
import _sortBy from 'lodash/sortBy';
const EMOJI_REGEX = /:\w*:?/g;
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
import EmojiData from '../stripped-emoji.json';
const LIMIT = 20;
const CATEGORY_ORDER = [
'people',
'food',
'objects',
'activity',
'nature',
'travel',
'flags',
'regional',
'symbols',
'modifier',
];
// Match for ":wink:" or ascii-style ";-)" provided by emojione
// (^|\s|(emojiUnicode)) to make sure we're either at the start of the string or there's a
// whitespace character or an emoji before the emoji. The reason for unicodeRegexp is
// that we need to support inputting multiple emoji with no space between them.
const EMOJI_REGEX = new RegExp('(?:^|\\s|' + unicodeRegexp + ')(' + asciiRegexp + '|:\\w*:?)$', 'g');
// We also need to match the non-zero-length prefixes to remove them from the final match,
// and update the range so that we don't replace the whitespace or the previous emoji.
const MATCH_PREFIX_REGEX = new RegExp('(\\s|' + unicodeRegexp + ')');
const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort(
(a, b) => {
if (a.category === b.category) {
return a.emoji_order - b.emoji_order;
}
return CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category);
},
).map((a, index) => {
return {
name: a.name,
shortname: a.shortname,
aliases_ascii: a.aliases_ascii ? a.aliases_ascii.join(' ') : '',
// Include the index so that we can preserve the original order
_orderBy: index,
};
});
let instance = null;
function score(query, space) {
const index = space.indexOf(query);
if (index === -1) {
return Infinity;
} else {
return index;
}
}
export default class EmojiProvider extends AutocompleteProvider {
constructor() {
super(EMOJI_REGEX);
this.fuse = new Fuse(EMOJI_SHORTNAMES);
this.matcher = new FuzzyMatcher(EMOJI_SHORTNAMES, {
keys: ['aliases_ascii', 'shortname'],
// For matching against ascii equivalents
shouldMatchWordsOnly: false,
});
this.nameMatcher = new FuzzyMatcher(EMOJI_SHORTNAMES, {
keys: ['name'],
// For removing punctuation
shouldMatchWordsOnly: true,
});
}
async getCompletions(query: string, selection: SelectionRange) {
const EmojiText = sdk.getComponent('views.elements.EmojiText');
let completions = [];
let {command, range} = this.getCurrentCommand(query, selection);
const {command, range} = this.getCurrentCommand(query, selection);
if (command) {
completions = this.fuse.search(command[0]).map(result => {
const shortname = EMOJI_SHORTNAMES[result];
let matchedString = command[0];
// Remove prefix of any length (single whitespace or unicode emoji)
const prefixMatch = MATCH_PREFIX_REGEX.exec(matchedString);
if (prefixMatch) {
matchedString = matchedString.slice(prefixMatch[0].length);
range.start += prefixMatch[0].length;
}
completions = this.matcher.match(matchedString);
// Do second match with shouldMatchWordsOnly in order to match against 'name'
completions = completions.concat(this.nameMatcher.match(matchedString));
const sorters = [];
// First, sort by score (Infinity if matchedString not in shortname)
sorters.push((c) => score(matchedString, c.shortname));
// If the matchedString is not empty, sort by length of shortname. Example:
// matchedString = ":bookmark"
// completions = [":bookmark:", ":bookmark_tabs:", ...]
if (matchedString.length > 1) {
sorters.push((c) => c.shortname.length);
}
// Finally, sort by original ordering
sorters.push((c) => c._orderBy);
completions = _sortBy(_uniq(completions), sorters);
completions = completions.map((result) => {
const {shortname} = result;
const unicode = shortnameToUnicode(shortname);
return {
completion: unicode,
@ -33,13 +137,13 @@ export default class EmojiProvider extends AutocompleteProvider {
),
range,
};
}).slice(0, 8);
}).slice(0, LIMIT);
}
return completions;
}
getName() {
return '😃 Emoji';
return '😃 ' + _t('Emoji');
}
static getInstance() {

View file

@ -0,0 +1,107 @@
/*
Copyright 2017 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 Levenshtein from 'liblevenshtein';
//import _at from 'lodash/at';
//import _flatMap from 'lodash/flatMap';
//import _sortBy from 'lodash/sortBy';
//import _sortedUniq from 'lodash/sortedUniq';
//import _keys from 'lodash/keys';
//
//class KeyMap {
// keys: Array<String>;
// objectMap: {[String]: Array<Object>};
// priorityMap: {[String]: number}
//}
//
//const DEFAULT_RESULT_COUNT = 10;
//const DEFAULT_DISTANCE = 5;
// FIXME Until Fuzzy matching works better, we use prefix matching.
import PrefixMatcher from './QueryMatcher';
export default PrefixMatcher;
//class FuzzyMatcher { // eslint-disable-line no-unused-vars
// /**
// * @param {object[]} objects the objects to perform a match on
// * @param {string[]} keys an array of keys within each object to match on
// * Keys can refer to object properties by name and as in JavaScript (for nested properties)
// *
// * To use, simply presort objects by required criteria, run through this function and create a FuzzyMatcher with the
// * resulting KeyMap.
// *
// * TODO: Handle arrays and objects (Fuse did this, RoomProvider uses it)
// * @return {KeyMap}
// */
// static valuesToKeyMap(objects: Array<Object>, keys: Array<String>): KeyMap {
// const keyMap = new KeyMap();
// const map = {};
// const priorities = {};
//
// objects.forEach((object, i) => {
// const keyValues = _at(object, keys);
// console.log(object, keyValues, keys);
// for (const keyValue of keyValues) {
// if (!map.hasOwnProperty(keyValue)) {
// map[keyValue] = [];
// }
// map[keyValue].push(object);
// }
// priorities[object] = i;
// });
//
// keyMap.objectMap = map;
// keyMap.priorityMap = priorities;
// keyMap.keys = _sortBy(_keys(map), [(value) => priorities[value]]);
// return keyMap;
// }
//
// constructor(objects: Array<Object>, options: {[Object]: Object} = {}) {
// this.options = options;
// this.keys = options.keys;
// this.setObjects(objects);
// }
//
// setObjects(objects: Array<Object>) {
// this.keyMap = FuzzyMatcher.valuesToKeyMap(objects, this.keys);
// console.log(this.keyMap.keys);
// this.matcher = new Levenshtein.Builder()
// .dictionary(this.keyMap.keys, true)
// .algorithm('transposition')
// .sort_candidates(false)
// .case_insensitive_sort(true)
// .include_distance(true)
// .maximum_candidates(this.options.resultCount || DEFAULT_RESULT_COUNT) // result count 0 doesn't make much sense
// .build();
// }
//
// match(query: String): Array<Object> {
// const candidates = this.matcher.transduce(query, this.options.distance || DEFAULT_DISTANCE);
// // TODO FIXME This is hideous. Clean up when possible.
// const val = _sortedUniq(_sortBy(_flatMap(candidates, (candidate) => {
// return this.keyMap.objectMap[candidate[0]].map((value) => {
// return {
// distance: candidate[1],
// ...value,
// };
// });
// }),
// [(candidate) => candidate.distance, (candidate) => this.keyMap.priorityMap[candidate]]));
// console.log(val);
// return val;
// }
//}

View file

@ -0,0 +1,112 @@
//@flow
/*
Copyright 2017 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 _at from 'lodash/at';
import _flatMap from 'lodash/flatMap';
import _sortBy from 'lodash/sortBy';
import _uniq from 'lodash/uniq';
import _keys from 'lodash/keys';
class KeyMap {
keys: Array<String>;
objectMap: {[String]: Array<Object>};
priorityMap = new Map();
}
export default class QueryMatcher {
/**
* @param {object[]} objects the objects to perform a match on
* @param {string[]} keys an array of keys within each object to match on
* Keys can refer to object properties by name and as in JavaScript (for nested properties)
*
* To use, simply presort objects by required criteria, run through this function and create a QueryMatcher with the
* resulting KeyMap.
*
* TODO: Handle arrays and objects (Fuse did this, RoomProvider uses it)
* @return {KeyMap}
*/
static valuesToKeyMap(objects: Array<Object>, keys: Array<String>): KeyMap {
const keyMap = new KeyMap();
const map = {};
objects.forEach((object, i) => {
const keyValues = _at(object, keys);
for (const keyValue of keyValues) {
if (!map.hasOwnProperty(keyValue)) {
map[keyValue] = [];
}
map[keyValue].push(object);
}
keyMap.priorityMap.set(object, i);
});
keyMap.objectMap = map;
keyMap.keys = _keys(map);
return keyMap;
}
constructor(objects: Array<Object>, options: {[Object]: Object} = {}) {
this.options = options;
this.keys = options.keys;
this.setObjects(objects);
// By default, we remove any non-alphanumeric characters ([^A-Za-z0-9_]) from the
// query and the value being queried before matching
if (this.options.shouldMatchWordsOnly === undefined) {
this.options.shouldMatchWordsOnly = true;
}
// By default, match anywhere in the string being searched. If enabled, only return
// matches that are prefixed with the query.
if (this.options.shouldMatchPrefix === undefined) {
this.options.shouldMatchPrefix = false;
}
}
setObjects(objects: Array<Object>) {
this.keyMap = QueryMatcher.valuesToKeyMap(objects, this.keys);
}
match(query: String): Array<Object> {
query = query.toLowerCase();
if (this.options.shouldMatchWordsOnly) {
query = query.replace(/[^\w]/g, '');
}
if (query.length === 0) {
return [];
}
const results = [];
this.keyMap.keys.forEach((key) => {
let resultKey = key.toLowerCase();
if (this.options.shouldMatchWordsOnly) {
resultKey = resultKey.replace(/[^\w]/g, '');
}
const index = resultKey.indexOf(query);
if (index !== -1 && (!this.options.shouldMatchPrefix || index === 0)) {
results.push({key, index});
}
});
return _uniq(_flatMap(_sortBy(results, (candidate) => {
return candidate.index;
}).map((candidate) => {
// return an array of objects (those given to setObjects) that have the given
// key as a property.
return this.keyMap.objectMap[candidate.key];
})));
}
}

View file

@ -1,56 +1,93 @@
/*
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 { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import MatrixClientPeg from '../MatrixClientPeg';
import Fuse from 'fuse.js';
import FuzzyMatcher from './FuzzyMatcher';
import {PillCompletion} from './Components';
import {getDisplayAliasForRoom} from '../Rooms';
import sdk from '../index';
import _sortBy from 'lodash/sortBy';
const ROOM_REGEX = /(?=#)(\S*)/g;
let instance = null;
function score(query, space) {
const index = space.indexOf(query);
if (index === -1) {
return Infinity;
} else {
return index;
}
}
export default class RoomProvider extends AutocompleteProvider {
constructor() {
super(ROOM_REGEX, {
keys: ['displayName', 'userId'],
});
this.fuse = new Fuse([], {
keys: ['name', 'roomId', 'aliases'],
super(ROOM_REGEX);
this.matcher = new FuzzyMatcher([], {
keys: ['displayedAlias', 'name'],
});
}
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
let client = MatrixClientPeg.get();
const client = MatrixClientPeg.get();
let completions = [];
const {command, range} = this.getCurrentCommand(query, selection, force);
if (command) {
// the only reason we need to do this is because Fuse only matches on properties
this.fuse.set(client.getRooms().filter(room => !!room).map(room => {
this.matcher.setObjects(client.getRooms().filter(
(room) => !!room && !!getDisplayAliasForRoom(room),
).map((room) => {
return {
room: room,
name: room.name,
aliases: room.getAliases(),
displayedAlias: getDisplayAliasForRoom(room),
};
}));
completions = this.fuse.search(command[0]).map(room => {
let displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
const matchedString = command[0];
completions = this.matcher.match(matchedString);
completions = _sortBy(completions, [
(c) => score(matchedString, c.displayedAlias),
(c) => c.displayedAlias.length,
]).map((room) => {
const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
return {
completion: displayAlias,
suffix: ' ',
href: 'https://matrix.to/#/' + displayAlias,
component: (
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
),
range,
};
}).filter(completion => !!completion.completion && completion.completion.length > 0).slice(0, 4);
})
.filter((completion) => !!completion.completion && completion.completion.length > 0)
.slice(0, 4);
}
return completions;
}
getName() {
return '💬 Rooms';
return '💬 ' + _t('Rooms');
}
static getInstance() {
@ -62,12 +99,8 @@ export default class RoomProvider extends AutocompleteProvider {
}
renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill">
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
{completions}
</div>;
}
shouldForceComplete(): boolean {
return true;
}
}

View file

@ -1,22 +1,47 @@
//@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 { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import Q from 'q';
import Fuse from 'fuse.js';
import {PillCompletion} from './Components';
import sdk from '../index';
import FuzzyMatcher from './FuzzyMatcher';
import _pull from 'lodash/pull';
import _sortBy from 'lodash/sortBy';
import MatrixClientPeg from '../MatrixClientPeg';
import type {Room, RoomMember} from 'matrix-js-sdk';
const USER_REGEX = /@\S*/g;
let instance = null;
export default class UserProvider extends AutocompleteProvider {
users: Array<RoomMember> = [];
constructor() {
super(USER_REGEX, {
keys: ['name', 'userId'],
keys: ['name'],
});
this.users = [];
this.fuse = new Fuse([], {
keys: ['name', 'userId'],
this.matcher = new FuzzyMatcher([], {
keys: ['name'],
shouldMatchPrefix: true,
});
}
@ -26,17 +51,12 @@ export default class UserProvider extends AutocompleteProvider {
let completions = [];
let {command, range} = this.getCurrentCommand(query, selection, force);
if (command) {
this.fuse.set(this.users);
completions = this.fuse.search(command[0]).map(user => {
let displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
let completion = displayName;
if (range.start === 0) {
completion += ': ';
} else {
completion += ' ';
}
completions = this.matcher.match(command[0]).map((user) => {
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
return {
completion,
completion: displayName,
suffix: range.start === 0 ? ': ' : ' ',
href: 'https://matrix.to/#/' + user.userId,
component: (
<PillCompletion
initialComponent={<MemberAvatar member={user} width={24} height={24}/>}
@ -45,17 +65,44 @@ export default class UserProvider extends AutocompleteProvider {
),
range,
};
}).slice(0, 4);
});
}
return completions;
}
getName() {
return '👥 Users';
return '👥 ' + _t('Users');
}
setUserList(users) {
this.users = users;
setUserListFromRoom(room: Room) {
const events = room.getLiveTimeline().getEvents();
const lastSpoken = {};
for(const event of events) {
lastSpoken[event.getSender()] = event.getTs();
}
const currentUserId = MatrixClientPeg.get().credentials.userId;
this.users = room.getJoinedMembers().filter((member) => {
if (member.userId !== currentUserId) return true;
});
this.users = _sortBy(this.users, (member) =>
1E20 - lastSpoken[member.userId] || 1E20,
);
this.matcher.setObjects(this.users);
}
onUserSpoke(user: RoomMember) {
if(user.userId === MatrixClientPeg.get().credentials.userId) return;
// Move the user that spoke to the front of the array
this.users.splice(
this.users.findIndex((user2) => user2.userId === user.userId), 1);
this.users = [user, ...this.users];
this.matcher.setObjects(this.users);
}
static getInstance(): UserProvider {
@ -66,7 +113,7 @@ export default class UserProvider extends AutocompleteProvider {
}
renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill">
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
{completions}
</div>;
}

View file

@ -1,255 +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$TextInputWithCheckboxDialog from './components/views/dialogs/TextInputWithCheckboxDialog';
views$dialogs$TextInputWithCheckboxDialog && (module.exports.components['views.dialogs.TextInputWithCheckboxDialog'] = views$dialogs$TextInputWithCheckboxDialog);
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$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$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$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$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';
var React = require("react");
var MatrixClientPeg = require("../../MatrixClientPeg");
var PresetValues = {
import React from 'react';
import { _t } from '../../languageHandler';
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
const PresetValues = {
PrivateChat: "private_chat",
PublicChat: "public_chat",
Custom: "custom",
};
var q = require('q');
var sdk = require('../../index');
module.exports = React.createClass({
displayName: 'CreateRoom',
@ -231,7 +231,7 @@ module.exports = React.createClass({
if (curr_phase == this.phases.ERROR) {
error_box = (
<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>
);
}
@ -246,29 +246,29 @@ module.exports = React.createClass({
return (
<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">
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder="Name"/> <br />
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder="Topic"/> <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={_t('Topic')}/> <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 />
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
<div>
<label>
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/>
Make this room private
{_t('Make this room private')}
</label>
</div>
<div>
<label>
<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>
</div>
<div className="mx_CreateRoom_encrypt">
<label>
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/>
Encrypt room
{_t('Encrypt room')}
</label>
</div>
<div>

View file

@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var ReactDOM = require("react-dom");
import React from 'react';
var Matrix = require("matrix-js-sdk");
var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
import Matrix from 'matrix-js-sdk';
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
import { _t, _tJsx } from '../../languageHandler';
/*
* Component which shows the filtered file using a TimelinePanel
@ -59,6 +58,8 @@ var FilePanel = React.createClass({
var client = MatrixClientPeg.get();
var room = client.getRoom(roomId);
this.noRoom = !room;
if (room) {
var filter = new Matrix.Filter(client.credentials.userId);
filter.setDefinition(
@ -82,13 +83,24 @@ var FilePanel = React.createClass({
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!");
}
},
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.
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
var Loader = sdk.getComponent("elements.Spinner");
@ -105,7 +117,7 @@ var FilePanel = React.createClass({
showUrlPreview = { false }
tileShape="file_grid"
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

@ -0,0 +1,531 @@
/*
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 PropTypes from 'prop-types';
import MatrixClientPeg from '../../MatrixClientPeg';
import sdk from '../../index';
import dis from '../../dispatcher';
import { sanitizedHtmlNode } from '../../HtmlUtils';
import { _t } from '../../languageHandler';
import AccessibleButton from '../views/elements/AccessibleButton';
import Modal from '../../Modal';
import classnames from 'classnames';
const RoomSummaryType = PropTypes.shape({
room_id: PropTypes.string.isRequired,
profile: PropTypes.shape({
name: PropTypes.string,
avatar_url: PropTypes.string,
canonical_alias: PropTypes.string,
}).isRequired,
});
const UserSummaryType = PropTypes.shape({
summaryInfo: PropTypes.shape({
user_id: PropTypes.string.isRequired,
}).isRequired,
});
const CategoryRoomList = React.createClass({
displayName: 'CategoryRoomList',
props: {
rooms: PropTypes.arrayOf(RoomSummaryType).isRequired,
category: PropTypes.shape({
profile: PropTypes.shape({
name: PropTypes.string,
}).isRequired,
}),
},
render: function() {
const roomNodes = this.props.rooms.map((r) => {
return <FeaturedRoom key={r.room_id} summaryInfo={r} />;
});
let catHeader = null;
if (this.props.category && this.props.category.profile) {
catHeader = <div className="mx_GroupView_featuredThings_category">{this.props.category.profile.name}</div>;
}
return <div>
{catHeader}
{roomNodes}
</div>;
},
});
const FeaturedRoom = React.createClass({
displayName: 'FeaturedRoom',
props: {
summaryInfo: RoomSummaryType.isRequired,
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'view_room',
room_alias: this.props.summaryInfo.profile.canonical_alias,
room_id: this.props.summaryInfo.room_id,
});
},
render: function() {
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
const oobData = {
roomId: this.props.summaryInfo.room_id,
avatarUrl: this.props.summaryInfo.profile.avatar_url,
name: this.props.summaryInfo.profile.name,
};
let permalink = null;
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
}
let roomNameNode = null;
if (permalink) {
roomNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.profile.name}</a>;
} else {
roomNameNode = <span>{this.props.summaryInfo.profile.name}</span>;
}
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
<RoomAvatar oobData={oobData} width={64} height={64} />
<div className="mx_GroupView_featuredThing_name">{roomNameNode}</div>
</AccessibleButton>;
},
});
const RoleUserList = React.createClass({
displayName: 'RoleUserList',
props: {
users: PropTypes.arrayOf(UserSummaryType).isRequired,
role: PropTypes.shape({
profile: PropTypes.shape({
name: PropTypes.string,
}).isRequired,
}),
},
render: function() {
const userNodes = this.props.users.map((u) => {
return <FeaturedUser key={u.user_id} summaryInfo={u} />;
});
let roleHeader = null;
if (this.props.role && this.props.role.profile) {
roleHeader = <div className="mx_GroupView_featuredThings_category">{this.props.role.profile.name}</div>;
}
return <div>
{roleHeader}
{userNodes}
</div>;
},
});
const FeaturedUser = React.createClass({
displayName: 'FeaturedUser',
props: {
summaryInfo: UserSummaryType.isRequired,
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'view_start_chat_or_reuse',
user_id: this.props.summaryInfo.user_id,
go_home_on_cancel: false,
});
},
render: function() {
// Add avatar once we get profile info inline in the summary response
//const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id;
const userNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.user_id}</a>;
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
<div className="mx_GroupView_featuredThing_name">{userNameNode}</div>
</AccessibleButton>;
},
});
export default React.createClass({
displayName: 'GroupView',
propTypes: {
groupId: PropTypes.string.isRequired,
},
getInitialState: function() {
return {
summary: null,
error: null,
editing: false,
saving: false,
uploadingAvatar: false,
};
},
componentWillMount: function() {
this._changeAvatarComponent = null;
this._loadGroupFromServer(this.props.groupId);
},
componentWillReceiveProps: function(newProps) {
if (this.props.groupId != newProps.groupId) {
this.setState({
summary: null,
error: null,
}, () => {
this._loadGroupFromServer(newProps.groupId);
});
}
},
_loadGroupFromServer: function(groupId) {
MatrixClientPeg.get().getGroupSummary(groupId).done((res) => {
this.setState({
summary: res,
error: null,
});
}, (err) => {
this.setState({
summary: null,
error: err,
});
});
},
_onEditClick: function() {
this.setState({
editing: true,
profileForm: Object.assign({}, this.state.summary.profile),
});
},
_onCancelClick: function() {
this.setState({
editing: false,
profileForm: null,
});
},
_onNameChange: function(e) {
const newProfileForm = Object.assign(this.state.profileForm, { name: e.target.value });
this.setState({
profileForm: newProfileForm,
});
},
_onShortDescChange: function(e) {
const newProfileForm = Object.assign(this.state.profileForm, { short_description: e.target.value });
this.setState({
profileForm: newProfileForm,
});
},
_onLongDescChange: function(e) {
const newProfileForm = Object.assign(this.state.profileForm, { long_description: e.target.value });
this.setState({
profileForm: newProfileForm,
});
},
_onAvatarSelected: function(ev) {
const file = ev.target.files[0];
if (!file) return;
this.setState({uploadingAvatar: true});
MatrixClientPeg.get().uploadContent(file).then((url) => {
const newProfileForm = Object.assign(this.state.profileForm, { avatar_url: url });
this.setState({
uploadingAvatar: false,
profileForm: newProfileForm,
});
}).catch((e) => {
this.setState({uploadingAvatar: false});
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to upload avatar image", e);
Modal.createDialog(ErrorDialog, {
title: _t('Error'),
description: _t('Failed to upload image'),
});
}).done();
},
_onSaveClick: function() {
this.setState({saving: true});
MatrixClientPeg.get().setGroupProfile(this.props.groupId, this.state.profileForm).then((result) => {
this.setState({
saving: false,
editing: false,
summary: null,
});
this._loadGroupFromServer(this.props.groupId);
}).catch((e) => {
this.setState({
saving: false,
});
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to save group profile", e);
Modal.createDialog(ErrorDialog, {
title: _t('Error'),
description: _t('Failed to update group'),
});
}).done();
},
_getFeaturedRoomsNode() {
const summary = this.state.summary;
if (summary.rooms_section.rooms.length == 0) return null;
const defaultCategoryRooms = [];
const categoryRooms = {};
summary.rooms_section.rooms.forEach((r) => {
if (r.category_id === null) {
defaultCategoryRooms.push(r);
} else {
let list = categoryRooms[r.category_id];
if (list === undefined) {
list = [];
categoryRooms[r.category_id] = list;
}
list.push(r);
}
});
let defaultCategoryNode = null;
if (defaultCategoryRooms.length > 0) {
defaultCategoryNode = <CategoryRoomList rooms={defaultCategoryRooms} />;
}
const categoryRoomNodes = Object.keys(categoryRooms).map((catId) => {
const cat = summary.rooms_section.categories[catId];
return <CategoryRoomList key={catId} rooms={categoryRooms[catId]} category={cat} />;
});
return <div className="mx_GroupView_featuredThings">
<div className="mx_GroupView_featuredThings_header">
{_t('Featured Rooms:')}
</div>
{defaultCategoryNode}
{categoryRoomNodes}
</div>;
},
_getFeaturedUsersNode() {
const summary = this.state.summary;
if (summary.users_section.users.length == 0) return null;
const noRoleUsers = [];
const roleUsers = {};
summary.users_section.users.forEach((u) => {
if (u.role_id === null) {
noRoleUsers.push(u);
} else {
let list = roleUsers[u.role_id];
if (list === undefined) {
list = [];
roleUsers[u.role_id] = list;
}
list.push(u);
}
});
let noRoleNode = null;
if (noRoleUsers.length > 0) {
noRoleNode = <RoleUserList users={noRoleUsers} />;
}
const roleUserNodes = Object.keys(roleUsers).map((roleId) => {
const role = summary.users_section.roles[roleId];
return <RoleUserList key={roleId} users={roleUsers[roleId]} role={role} />;
});
return <div className="mx_GroupView_featuredThings">
<div className="mx_GroupView_featuredThings_header">
{_t('Featured Users:')}
</div>
{noRoleNode}
{roleUserNodes}
</div>;
},
render: function() {
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
const Loader = sdk.getComponent("elements.Spinner");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
if (this.state.summary === null && this.state.error === null || this.state.saving) {
return <Loader />;
} else if (this.state.summary) {
const summary = this.state.summary;
let avatarNode;
let nameNode;
let shortDescNode;
let rightButtons;
let roomBody;
const headerClasses = {
mx_GroupView_header: true,
};
if (this.state.editing) {
let avatarImage;
if (this.state.uploadingAvatar) {
avatarImage = <Loader />;
} else {
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
avatarImage = <GroupAvatar groupId={this.props.groupId}
groupAvatarUrl={this.state.profileForm.avatar_url}
width={48} height={48} resizeMethod='crop'
/>;
}
avatarNode = (
<div className="mx_GroupView_avatarPicker">
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
{avatarImage}
</label>
<div className="mx_GroupView_avatarPicker_edit">
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
<img src="img/camera.svg"
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
width="17" height="15" />
</label>
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected}/>
</div>
</div>
);
nameNode = <input type="text"
value={this.state.profileForm.name}
onChange={this._onNameChange}
placeholder={_t('Group Name')}
tabIndex="1"
/>;
shortDescNode = <input type="text"
value={this.state.profileForm.short_description}
onChange={this._onShortDescChange}
placeholder={_t('Description')}
tabIndex="2"
/>;
rightButtons = <span>
<AccessibleButton className="mx_GroupView_saveButton mx_RoomHeader_textButton" onClick={this._onSaveClick}>
{_t('Save')}
</AccessibleButton>
<AccessibleButton className='mx_GroupView_cancelButton' onClick={this._onCancelClick}>
<img src="img/cancel.svg" className='mx_filterFlipColor'
width="18" height="18" alt={_t("Cancel")}/>
</AccessibleButton>
</span>;
roomBody = <div>
<textarea className="mx_GroupView_editLongDesc" value={this.state.profileForm.long_description}
onChange={this._onLongDescChange}
tabIndex="3"
/>
</div>;
} else {
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
avatarNode = <GroupAvatar
groupId={this.props.groupId}
groupAvatarUrl={groupAvatarUrl}
width={48} height={48}
/>;
if (summary.profile && summary.profile.name) {
nameNode = <div>
<span>{summary.profile.name}</span>
<span className="mx_GroupView_header_groupid">
({this.props.groupId})
</span>
</div>;
} else {
nameNode = <span>{this.props.groupId}</span>;
}
shortDescNode = <span>{summary.profile.short_description}</span>;
let description = null;
if (summary.profile && summary.profile.long_description) {
description = sanitizedHtmlNode(summary.profile.long_description);
}
roomBody = <div>
<div className="mx_GroupView_groupDesc">{description}</div>
{this._getFeaturedRoomsNode()}
{this._getFeaturedUsersNode()}
</div>;
// disabled until editing works
rightButtons = <AccessibleButton className="mx_GroupHeader_button"
onClick={this._onEditClick} title={_t("Edit Group")}
>
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
</AccessibleButton>;
headerClasses.mx_GroupView_header_view = true;
}
return (
<div className="mx_GroupView">
<div className={classnames(headerClasses)}>
<div className="mx_GroupView_header_leftCol">
<div className="mx_GroupView_header_avatar">
{avatarNode}
</div>
<div className="mx_GroupView_header_info">
<div className="mx_GroupView_header_name">
{nameNode}
</div>
<div className="mx_GroupView_header_shortDesc">
{shortDescNode}
</div>
</div>
</div>
<div className="mx_GroupView_header_rightCol">
{rightButtons}
</div>
</div>
{roomBody}
</div>
);
} else if (this.state.error) {
if (this.state.error.httpStatus === 404) {
return (
<div className="mx_GroupView_error">
Group {this.props.groupId} not found
</div>
);
} else {
let extraText;
if (this.state.error.errcode === 'M_UNRECOGNIZED') {
extraText = <div>{_t('This Home server does not support groups')}</div>;
}
return (
<div className="mx_GroupView_error">
Failed to load {this.props.groupId}
{extraText}
</div>
);
}
} else {
console.error("Invalid state for GroupView");
return <div />;
}
},
});

View file

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

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 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.
@ -17,11 +18,15 @@ limitations under the License.
import * as Matrix from 'matrix-js-sdk';
import React from 'react';
import UserSettingsStore from '../../UserSettingsStore';
import KeyCode from '../../KeyCode';
import Notifier from '../../Notifier';
import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler';
import sdk from '../../index';
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
@ -38,10 +43,13 @@ export default React.createClass({
propTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
page_type: React.PropTypes.string.isRequired,
onRoomIdResolved: React.PropTypes.func,
onRoomCreated: 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,
// and lots and lots of other stuff.
@ -62,6 +70,13 @@ export default React.createClass({
};
},
getInitialState: function() {
return {
// use compact timeline view
useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'),
};
},
componentWillMount: function() {
// stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient;
@ -70,11 +85,35 @@ export default React.createClass({
// RoomView.getScrollState()
this._scrollStateMap = {};
CallMediaHandler.loadDevices();
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() {
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) {
@ -88,6 +127,20 @@ export default React.createClass({
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) {
/*
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
@ -103,13 +156,20 @@ export default React.createClass({
}
*/
var handled = false;
let handled = false;
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
let ctrlCmdOnly;
if (isMac) {
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
} else {
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
}
switch (ev.keyCode) {
case KeyCode.UP:
case KeyCode.DOWN:
if (ev.altKey) {
var action = ev.keyCode == KeyCode.UP ?
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
let action = ev.keyCode == KeyCode.UP ?
'view_prev_room' : 'view_next_room';
dis.dispatch({action: action});
handled = true;
@ -118,17 +178,27 @@ export default React.createClass({
case KeyCode.PAGE_UP:
case KeyCode.PAGE_DOWN:
this._onScrollKeyPressed(ev);
handled = true;
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this._onScrollKeyPressed(ev);
handled = true;
}
break;
case KeyCode.HOME:
case KeyCode.END:
if (ev.ctrlKey) {
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this._onScrollKeyPressed(ev);
handled = true;
}
break;
case KeyCode.KEY_K:
if (ctrlCmdOnly) {
dis.dispatch({
action: 'focus_room_filter',
});
handled = true;
}
break;
}
if (handled) {
@ -142,54 +212,60 @@ export default React.createClass({
if (this.refs.roomView) {
this.refs.roomView.handleScrollKey(ev);
}
else if (this.refs.roomDirectory) {
this.refs.roomDirectory.handleScrollKey(ev);
}
},
render: function() {
var LeftPanel = sdk.getComponent('structures.LeftPanel');
var RightPanel = sdk.getComponent('structures.RightPanel');
var RoomView = sdk.getComponent('structures.RoomView');
var UserSettings = sdk.getComponent('structures.UserSettings');
var CreateRoom = sdk.getComponent('structures.CreateRoom');
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
var HomePage = sdk.getComponent('structures.HomePage');
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RightPanel = sdk.getComponent('structures.RightPanel');
const RoomView = sdk.getComponent('structures.RoomView');
const UserSettings = sdk.getComponent('structures.UserSettings');
const CreateRoom = sdk.getComponent('structures.CreateRoom');
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
const HomePage = sdk.getComponent('structures.HomePage');
const GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
var page_element;
var right_panel = '';
let page_element;
let right_panel = '';
switch (this.props.page_type) {
case PageTypes.RoomView:
page_element = <RoomView
ref='roomView'
roomAddress={this.props.currentRoomAlias || this.props.currentRoomId}
autoJoin={this.props.autoJoin}
onRoomIdResolved={this.props.onRoomIdResolved}
eventId={this.props.initialEventId}
onRegistered={this.props.onRegistered}
thirdPartyInvite={this.props.thirdPartyInvite}
oobData={this.props.roomOobData}
highlightedEventId={this.props.highlightedEventId}
eventPixelOffset={this.props.initialEventPixelOffset}
key={this.props.currentRoomAlias || this.props.currentRoomId}
key={this.props.currentRoomId || 'roomview'}
opacity={this.props.middleOpacity}
collapsedRhs={this.props.collapse_rhs}
ConferenceHandler={this.props.ConferenceHandler}
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;
case PageTypes.UserSettings:
page_element = <UserSettings
onClose={this.props.onUserSettingsClose}
brand={this.props.config.brand}
collapsedRhs={this.props.collapse_rhs}
enableLabs={this.props.config.enableLabs}
referralBaseUrl={this.props.config.referralBaseUrl}
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;
case PageTypes.MyGroups:
page_element = <MyGroups />;
break;
case PageTypes.CreateRoom:
@ -197,42 +273,55 @@ export default React.createClass({
onRoomCreated={this.props.onRoomCreated}
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;
case PageTypes.RoomDirectory:
page_element = <RoomDirectory
collapsedRhs={this.props.collapse_rhs}
ref="roomDirectory"
config={this.props.config.roomDirectory}
/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
break;
case PageTypes.HomePage:
page_element = <HomePage
collapsedRhs={this.props.collapse_rhs}
teamServerUrl={this.props.config.teamServerConfig.teamServerURL}
teamToken={this.props.teamToken}
/>
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>
{
// 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
teamServerUrl={teamServerUrl}
teamToken={this.props.teamToken}
homePageUrl={this.props.config.welcomePageUrl}
/>;
}
break;
case PageTypes.UserView:
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;
case PageTypes.GroupView:
page_element = <GroupView
groupId={this.props.currentGroupId}
/>;
//right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
break;
}
var topBar;
let topBar;
const isGuest = this.props.matrixClient.isGuest();
if (this.props.hasNewVersion) {
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
releaseNotes={this.props.newVersionReleaseNotes}
releaseNotes={this.props.newVersionReleaseNotes}
/>;
}
else if (this.props.matrixClient.isGuest()) {
topBar = <GuestWarningBar />;
}
else if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
} else if (this.props.checkingForUpdate) {
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
} else if (this.state.userHasGeneratedPassword) {
topBar = <PasswordNagBar />;
} else if (!isGuest && Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
topBar = <MatrixToolbar />;
}
@ -240,6 +329,9 @@ export default React.createClass({
if (topBar) {
bodyClasses += ' mx_MatrixChat_toolbarShowing';
}
if (this.state.useCompactLayout) {
bodyClasses += ' mx_MatrixChat_useCompactLayout';
}
return (
<div className='mx_MatrixChat_wrapper'>
@ -248,8 +340,7 @@ export default React.createClass({
<LeftPanel
selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapse_lhs || false}
opacity={this.props.sideOpacity}
teamToken={this.props.teamToken}
opacity={this.props.leftOpacity}
/>
<main className='mx_MatrixChat_middlePanel'>
{page_element}

File diff suppressed because it is too large Load diff

View file

@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var ReactDOM = require("react-dom");
var dis = require("../../dispatcher");
var sdk = require('../../index');
import React from 'react';
import ReactDOM from 'react-dom';
import UserSettingsStore from '../../UserSettingsStore';
import shouldHideEvent from '../../shouldHideEvent';
import dis from "../../dispatcher";
import sdk from '../../index';
var MatrixClientPeg = require('../../MatrixClientPeg');
import MatrixClientPeg from '../../MatrixClientPeg';
const MILLIS_IN_DAY = 86400000;
@ -84,6 +86,12 @@ module.exports = React.createClass({
// shape parameter to be passed to EventTiles
tileShape: React.PropTypes.string,
// show twelve hour timestamps
isTwelveHour: React.PropTypes.bool,
// show timestamps always
alwaysShowTimestamps: React.PropTypes.bool,
},
componentWillMount: function() {
@ -104,6 +112,8 @@ module.exports = React.createClass({
// Velocity requires
this._readMarkerGhostNode = null;
this._syncedSettings = UserSettingsStore.getSyncedSettings();
this._isMounted = true;
},
@ -229,9 +239,21 @@ module.exports = React.createClass({
return !this._isMounted;
},
// TODO: Implement granular (per-room) hide options
_shouldShowEvent: function(mxEv) {
const EventTile = sdk.getComponent('rooms.EventTile');
if (!EventTile.haveTileForEvent(mxEv)) {
return false; // no tile = no show
}
// Always show highlighted event
if (this.props.highlightedEventId === mxEv.getId()) return true;
return !shouldHideEvent(mxEv, this._syncedSettings);
},
_getEventTiles: function() {
var EventTile = sdk.getComponent('rooms.EventTile');
var DateSeparator = sdk.getComponent('messages.DateSeparator');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
this.eventNodes = {};
@ -240,20 +262,21 @@ module.exports = React.createClass({
// first figure out which is the last event in the list which we're
// actually going to show; this allows us to behave slightly
// differently for the last event in the list.
// differently for the last event in the list. (eg show timestamp)
//
// we also need to figure out which is the last event we show which isn't
// a local echo, to manage the read-marker.
var lastShownEventIndex = -1;
let lastShownEvent;
var lastShownNonLocalEchoIndex = -1;
for (i = this.props.events.length-1; i >= 0; i--) {
var mxEv = this.props.events[i];
if (!EventTile.haveTileForEvent(mxEv)) {
if (!this._shouldShowEvent(mxEv)) {
continue;
}
if (lastShownEventIndex < 0) {
lastShownEventIndex = i;
if (lastShownEvent === undefined) {
lastShownEvent = mxEv;
}
if (mxEv.status) {
@ -279,26 +302,18 @@ module.exports = React.createClass({
this.currentGhostEventId = null;
}
var isMembershipChange = (e) =>
e.getType() === 'm.room.member'
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
const isMembershipChange = (e) => e.getType() === 'm.room.member';
for (i = 0; i < this.props.events.length; i++) {
var mxEv = this.props.events[i];
var wantTile = true;
var eventId = mxEv.getId();
let mxEv = this.props.events[i];
let eventId = mxEv.getId();
let readMarkerInMels = false;
let last = (mxEv === lastShownEvent);
if (!EventTile.haveTileForEvent(mxEv)) {
wantTile = false;
}
var last = (i == lastShownEventIndex);
const wantTile = this._shouldShowEvent(mxEv);
// Wrap consecutive member events in a ListSummary, ignore if redacted
if (isMembershipChange(mxEv) &&
EventTile.haveTileForEvent(mxEv) &&
!mxEv.isRedacted()
) {
if (isMembershipChange(mxEv) && wantTile) {
let ts1 = mxEv.getTs();
// Ensure that the key of the MemberEventListSummary does not change with new
// member events. This will prevent it from being re-created unnecessarily, and
@ -311,38 +326,42 @@ module.exports = React.createClass({
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
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);
}
let summarisedEvents = [mxEv];
for (;i + 1 < this.props.events.length; i++) {
let collapsedMxEv = this.props.events[i + 1];
// Ignore redacted member events
if (!EventTile.haveTileForEvent(collapsedMxEv)) {
continue;
}
const collapsedMxEv = this.props.events[i + 1];
if (!isMembershipChange(collapsedMxEv) ||
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
break;
}
// If RM event is in MELS mark it as such and the RM will be appended after MELS.
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
readMarkerInMels = true;
}
// Ignore redacted/hidden member events
if (!this._shouldShowEvent(collapsedMxEv)) {
continue;
}
summarisedEvents.push(collapsedMxEv);
}
// At this point, i = the index of the last event in the summary sequence
let eventTiles = summarisedEvents.map(
(e) => {
// In order to prevent DateSeparators from appearing in the expanded form
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeperator is inserted.
let ret = this._getTilesForEvent(e, e);
prevEvent = e;
return ret;
}
).reduce((a, b) => a.concat(b));
// At this point, i = the index of the last event in the summary sequence
let eventTiles = summarisedEvents.map((e) => {
// In order to prevent DateSeparators from appearing in the expanded form
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeperator is inserted.
const ret = this._getTilesForEvent(e, e, e === lastShownEvent);
prevEvent = e;
return ret;
}).reduce((a, b) => a.concat(b));
if (eventTiles.length === 0) {
eventTiles = null;
@ -352,12 +371,16 @@ module.exports = React.createClass({
<MemberEventListSummary
key={key}
events={summarisedEvents}
data-scroll-token={eventId}
onToggle={this._onWidgetLoad} // Update scroll state
>
{eventTiles}
</MemberEventListSummary>
);
if (readMarkerInMels) {
ret.push(this._getReadMarkerTile(visible));
}
continue;
}
@ -388,6 +411,8 @@ module.exports = React.createClass({
isVisibleReadMarker = visible;
}
// XXX: there should be no need for a ghost tile - we should just use a
// a dispatch (user_activity_end) to start the RM animation.
if (eventId == this.currentGhostEventId) {
// if we're showing an animation, continue to show it.
ret.push(this._getReadMarkerGhostTile());
@ -405,8 +430,8 @@ module.exports = React.createClass({
},
_getTilesForEvent: function(prevEvent, mxEv, last) {
var EventTile = sdk.getComponent('rooms.EventTile');
var DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
var ret = [];
// is this a continuation of the previous message?
@ -444,7 +469,7 @@ module.exports = React.createClass({
// do we need a date separator since the last event?
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);
continuation = false;
}
@ -460,11 +485,10 @@ module.exports = React.createClass({
if (this.props.manageReadReceipts) {
readReceipts = this._getReadReceiptsForEvent(mxEv);
}
ret.push(
<li key={eventId}
ref={this._collectEventNode.bind(this, eventId)}
data-scroll-token={scrollToken}>
data-scroll-tokens={scrollToken}>
<EventTile mxEvent={mxEv} continuation={continuation}
isRedacted={mxEv.isRedacted()}
onWidgetLoad={this._onWidgetLoad}
@ -474,6 +498,7 @@ module.exports = React.createClass({
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.status}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
last={last} isSelectedEvent={highlight}/>
</li>
);
@ -607,8 +632,13 @@ module.exports = React.createClass({
var style = this.props.hidden ? { display: 'none' } : {};
style.opacity = this.props.opacity;
var className = this.props.className + " mx_fadable";
if (this.props.alwaysShowTimestamps) {
className += " mx_MessagePanel_alwaysShowTimestamps";
}
return (
<ScrollPanel ref="scrollPanel" className={ this.props.className + " mx_fadable" }
<ScrollPanel ref="scrollPanel" className={ className }
onScroll={ this.props.onScroll }
onResize={ this.onResize }
onFillRequest={ this.props.onFillRequest }

View file

@ -0,0 +1,141 @@
/*
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 { _t, _tJsx } from '../../languageHandler';
import withMatrixClient from '../../wrappers/withMatrixClient';
import AccessibleButton from '../views/elements/AccessibleButton';
import dis from '../../dispatcher';
import PropTypes from 'prop-types';
import Modal from '../../Modal';
const GroupTile = React.createClass({
displayName: 'GroupTile',
propTypes: {
groupId: PropTypes.string.isRequired,
},
onClick: function(e) {
e.preventDefault();
dis.dispatch({
action: 'view_group',
group_id: this.props.groupId,
});
},
render: function() {
return <a onClick={this.onClick} href="#">{this.props.groupId}</a>;
},
});
export default withMatrixClient(React.createClass({
displayName: 'MyGroups',
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
},
getInitialState: function() {
return {
groups: null,
error: null,
};
},
componentWillMount: function() {
this._fetch();
},
_onCreateGroupClick: function() {
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
Modal.createDialog(CreateGroupDialog);
},
_fetch: function() {
this.props.matrixClient.getJoinedGroups().done((result) => {
this.setState({groups: result.groups, error: null});
}, (err) => {
this.setState({groups: null, error: err});
});
},
render: function() {
const Loader = sdk.getComponent("elements.Spinner");
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
let content;
if (this.state.groups) {
const groupNodes = [];
this.state.groups.forEach((g) => {
groupNodes.push(
<div key={g}>
<GroupTile groupId={g} />
</div>,
);
});
content = <div>
<div>{_t('You are a member of these groups:')}</div>
{groupNodes}
</div>;
} else if (this.state.error) {
content = <div className="mx_MyGroups_error">
{_t('Error whilst fetching joined groups')}
</div>;
} else {
content = <Loader />;
}
return <div className="mx_MyGroups">
<SimpleRoomHeader title={ _t("Groups") } />
<div className='mx_MyGroups_joinCreateBox'>
<div className="mx_MyGroups_createBox">
<div className="mx_MyGroups_joinCreateHeader">
{_t('Create a new group')}
</div>
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onCreateGroupClick}>
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
</AccessibleButton>
{_t(
'Create a group to represent your community! '+
'Define a set of rooms and your own custom homepage '+
'to mark out your space in the Matrix universe.',
)}
</div>
<div className="mx_MyGroups_joinBox">
<div className="mx_MyGroups_joinCreateHeader">
{_t('Join an existing group')}
</div>
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onJoinGroupClick}>
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
</AccessibleButton>
{_tJsx(
'To join an exisitng group you\'ll have to '+
'know its group identifier; this will look '+
'something like <i>+example:matrix.org</i>.',
/<i>(.*)<\/i>/,
(sub) => <i>{sub}</i>,
)}
</div>
</div>
<div className="mx_MyGroups_content">
{content}
</div>
</div>;
},
}));

View file

@ -16,7 +16,7 @@ limitations under the License.
var React = require('react');
var ReactDOM = require("react-dom");
import { _t } from '../../languageHandler';
var Matrix = require("matrix-js-sdk");
var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg");
@ -37,7 +37,6 @@ var NotificationPanel = React.createClass({
var Loader = sdk.getComponent("elements.Spinner");
var timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) {
return (
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
@ -48,7 +47,7 @@ var NotificationPanel = React.createClass({
showUrlPreview = { false }
opacity={ this.props.opacity }
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.
*/
var React = require('react');
var sdk = require('../../index');
var dis = require("../../dispatcher");
var WhoIsTyping = require("../../WhoIsTyping");
var MatrixClientPeg = require("../../MatrixClientPeg");
const MemberAvatar = require("../views/avatars/MemberAvatar");
import React from 'react';
import { _t, _tJsx } from '../../languageHandler';
import sdk from '../../index';
import WhoIsTyping from '../../WhoIsTyping';
import MatrixClientPeg from '../../MatrixClientPeg';
import MemberAvatar from '../views/avatars/MemberAvatar';
const HIDE_DEBOUNCE_MS = 10000;
const STATUS_BAR_HIDDEN = 0;
@ -33,9 +33,6 @@ module.exports = React.createClass({
// the room this statusbar is representing.
room: React.PropTypes.object.isRequired,
// a TabComplete object
tabComplete: React.PropTypes.object.isRequired,
// the number of messages which have arrived since we've been scrolled up
numUnreadMessages: React.PropTypes.number,
@ -143,12 +140,9 @@ module.exports = React.createClass({
(this.state.usersTyping.length > 0) ||
this.props.numUnreadMessages ||
!this.props.atEndOfLiveTimeline ||
this.props.hasActiveCall ||
this.props.tabComplete.isTabCompleting()
this.props.hasActiveCall
) {
return STATUS_BAR_EXPANDED;
} else if (this.props.tabCompleteEntries) {
return STATUS_BAR_HIDDEN;
} else if (this.props.unsentMessageError) {
return STATUS_BAR_EXPANDED_LARGE;
}
@ -175,8 +169,8 @@ module.exports = React.createClass({
<div className="mx_RoomStatusBar_scrollDownIndicator"
onClick={ this.props.onScrollToBottomClick }>
<img src="img/scrolldown.svg" width="24" height="24"
alt="Scroll to bottom of page"
title="Scroll to bottom of page"/>
alt={ _t("Scroll to bottom of page") }
title={ _t("Scroll to bottom of page") }/>
</div>
);
}
@ -237,8 +231,6 @@ module.exports = React.createClass({
// return suitable content for the main (text) part of the status bar.
_getContent: function() {
var TabCompleteBar = sdk.getComponent('rooms.TabCompleteBar');
var TintableSvg = sdk.getComponent("elements.TintableSvg");
const EmojiText = sdk.getComponent('elements.EmojiText');
// no conn bar trumps unread count since you can't get unread messages
@ -250,24 +242,10 @@ module.exports = React.createClass({
<div className="mx_RoomStatusBar_connectionLostBar">
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
<div className="mx_RoomStatusBar_connectionLostBar_title">
Connectivity to the server has been lost.
{_t('Connectivity to the server has been lost.')}
</div>
<div className="mx_RoomStatusBar_connectionLostBar_desc">
Sent messages will be stored until your connection has returned.
</div>
</div>
);
}
if (this.props.tabComplete.isTabCompleting()) {
return (
<div className="mx_RoomStatusBar_tabCompleteBar">
<div className="mx_RoomStatusBar_tabCompleteWrapper">
<TabCompleteBar tabComplete={this.props.tabComplete} />
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
<TintableSvg src="img/eol.svg" width="22" height="16"/>
Auto-complete
</div>
{_t('Sent messages will be stored until your connection has returned.')}
</div>
</div>
);
@ -281,15 +259,13 @@ module.exports = React.createClass({
{ this.props.unsentMessageError }
</div>
<div className="mx_RoomStatusBar_connectionLostBar_desc">
<a className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onResendAllClick }>
Resend all
</a> or <a
className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onCancelAllClick }>
cancel all
</a> now. You can also select individual messages to
resend or cancel.
{_tJsx("<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
[
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={ this.props.onResendAllClick }>{sub}</a>,
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={ this.props.onCancelAllClick }>{sub}</a>,
]
)}
</div>
</div>
);
@ -298,8 +274,8 @@ module.exports = React.createClass({
// unread count trumps who is typing since the unread count is only
// set when you've scrolled up
if (this.props.numUnreadMessages) {
var unreadMsgs = this.props.numUnreadMessages + " new message" +
(this.props.numUnreadMessages > 1 ? "s" : "");
// MUST use var name "count" for pluralization to kick in
var unreadMsgs = _t("%(count)s new messages", {count: this.props.numUnreadMessages});
return (
<div className="mx_RoomStatusBar_unreadMessagesBar"
@ -324,7 +300,7 @@ module.exports = React.createClass({
if (this.props.hasActiveCall) {
return (
<div className="mx_RoomStatusBar_callBar">
<b>Active call</b>
<b>{_t('Active call')}</b>
</div>
);
}

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,7 @@ limitations under the License.
var React = require("react");
var ReactDOM = require("react-dom");
var GeminiScrollbar = require('react-gemini-scrollbar');
var q = require("q");
import Promise from 'bluebird';
var KeyCode = require('../../KeyCode');
var DEBUG_SCROLL = false;
@ -46,9 +46,13 @@ if (DEBUG_SCROLL) {
* It also provides a hook which allows parents to provide more list elements
* when we get close to the start or end of the list.
*
* Each child element should have a 'data-scroll-token'. This token is used to
* serialise the scroll state, and returned as the 'trackedScrollToken'
* attribute by getScrollState().
* Each child element should have a 'data-scroll-tokens'. This string of
* comma-separated tokens may contain a single token or many, where many indicates
* that the element contains elements that have scroll tokens themselves. The first
* token in 'data-scroll-tokens' is used to serialise the scroll state, and returned
* as the 'trackedScrollToken' attribute by getScrollState().
*
* IMPORTANT: INDIVIDUAL TOKENS WITHIN 'data-scroll-tokens' MUST NOT CONTAIN COMMAS.
*
* Some notes about the implementation:
*
@ -141,7 +145,7 @@ module.exports = React.createClass({
return {
stickyBottom: true,
startAtBottom: true,
onFillRequest: function(backwards) { return q(false); },
onFillRequest: function(backwards) { return Promise.resolve(false); },
onUnfillRequest: function(backwards, scrollToken) {},
onScroll: function() {},
};
@ -348,13 +352,14 @@ module.exports = React.createClass({
const tile = tiles[backwards ? i : tiles.length - 1 - i];
// Subtract height of tile as if it were unpaginated
excessHeight -= tile.clientHeight;
// The tile may not have a scroll token, so guard it
if (tile.dataset.scrollToken) {
markerScrollToken = tile.dataset.scrollToken;
}
//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
if (tile.dataset.scrollTokens) {
markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
}
}
if (markerScrollToken) {
@ -381,19 +386,12 @@ module.exports = React.createClass({
debuglog("ScrollPanel: starting "+dir+" fill");
// onFillRequest can end up calling us recursively (via onScroll
// events) so make sure we set this before firing off the call. That
// does present the risk that we might not ever actually fire off the
// fill request, so wrap it in a try/catch.
// events) so make sure we set this before firing off the call.
this._pendingFillRequests[dir] = true;
var fillPromise;
try {
fillPromise = this.props.onFillRequest(backwards);
} catch (e) {
this._pendingFillRequests[dir] = false;
throw e;
}
q.finally(fillPromise, () => {
Promise.try(() => {
return this.props.onFillRequest(backwards);
}).finally(() => {
this._pendingFillRequests[dir] = false;
}).then((hasMoreResults) => {
if (this.unmounted) {
@ -419,7 +417,8 @@ module.exports = React.createClass({
* scroll. false if we are tracking a particular child.
*
* string trackedScrollToken: undefined if stuckAtBottom is true; if it is
* false, the data-scroll-token of the child which we are tracking.
* false, the first token in data-scroll-tokens of the child which we are
* tracking.
*
* number pixelOffset: undefined if stuckAtBottom is true; if it is false,
* the number of pixels the bottom of the tracked child is above the
@ -483,21 +482,25 @@ module.exports = React.createClass({
handleScrollKey: function(ev) {
switch (ev.keyCode) {
case KeyCode.PAGE_UP:
this.scrollRelative(-1);
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollRelative(-1);
}
break;
case KeyCode.PAGE_DOWN:
this.scrollRelative(1);
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollRelative(1);
}
break;
case KeyCode.HOME:
if (ev.ctrlKey) {
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollToTop();
}
break;
case KeyCode.END:
if (ev.ctrlKey) {
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollToBottom();
}
break;
@ -547,8 +550,10 @@ module.exports = React.createClass({
var messages = this.refs.itemlist.children;
for (var i = messages.length-1; i >= 0; --i) {
var m = messages[i];
if (!m.dataset.scrollToken) continue;
if (m.dataset.scrollToken == scrollToken) {
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
// There might only be one scroll token
if (m.dataset.scrollTokens &&
m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) {
node = m;
break;
}
@ -564,7 +569,7 @@ module.exports = React.createClass({
var boundingRect = node.getBoundingClientRect();
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" +
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")");
if(scrollDelta != 0) {
@ -587,12 +592,12 @@ module.exports = React.createClass({
for (var i = messages.length-1; i >= 0; --i) {
var node = messages[i];
if (!node.dataset.scrollToken) continue;
if (!node.dataset.scrollTokens) continue;
var boundingRect = node.getBoundingClientRect();
newScrollState = {
stuckAtBottom: false,
trackedScrollToken: node.dataset.scrollToken,
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
};
// If the bottom of the panel intersects the ClientRect of node, use this node
@ -604,7 +609,7 @@ module.exports = React.createClass({
break;
}
}
// This is only false if there were no nodes with `node.dataset.scrollToken` set.
// This is only false if there were no nodes with `node.dataset.scrollTokens` set.
if (newScrollState) {
this.scrollState = newScrollState;
debuglog("ScrollPanel: saved scroll state", this.scrollState);

View file

@ -1,5 +1,6 @@
/*
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.
@ -16,18 +17,20 @@ limitations under the License.
var React = require('react');
var ReactDOM = require("react-dom");
var q = require("q");
import Promise from 'bluebird';
var Matrix = require("matrix-js-sdk");
var EventTimeline = Matrix.EventTimeline;
var sdk = require('../../index');
import { _t } from '../../languageHandler';
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
var ObjectUtils = require('../../ObjectUtils');
var Modal = require("../../Modal");
var UserActivity = require("../../UserActivity");
var KeyCode = require('../../KeyCode');
import UserSettingsStore from '../../UserSettingsStore';
var PAGINATE_SIZE = 20;
var INITIAL_SIZE = 20;
@ -102,9 +105,6 @@ var TimelinePanel = React.createClass({
},
statics: {
// a map from room id to read marker event ID
roomReadMarkerMap: {},
// a map from room id to read marker event timestamp
roomReadMarkerTsMap: {},
},
@ -121,12 +121,18 @@ var TimelinePanel = React.createClass({
getInitialState: function() {
// XXX: we could track RM per TimelineSet rather than per Room.
// but for now we just do it per room for simplicity.
let initialReadMarker = null;
if (this.props.manageReadMarkers) {
var initialReadMarker =
TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId]
|| this._getCurrentReadReceipt();
const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read');
if (readmarker) {
initialReadMarker = readmarker.getContent().event_id;
} else {
initialReadMarker = this._getCurrentReadReceipt();
}
}
const syncedSettings = UserSettingsStore.getSyncedSettings();
return {
events: [],
timelineLoading: true, // track whether our room timeline is loading
@ -166,13 +172,23 @@ var TimelinePanel = React.createClass({
backPaginating: false,
forwardPaginating: false,
// cache of matrixClient.getSyncState() (but from the 'sync' event)
clientSyncState: MatrixClientPeg.get().getSyncState(),
// should the event tiles have twelve hour times
isTwelveHour: syncedSettings.showTwelveHourTimestamps,
// always show timestamps on event tiles?
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps,
};
},
componentWillMount: function() {
debuglog("TimelinePanel: mounting");
this.last_rr_sent_event_id = undefined;
this.lastRRSentEventId = undefined;
this.lastRMSentEventId = undefined;
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
@ -180,6 +196,8 @@ var TimelinePanel = React.createClass({
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
MatrixClientPeg.get().on("sync", this.onSync);
this._initTimeline(this.props);
},
@ -247,6 +265,8 @@ var TimelinePanel = React.createClass({
client.removeListener("Room.redaction", this.onRoomRedaction);
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
client.removeListener("Room.accountData", this.onAccountData);
client.removeListener("sync", this.onSync);
}
},
@ -288,13 +308,13 @@ var TimelinePanel = React.createClass({
if (!this.state[canPaginateKey]) {
debuglog("TimelinePanel: have given up", dir, "paginating this timeline");
return q(false);
return Promise.resolve(false);
}
if(!this._timelineWindow.canPaginate(dir)) {
debuglog("TimelinePanel: can't", dir, "paginate any further");
this.setState({[canPaginateKey]: false});
return q(false);
return Promise.resolve(false);
}
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
@ -327,9 +347,9 @@ var TimelinePanel = React.createClass({
});
},
onMessageListScroll: function() {
onMessageListScroll: function(e) {
if (this.props.onScroll) {
this.props.onScroll();
this.props.onScroll(e);
}
if (this.props.manageReadMarkers) {
@ -414,6 +434,7 @@ var TimelinePanel = React.createClass({
} else if(lastEv && this.getReadMarkerPosition() === 0) {
// we know we're stuckAtBottom, so we can advance the RM
// immediately, to save a later render cycle
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
updatedState.readMarkerVisible = false;
updatedState.readMarkerEventId = lastEv.getId();
@ -466,6 +487,25 @@ var TimelinePanel = React.createClass({
this._reloadEvents();
},
onAccountData: function(ev, room) {
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.timelineSet.room) return;
if (ev.getType() !== "m.fully_read") return;
// XXX: roomReadMarkerTsMap not updated here so it is now inconsistent. Replace
// this mechanism of determining where the RM is relative to the view-port with
// one supported by the server (the client needs more than an event ID).
this.setState({
readMarkerEventId: ev.getContent().event_id,
}, this.props.onReadMarkerUpdated);
},
onSync: function(state, prevState, data) {
this.setState({clientSyncState: state});
},
sendReadReceipt: function() {
if (!this.refs.messagePanel) return;
@ -473,11 +513,14 @@ var TimelinePanel = React.createClass({
// This happens on user_activity_end which is delayed, and it's
// very possible have logged out within that timeframe, so check
// we still have a client.
if (!MatrixClientPeg.get()) return;
const cli = MatrixClientPeg.get();
// if no client or client is guest don't send RR or RM
if (!cli || cli.isGuest()) return;
var currentReadUpToEventId = this._getCurrentReadReceipt(true);
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
let shouldSendRR = true;
const currentRREventId = this._getCurrentReadReceipt(true);
const currentRREventIndex = this._indexForEventId(currentRREventId);
// We want to avoid sending out read receipts when we are looking at
// events in the past which are before the latest RR.
//
@ -491,26 +534,60 @@ var TimelinePanel = React.createClass({
// RRs) - but that is a bit of a niche case. It will sort itself out when
// the user eventually hits the live timeline.
//
if (currentReadUpToEventId && currentReadUpToEventIndex === null &&
if (currentRREventId && currentRREventIndex === null &&
this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
return;
shouldSendRR = false;
}
var lastReadEventIndex = this._getLastDisplayedEventIndex({
ignoreOwn: true
const lastReadEventIndex = this._getLastDisplayedEventIndex({
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
// same one at the server repeatedly
if (lastReadEventIndex > currentReadUpToEventIndex
&& this.last_rr_sent_event_id != lastReadEvent.getId()) {
this.last_rr_sent_event_id = lastReadEvent.getId();
MatrixClientPeg.get().sendReadReceipt(lastReadEvent).catch(() => {
if (shouldSendRR || shouldSendRM) {
if (shouldSendRR) {
this.lastRRSentEventId = lastReadEvent.getId();
} else {
lastReadEvent = null;
}
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(
this.props.timelineSet.room.roomId,
this.state.readMarkerEventId,
lastReadEvent, // Could be null, in which case no RR is sent
).catch((e) => {
// /read_markers API is not implemented on this HS, fallback to just RR
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
return MatrixClientPeg.get().sendReadReceipt(
lastReadEvent,
).catch(() => {
this.lastRRSentEventId = undefined;
});
}
// it failed, so allow retries next time the user is active
this.last_rr_sent_event_id = undefined;
this.lastRRSentEventId = undefined;
this.lastRMSentEventId = undefined;
});
// do a quick-reset of our unreadNotificationCount to avoid having
@ -706,7 +783,7 @@ var TimelinePanel = React.createClass({
// the messagePanel doesn't know where the read marker is.
// if we know the timestamp of the read marker, make a guess based on that.
var rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.roomId];
const rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.room.roomId];
if (rmTs && this.state.events.length > 0) {
if (rmTs < this.state.events[0].getTs()) {
return -1;
@ -718,6 +795,19 @@ var TimelinePanel = React.createClass({
return null;
},
canJumpToReadMarker: function() {
// 1. Do not show jump bar if neither the RM nor the RR are set.
// 2. Only show jump bar if RR !== RM. If they are the same, there are only fully
// read messages and unread messages. We already have a badge count and the bottom
// bar to jump to "live" when we have unread messages.
// 3. We want to show the bar if the read-marker is off the top of the screen.
// 4. Also, if pos === null, the event might not be paginated - show the unread bar
const pos = this.getReadMarkerPosition();
return this.state.readMarkerEventId !== null && // 1.
this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
(pos < 0 || pos === null); // 3., 4.
},
/**
* called by the parent component when PageUp/Down/etc is pressed.
*
@ -728,7 +818,9 @@ var TimelinePanel = React.createClass({
// jump to the live timeline on ctrl-end, rather than the end of the
// timeline window.
if (ev.ctrlKey && ev.keyCode == KeyCode.END) {
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
ev.keyCode == KeyCode.END)
{
this.jumpToLiveTimeline();
} else {
this.refs.messagePanel.handleScrollKey(ev);
@ -807,6 +899,9 @@ var TimelinePanel = React.createClass({
var onError = (error) => {
this.setState({timelineLoading: false});
console.error(
`Error loading timeline panel at ${eventId}: ${error}`,
);
var msg = error.message ? error.message : JSON.stringify(error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -825,14 +920,11 @@ var TimelinePanel = React.createClass({
});
};
}
var message = "Tried to load a specific point in this room's timeline, but ";
if (error.errcode == 'M_FORBIDDEN') {
message += "you do not have permission to view the message in question.";
} else {
message += "was unable to find it.";
}
var message = (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.")
: _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
Modal.createDialog(ErrorDialog, {
title: "Failed to load timeline position",
title: _t("Failed to load timeline position"),
description: message,
onFinished: onFinished,
});
@ -956,16 +1048,12 @@ var TimelinePanel = React.createClass({
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
var roomId = this.props.timelineSet.room.roomId;
if (TimelinePanel.roomReadMarkerMap[roomId] == eventId) {
// don't update the state (and cause a re-render) if there is
// no change to the RM.
// don't update the state (and cause a re-render) if there is
// no change to the RM.
if (eventId === this.state.readMarkerEventId) {
return;
}
// ideally we'd sync these via the server, but for now just stash them
// in a map.
TimelinePanel.roomReadMarkerMap[roomId] = eventId;
// in order to later figure out if the read marker is
// above or below the visible timeline, we stash the timestamp.
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
@ -974,6 +1062,7 @@ var TimelinePanel = React.createClass({
return;
}
// Do the local echo of the RM
// run the render cycle before calling the callback, so that
// getReadMarkerPosition() returns the right thing.
this.setState({
@ -1022,26 +1111,33 @@ var TimelinePanel = React.createClass({
// of paginating our way through the entire history of the room.
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
// If the state is PREPARED, we're still waiting for the js-sdk to sync with
// the HS and fetch the latest events, so we are effectively forward paginating.
const forwardPaginating = (
this.state.forwardPaginating || this.state.clientSyncState == 'PREPARED'
);
return (
<MessagePanel ref="messagePanel"
hidden={ this.props.hidden }
backPaginating={ this.state.backPaginating }
forwardPaginating={ this.state.forwardPaginating }
events={ this.state.events }
highlightedEventId={ this.props.highlightedEventId }
readMarkerEventId={ this.state.readMarkerEventId }
readMarkerVisible={ this.state.readMarkerVisible }
suppressFirstDateSeparator={ this.state.canBackPaginate }
showUrlPreview = { this.props.showUrlPreview }
manageReadReceipts = { this.props.manageReadReceipts }
ourUserId={ MatrixClientPeg.get().credentials.userId }
stickyBottom={ stickyBottom }
onScroll={ this.onMessageListScroll }
onFillRequest={ this.onMessageListFillRequest }
onUnfillRequest={ this.onMessageListUnfillRequest }
opacity={ this.props.opacity }
className={ this.props.className }
tileShape={ this.props.tileShape }
hidden={ this.props.hidden }
backPaginating={ this.state.backPaginating }
forwardPaginating={ forwardPaginating }
events={ this.state.events }
highlightedEventId={ this.props.highlightedEventId }
readMarkerEventId={ this.state.readMarkerEventId }
readMarkerVisible={ this.state.readMarkerVisible }
suppressFirstDateSeparator={ this.state.canBackPaginate }
showUrlPreview = { this.props.showUrlPreview }
manageReadReceipts = { this.props.manageReadReceipts }
ourUserId={ MatrixClientPeg.get().credentials.userId }
stickyBottom={ stickyBottom }
onScroll={ this.onMessageListScroll }
onFillRequest={ this.onMessageListFillRequest }
onUnfillRequest={ this.onMessageListUnfillRequest }
opacity={ this.props.opacity }
isTwelveHour={ this.state.isTwelveHour }
alwaysShowTimestamps={ this.state.alwaysShowTimestamps }
className={ this.props.className }
tileShape={ this.props.tileShape }
/>
);
},

View file

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

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
var React = require('react');
import { _t } from '../../../languageHandler';
var sdk = require('../../../index');
var Modal = require("../../../Modal");
var MatrixClientPeg = require('../../../MatrixClientPeg');
@ -54,7 +55,7 @@ module.exports = React.createClass({
progress: "sent_email"
});
}, (err) => {
this.showErrorDialog("Failed to send email: " + err.message);
this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
this.setState({
progress: null
});
@ -78,30 +79,33 @@ module.exports = React.createClass({
ev.preventDefault();
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) {
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) {
this.showErrorDialog("New passwords must match each other.");
this.showErrorDialog(_t('New passwords must match each other.'));
}
else {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Warning",
title: _t('Warning!'),
description:
<div>
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 <a href="https://github.com/vector-im/riot-web/issues/2671">will be improved</a>.
{ _t(
'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.'
) }
</div>,
button: "Continue",
button: _t('Continue'),
extraButtons: [
<button className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}>
Export E2E room keys
{ _t('Export E2E room keys') }
</button>
],
onFinished: (confirmed) => {
@ -150,7 +154,7 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: title,
description: body
description: body,
});
},
@ -168,22 +172,20 @@ module.exports = React.createClass({
else if (this.state.progress === "sent_email") {
resetPasswordJsx = (
<div>
An email has been sent to {this.state.email}. Once you&#39;ve followed
the link it contains, click below.
{ _t('An email has been sent to') } {this.state.email}. { _t('Once you&#39;ve followed the link it contains, click below') }.
<br />
<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>
);
}
else if (this.state.progress === "complete") {
resetPasswordJsx = (
<div>
<p>Your password has been reset.</p>
<p>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>
<p>{ _t('Your password has been reset') }.</p>
<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>
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
value="Return to login screen" />
value={ _t('Return to login screen') } />
</div>
);
}
@ -191,7 +193,7 @@ module.exports = React.createClass({
resetPasswordJsx = (
<div>
<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>
<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
value={this.state.email}
onChange={this.onInputChanged.bind(this, "email")}
placeholder="Email address" autoFocus />
placeholder={ _t('Email address') } autoFocus />
<br />
<input className="mx_Login_field" ref="pass" type="password"
name="reset_password"
value={this.state.password}
onChange={this.onInputChanged.bind(this, "password")}
placeholder="New password" />
placeholder={ _t('New password') } />
<br />
<input className="mx_Login_field" ref="pass" type="password"
name="reset_password_confirm"
value={this.state.password2}
onChange={this.onInputChanged.bind(this, "password2")}
placeholder="Confirm your new password" />
placeholder={ _t('Confirm your new password') } />
<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>
<ServerConfig ref="serverConfig"
withToggleButton={true}
@ -227,10 +229,10 @@ module.exports = React.createClass({
<div className="mx_Login_error">
</div>
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
Return to login
{_t('Return to login screen')}
</a>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
Create a new account
{ _t('Create an account') }
</a>
<LoginFooter />
</div>

View file

@ -17,13 +17,13 @@ limitations under the License.
'use strict';
var React = require('react');
var ReactDOM = require('react-dom');
var sdk = require('../../../index');
var Login = require("../../../Login");
var PasswordLogin = require("../../views/login/PasswordLogin");
var CasLogin = require("../../views/login/CasLogin");
var ServerConfig = require("../../views/login/ServerConfig");
import React from 'react';
import { _t, _tJsx } from '../../../languageHandler';
import sdk from '../../../index';
import Login from '../../../Login';
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
/**
* A wire component which glues together login UI components and Login logic
@ -67,13 +67,19 @@ module.exports = React.createClass({
username: "",
phoneCountry: null,
phoneNumber: "",
currentFlow: "m.login.password",
};
},
componentWillMount: function() {
this._unmounted = false;
this._initLoginLogic();
},
componentWillUnmount: function() {
this._unmounted = true;
},
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
this.setState({
busy: true,
@ -86,10 +92,36 @@ module.exports = React.createClass({
).then((data) => {
this.props.onLoggedIn(data);
}, (error) => {
this._setStateFromError(error, true);
}).finally(() => {
if(this._unmounted) {
return;
}
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({
busy: false
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(() => {
if(this._unmounted) {
return;
}
this.setState({
busy: false,
});
}).done();
},
@ -109,7 +141,16 @@ module.exports = React.createClass({
this._loginLogic.loginAsGuest().then(function(data) {
self.props.onLoggedIn(data);
}, 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() {
self.setState({
busy: false
@ -126,26 +167,31 @@ module.exports = React.createClass({
},
onPhoneNumberChanged: function(phoneNumber) {
this.setState({ phoneNumber: phoneNumber });
},
// Validate the phone number entered
if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
this.setState({ errorText: _t('The phone number entered looks invalid') });
return;
}
onHsUrlChanged: function(newHsUrl) {
var self = this;
this.setState({
enteredHomeserverUrl: newHsUrl,
errorText: null, // reset err messages
}, function() {
self._initLoginLogic(newHsUrl);
phoneNumber: phoneNumber,
errorText: null,
});
},
onIsUrlChanged: function(newIsUrl) {
onServerConfigChange: function(config) {
var self = this;
this.setState({
enteredIdentityServerUrl: newIsUrl,
let newState = {
errorText: null, // reset err messages
}, function() {
self._initLoginLogic(null, newIsUrl);
};
if (config.hsUrl !== undefined) {
newState.enteredHomeserverUrl = config.hsUrl;
}
if (config.isUrl !== undefined) {
newState.enteredIdentityServerUrl = config.isUrl;
}
this.setState(newState, function() {
self._initLoginLogic(config.hsUrl || null, config.isUrl);
});
},
@ -161,66 +207,64 @@ module.exports = React.createClass({
});
this._loginLogic = loginLogic;
loginLogic.getFlows().then(function(flows) {
// old behaviour was to always use the first flow without presenting
// options. This works in most cases (we don't have a UI for multiple
// logins so let's skip that for now).
loginLogic.chooseFlow(0);
}, function(err) {
self._setStateFromError(err, false);
}).finally(function() {
self.setState({
busy: false
});
});
this.setState({
enteredHomeserverUrl: hsUrl,
enteredIdentityServerUrl: isUrl,
busy: true,
loginIncorrect: false,
});
loginLogic.getFlows().then(function(flows) {
// old behaviour was to always use the first flow without presenting
// options. This works in most cases (we don't have a UI for multiple
// logins so let's skip that for now).
loginLogic.chooseFlow(0);
self.setState({
currentFlow: self._getCurrentFlowStep(),
});
}, function(err) {
self.setState({
errorText: self._errorTextFromError(err),
loginIncorrect: false,
});
}).finally(function() {
self.setState({
busy: false,
});
}).done();
},
_getCurrentFlowStep: function() {
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) {
if (err.friendlyText) {
return err.friendlyText;
}
let errCode = err.errcode;
if (!errCode && err.httpStatus) {
errCode = "HTTP " + err.httpStatus;
}
let errorText = "Error: Problem communicating with the given homeserver " +
(errCode ? "(" + errCode + ")" : "");
let errorText = _t("Error: Problem communicating with the given homeserver.") +
(errCode ? " (" + errCode + ")" : "");
if (err.cors === 'rejected') {
if (window.location.protocol === 'https:' &&
(this.state.enteredHomeserverUrl.startsWith("http:") ||
!this.state.enteredHomeserverUrl.startsWith("http")))
{
!this.state.enteredHomeserverUrl.startsWith("http"))
) {
errorText = <span>
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>
{ _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
"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>;
}
else {
} else {
errorText = <span>
Can't connect to homeserver - please check your connectivity and ensure
your <a href={ this.state.enteredHomeserverUrl }>homeserver's SSL certificate</a> is trusted.
{ _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.",
/<a>(.*?)<\/a>/,
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; }
)}
</span>;
}
}
@ -231,6 +275,7 @@ module.exports = React.createClass({
componentForStep: function(step) {
switch (step) {
case 'm.login.password':
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
return (
<PasswordLogin
onSubmit={this.onPasswordLogin}
@ -245,6 +290,7 @@ module.exports = React.createClass({
/>
);
case 'm.login.cas':
const CasLogin = sdk.getComponent('login.CasLogin');
return (
<CasLogin onSubmit={this.onCasLogin} />
);
@ -254,24 +300,24 @@ module.exports = React.createClass({
}
return (
<div>
Sorry, this homeserver is using a login which is not
recognised ({step})
{ _t('Sorry, this homeserver is using a login which is not recognised ')}({step})
</div>
);
}
},
render: function() {
var Loader = sdk.getComponent("elements.Spinner");
var LoginHeader = sdk.getComponent("login.LoginHeader");
var LoginFooter = sdk.getComponent("login.LoginFooter");
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
const Loader = sdk.getComponent("elements.Spinner");
const LoginHeader = sdk.getComponent("login.LoginHeader");
const LoginFooter = sdk.getComponent("login.LoginFooter");
const ServerConfig = sdk.getComponent("login.ServerConfig");
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
var loginAsGuestJsx;
if (this.props.enableGuest) {
loginAsGuestJsx =
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
Login as guest
{ _t('Login as guest')}
</a>;
}
@ -279,7 +325,7 @@ module.exports = React.createClass({
if (this.props.onCancelClick) {
returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app
{ _t('Return to app')}
</a>;
}
@ -288,24 +334,23 @@ module.exports = React.createClass({
<div className="mx_Login_box">
<LoginHeader />
<div>
<h2>Sign in
<h2>{ _t('Sign in')}
{ loader }
</h2>
{ this.componentForStep(this._getCurrentFlowStep()) }
{ this.componentForStep(this.state.currentFlow) }
<ServerConfig ref="serverConfig"
withToggleButton={true}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onHsUrlChanged={this.onHsUrlChanged}
onIsUrlChanged={this.onIsUrlChanged}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}/>
<div className="mx_Login_error">
{ this.state.errorText }
</div>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
Create a new account
{ _t('Create an account')}
</a>
{ loginAsGuestJsx }
{ returnToAppJsx }

View file

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

View file

@ -17,16 +17,15 @@ limitations under the License.
import Matrix from 'matrix-js-sdk';
import q from 'q';
import Promise from 'bluebird';
import React from 'react';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import ServerConfig from '../../views/login/ServerConfig';
import MatrixClientPeg from '../../../MatrixClientPeg';
import RegistrationForm from '../../views/login/RegistrationForm';
import CaptchaForm from '../../views/login/CaptchaForm';
import RtsClient from '../../../RtsClient';
import { _t } from '../../../languageHandler';
const MIN_PASSWORD_LENGTH = 6;
@ -46,8 +45,6 @@ module.exports = React.createClass({
brand: React.PropTypes.string,
email: React.PropTypes.string,
referrer: React.PropTypes.string,
username: React.PropTypes.string,
guestAccessToken: React.PropTypes.string,
teamServerConfig: React.PropTypes.shape({
// Email address to request new teams
supportEmail: React.PropTypes.string.isRequired,
@ -98,7 +95,7 @@ module.exports = React.createClass({
this.props.teamServerConfig.teamServerURL &&
!this._rtsClient
) {
this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL);
this._rtsClient = this.props.rtsClient || new RtsClient(this.props.teamServerConfig.teamServerURL);
this.setState({
teamServerBusy: true,
@ -123,18 +120,17 @@ module.exports = React.createClass({
}
},
onHsUrlChanged: function(newHsUrl) {
this.setState({
hsUrl: newHsUrl,
onServerConfigChange: function(config) {
let newState = {};
if (config.hsUrl !== undefined) {
newState.hsUrl = config.hsUrl;
}
if (config.isUrl !== undefined) {
newState.isUrl = config.isUrl;
}
this.setState(newState, function() {
this._replaceClient();
});
this._replaceClient();
},
onIsUrlChanged: function(newIsUrl) {
this.setState({
isUrl: newIsUrl,
});
this._replaceClient();
},
_replaceClient: function() {
@ -163,7 +159,7 @@ module.exports = React.createClass({
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
}
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({
@ -184,7 +180,7 @@ module.exports = React.createClass({
// will just nop. The point of this being we might not have the email address
// that the user registered with at this stage (depending on whether this
// is the client they initiated registration).
let trackPromise = q(null);
let trackPromise = Promise.resolve(null);
if (this._rtsClient && extra.emailSid) {
// Track referral if this.props.referrer set, get team_token in order to
// retrieve team config and see welcome page etc.
@ -222,30 +218,29 @@ module.exports = React.createClass({
}
trackPromise.then((teamToken) => {
console.info('Team token promise',teamToken);
this.props.onLoggedIn({
return this.props.onLoggedIn({
userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: this._matrixClient.getHomeserverUrl(),
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token
}, teamToken);
}).then(() => {
return this._setupPushers();
}).then((cli) => {
return this._setupPushers(cli);
});
},
_setupPushers: function() {
_setupPushers: function(matrixClient) {
if (!this.props.brand) {
return q();
return Promise.resolve();
}
return MatrixClientPeg.get().getPushers().then((resp)=>{
return matrixClient.getPushers().then((resp)=>{
const pushers = resp.pushers;
for (let i = 0; i < pushers.length; ++i) {
if (pushers[i].kind == 'email') {
const emailPusher = pushers[i];
emailPusher.data = { brand: this.props.brand };
MatrixClientPeg.get().setPusher(emailPusher).done(() => {
matrixClient.setPusher(emailPusher).done(() => {
console.log("Set email branding to " + this.props.brand);
}, (error) => {
console.error("Couldn't set email branding: " + error);
@ -261,29 +256,29 @@ module.exports = React.createClass({
var errMsg;
switch (errCode) {
case "RegistrationForm.ERR_PASSWORD_MISSING":
errMsg = "Missing password.";
errMsg = _t('Missing password.');
break;
case "RegistrationForm.ERR_PASSWORD_MISMATCH":
errMsg = "Passwords don't match.";
errMsg = _t('Passwords don\'t match.');
break;
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;
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;
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;
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;
case "RegistrationForm.ERR_USERNAME_BLANK":
errMsg = "You need to enter a user name";
errMsg = _t('You need to enter a user name.');
break;
default:
console.error("Unknown error code: %s", errCode);
errMsg = "An unknown error occurred.";
errMsg = _t('An unknown error occurred.');
break;
}
this.setState({
@ -298,17 +293,6 @@ module.exports = React.createClass({
},
_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
// (Since we need to send no params at all to use the ones saved in the
// session).
@ -323,7 +307,7 @@ module.exports = React.createClass({
undefined, // session id: included in the auth dict already
auth,
bindThreepids,
guestAccessToken,
null,
);
},
@ -360,10 +344,6 @@ module.exports = React.createClass({
} else if (this.state.busy || this.state.teamServerBusy) {
registerBody = <Spinner />;
} else {
let guestUsername = this.props.username;
if (this.state.hsUrl != this.props.defaultHsUrl) {
guestUsername = null;
}
let errorSection;
if (this.state.errorText) {
errorSection = <div className="mx_Login_error">{this.state.errorText}</div>;
@ -377,7 +357,6 @@ module.exports = React.createClass({
defaultPhoneNumber={this.state.formVals.phoneNumber}
defaultPassword={this.state.formVals.password}
teamsConfig={this.state.teamsConfig}
guestUsername={guestUsername}
minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed}
onRegisterClick={this.onFormSubmit}
@ -390,8 +369,7 @@ module.exports = React.createClass({
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onHsUrlChanged={this.onHsUrlChanged}
onIsUrlChanged={this.onIsUrlChanged}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}
/>
</div>
@ -402,7 +380,7 @@ module.exports = React.createClass({
if (this.props.onCancelClick) {
returnToAppJsx = (
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app
{_t('Return to app')}
</a>
);
}
@ -415,10 +393,10 @@ module.exports = React.createClass({
this.state.teamSelected.domain + "/icon.png" :
null}
/>
<h2>Create an account</h2>
<h2>{_t('Create an account')}</h2>
{registerBody}
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
I already have an account
{_t('I already have an account')}
</a>
{returnToAppJsx}
<LoginFooter />

View file

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

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