Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into set_default_federate_by_settings
This commit is contained in:
commit
fd454b476a
268 changed files with 27696 additions and 5893 deletions
2
.babelrc
2
.babelrc
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"presets": ["react", "es2015", "es2016"],
|
"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
161
.eslintignore.errorfiles
Normal 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
|
|
@ -64,7 +64,7 @@ module.exports = {
|
||||||
// to JSX.
|
// to JSX.
|
||||||
ignorePattern: '^\\s*<',
|
ignorePattern: '^\\s*<',
|
||||||
ignoreComments: true,
|
ignoreComments: true,
|
||||||
code: 90,
|
code: 120,
|
||||||
}],
|
}],
|
||||||
"valid-jsdoc": ["warn"],
|
"valid-jsdoc": ["warn"],
|
||||||
"new-cap": ["warn"],
|
"new-cap": ["warn"],
|
||||||
|
|
6
.flowconfig
Normal file
6
.flowconfig
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[include]
|
||||||
|
src/**/*.js
|
||||||
|
test/**/*.js
|
||||||
|
|
||||||
|
[ignore]
|
||||||
|
node_modules/
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -9,3 +9,8 @@ npm-debug.log
|
||||||
|
|
||||||
# test reports created by karma
|
# test reports created by karma
|
||||||
/karma-reports
|
/karma-reports
|
||||||
|
|
||||||
|
/.idea
|
||||||
|
/src/component-index.js
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
|
@ -9,16 +9,24 @@ set -ev
|
||||||
RIOT_WEB_DIR=riot-web
|
RIOT_WEB_DIR=riot-web
|
||||||
REACT_SDK_DIR=`pwd`
|
REACT_SDK_DIR=`pwd`
|
||||||
|
|
||||||
git clone --depth=1 --branch develop https://github.com/vector-im/riot-web.git \
|
curbranch="${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}"
|
||||||
|
echo "Determined branch to be $curbranch"
|
||||||
|
|
||||||
|
git clone https://github.com/vector-im/riot-web.git \
|
||||||
"$RIOT_WEB_DIR"
|
"$RIOT_WEB_DIR"
|
||||||
|
|
||||||
cd "$RIOT_WEB_DIR"
|
cd "$RIOT_WEB_DIR"
|
||||||
|
|
||||||
|
git checkout "$curbranch" || git checkout develop
|
||||||
|
|
||||||
mkdir node_modules
|
mkdir node_modules
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
(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
|
rm -r node_modules/matrix-react-sdk
|
||||||
ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk
|
ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk
|
||||||
|
|
||||||
|
|
12
.travis.yml
12
.travis.yml
|
@ -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
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- node # Latest stable version of nodejs.
|
- node # Latest stable version of nodejs.
|
||||||
|
addons:
|
||||||
|
chrome: stable
|
||||||
install:
|
install:
|
||||||
- npm install
|
- npm install
|
||||||
- (cd node_modules/matrix-js-sdk && npm install)
|
- (cd node_modules/matrix-js-sdk && npm install)
|
||||||
script:
|
script:
|
||||||
- npm run test
|
./scripts/travis.sh
|
||||||
- ./.travis-test-riot.sh
|
|
||||||
|
|
621
CHANGELOG.md
621
CHANGELOG.md
|
@ -1,3 +1,624 @@
|
||||||
|
Changes in [0.9.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.7) (2017-06-22)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.6...v0.9.7)
|
||||||
|
|
||||||
|
* Fix ability to invite users with caps in their user IDs
|
||||||
|
[\#1128](https://github.com/matrix-org/matrix-react-sdk/pull/1128)
|
||||||
|
* Fix another race with first-sync
|
||||||
|
[\#1131](https://github.com/matrix-org/matrix-react-sdk/pull/1131)
|
||||||
|
* Make the indexeddb worker script work again
|
||||||
|
[\#1132](https://github.com/matrix-org/matrix-react-sdk/pull/1132)
|
||||||
|
* Use the web worker when clearing js-sdk stores
|
||||||
|
[\#1133](https://github.com/matrix-org/matrix-react-sdk/pull/1133)
|
||||||
|
|
||||||
|
Changes in [0.9.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.6) (2017-06-20)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.5...v0.9.6)
|
||||||
|
|
||||||
|
* Fix infinite spinner on email registration
|
||||||
|
[\#1120](https://github.com/matrix-org/matrix-react-sdk/pull/1120)
|
||||||
|
* Translate help promots in room list
|
||||||
|
[\#1121](https://github.com/matrix-org/matrix-react-sdk/pull/1121)
|
||||||
|
* Internationalise the drop targets
|
||||||
|
[\#1122](https://github.com/matrix-org/matrix-react-sdk/pull/1122)
|
||||||
|
* Fix another infinite spin on register
|
||||||
|
[\#1124](https://github.com/matrix-org/matrix-react-sdk/pull/1124)
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [0.9.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.5) (2017-06-19)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.5-rc.2...v0.9.5)
|
||||||
|
|
||||||
|
* Don't peek when creating a room
|
||||||
|
[\#1113](https://github.com/matrix-org/matrix-react-sdk/pull/1113)
|
||||||
|
* More translations & translation fixes
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [0.9.5-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.5-rc.2) (2017-06-16)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.5-rc.1...v0.9.5-rc.2)
|
||||||
|
|
||||||
|
* Avoid getting stuck in a loop in CAS login
|
||||||
|
[\#1109](https://github.com/matrix-org/matrix-react-sdk/pull/1109)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1101](https://github.com/matrix-org/matrix-react-sdk/pull/1101)
|
||||||
|
* Correctly inspect state when rejecting invite
|
||||||
|
[\#1108](https://github.com/matrix-org/matrix-react-sdk/pull/1108)
|
||||||
|
* Make sure to pass the roomAlias to the preview header if we have it
|
||||||
|
[\#1107](https://github.com/matrix-org/matrix-react-sdk/pull/1107)
|
||||||
|
* Make sure captcha disappears when container does
|
||||||
|
[\#1106](https://github.com/matrix-org/matrix-react-sdk/pull/1106)
|
||||||
|
* Fix URL previews
|
||||||
|
[\#1105](https://github.com/matrix-org/matrix-react-sdk/pull/1105)
|
||||||
|
|
||||||
|
Changes in [0.9.5-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.5-rc.1) (2017-06-15)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.4...v0.9.5-rc.1)
|
||||||
|
|
||||||
|
* Groundwork for tests including a teamserver login
|
||||||
|
[\#1098](https://github.com/matrix-org/matrix-react-sdk/pull/1098)
|
||||||
|
* Show a spinner when accepting an invite and waitingForRoom
|
||||||
|
[\#1100](https://github.com/matrix-org/matrix-react-sdk/pull/1100)
|
||||||
|
* Display a spinner until new room object after join success
|
||||||
|
[\#1099](https://github.com/matrix-org/matrix-react-sdk/pull/1099)
|
||||||
|
* Luke/attempt fix peeking regression
|
||||||
|
[\#1097](https://github.com/matrix-org/matrix-react-sdk/pull/1097)
|
||||||
|
* Show correct text in set email password dialog (2)
|
||||||
|
[\#1096](https://github.com/matrix-org/matrix-react-sdk/pull/1096)
|
||||||
|
* Don't create a guest login if user went to /login
|
||||||
|
[\#1092](https://github.com/matrix-org/matrix-react-sdk/pull/1092)
|
||||||
|
* Give password confirmation correct title, description
|
||||||
|
[\#1095](https://github.com/matrix-org/matrix-react-sdk/pull/1095)
|
||||||
|
* Make enter submit change password form
|
||||||
|
[\#1094](https://github.com/matrix-org/matrix-react-sdk/pull/1094)
|
||||||
|
* When not specified, remove roomAlias state in RoomViewStore
|
||||||
|
[\#1093](https://github.com/matrix-org/matrix-react-sdk/pull/1093)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1091](https://github.com/matrix-org/matrix-react-sdk/pull/1091)
|
||||||
|
* Fixed pagination infinite loop caused by long messages
|
||||||
|
[\#1045](https://github.com/matrix-org/matrix-react-sdk/pull/1045)
|
||||||
|
* Clear persistent storage on login and logout
|
||||||
|
[\#1085](https://github.com/matrix-org/matrix-react-sdk/pull/1085)
|
||||||
|
* DM guessing: prefer oldest joined member
|
||||||
|
[\#1087](https://github.com/matrix-org/matrix-react-sdk/pull/1087)
|
||||||
|
* Ask for email address after setting password for the first time
|
||||||
|
[\#1090](https://github.com/matrix-org/matrix-react-sdk/pull/1090)
|
||||||
|
* i18n for setting password flow
|
||||||
|
[\#1089](https://github.com/matrix-org/matrix-react-sdk/pull/1089)
|
||||||
|
* remove mx_filterFlipColor from verified e2e icon so its not purple :/
|
||||||
|
[\#1088](https://github.com/matrix-org/matrix-react-sdk/pull/1088)
|
||||||
|
* width and height must be int otherwise synapse cries
|
||||||
|
[\#1083](https://github.com/matrix-org/matrix-react-sdk/pull/1083)
|
||||||
|
* remove RoomViewStore listener from MatrixChat on unmount
|
||||||
|
[\#1084](https://github.com/matrix-org/matrix-react-sdk/pull/1084)
|
||||||
|
* Add script to copy translations between files
|
||||||
|
[\#1082](https://github.com/matrix-org/matrix-react-sdk/pull/1082)
|
||||||
|
* Only process user_directory response if it's for the current query
|
||||||
|
[\#1081](https://github.com/matrix-org/matrix-react-sdk/pull/1081)
|
||||||
|
* Fix regressions with starting a 1-1.
|
||||||
|
[\#1080](https://github.com/matrix-org/matrix-react-sdk/pull/1080)
|
||||||
|
* allow forcing of TURN
|
||||||
|
[\#1079](https://github.com/matrix-org/matrix-react-sdk/pull/1079)
|
||||||
|
* Remove a bunch of dead code from react-sdk
|
||||||
|
[\#1077](https://github.com/matrix-org/matrix-react-sdk/pull/1077)
|
||||||
|
* Improve error logging/reporting in megolm import/export
|
||||||
|
[\#1061](https://github.com/matrix-org/matrix-react-sdk/pull/1061)
|
||||||
|
* Delinting
|
||||||
|
[\#1064](https://github.com/matrix-org/matrix-react-sdk/pull/1064)
|
||||||
|
* Show reason for a call hanging up unexpectedly.
|
||||||
|
[\#1071](https://github.com/matrix-org/matrix-react-sdk/pull/1071)
|
||||||
|
* Add reason for ban in room settings
|
||||||
|
[\#1072](https://github.com/matrix-org/matrix-react-sdk/pull/1072)
|
||||||
|
* adds mx_filterFlipColor so that the dark theme will invert this image
|
||||||
|
[\#1070](https://github.com/matrix-org/matrix-react-sdk/pull/1070)
|
||||||
|
|
||||||
|
Changes in [0.9.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.4) (2017-06-14)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.3...v0.9.4)
|
||||||
|
|
||||||
|
* Ask for email address after setting password for the first time
|
||||||
|
[\#1090](https://github.com/matrix-org/matrix-react-sdk/pull/1090)
|
||||||
|
* DM guessing: prefer oldest joined member
|
||||||
|
[\#1087](https://github.com/matrix-org/matrix-react-sdk/pull/1087)
|
||||||
|
* More translations
|
||||||
|
|
||||||
|
Changes in [0.9.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.3) (2017-06-12)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.3-rc.2...v0.9.3)
|
||||||
|
|
||||||
|
* Add more translations & fix some existing ones
|
||||||
|
|
||||||
|
Changes in [0.9.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.3-rc.2) (2017-06-09)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.3-rc.1...v0.9.3-rc.2)
|
||||||
|
|
||||||
|
* Fix flux dependency
|
||||||
|
* Fix translations on conference call bar
|
||||||
|
|
||||||
|
Changes in [0.9.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.3-rc.1) (2017-06-09)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.2...v0.9.3-rc.1)
|
||||||
|
|
||||||
|
* When ChatCreateOrReuseDialog is cancelled by a guest, go home
|
||||||
|
[\#1069](https://github.com/matrix-org/matrix-react-sdk/pull/1069)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1065](https://github.com/matrix-org/matrix-react-sdk/pull/1065)
|
||||||
|
* Goto /home when forgetting the last room
|
||||||
|
[\#1067](https://github.com/matrix-org/matrix-react-sdk/pull/1067)
|
||||||
|
* Default to home page when settings is closed
|
||||||
|
[\#1066](https://github.com/matrix-org/matrix-react-sdk/pull/1066)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1063](https://github.com/matrix-org/matrix-react-sdk/pull/1063)
|
||||||
|
* When joining, use a roomAlias if we have it
|
||||||
|
[\#1062](https://github.com/matrix-org/matrix-react-sdk/pull/1062)
|
||||||
|
* Control currently viewed event via RoomViewStore
|
||||||
|
[\#1058](https://github.com/matrix-org/matrix-react-sdk/pull/1058)
|
||||||
|
* Better error messages for login
|
||||||
|
[\#1060](https://github.com/matrix-org/matrix-react-sdk/pull/1060)
|
||||||
|
* Add remaining translations
|
||||||
|
[\#1056](https://github.com/matrix-org/matrix-react-sdk/pull/1056)
|
||||||
|
* Added button that copies code to clipboard
|
||||||
|
[\#1040](https://github.com/matrix-org/matrix-react-sdk/pull/1040)
|
||||||
|
* de-lint MegolmExportEncryption + test
|
||||||
|
[\#1059](https://github.com/matrix-org/matrix-react-sdk/pull/1059)
|
||||||
|
* Better RTL support
|
||||||
|
[\#1021](https://github.com/matrix-org/matrix-react-sdk/pull/1021)
|
||||||
|
* make mels emoji capable
|
||||||
|
[\#1057](https://github.com/matrix-org/matrix-react-sdk/pull/1057)
|
||||||
|
* Make travis check for lint on files which are clean to start with
|
||||||
|
[\#1055](https://github.com/matrix-org/matrix-react-sdk/pull/1055)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1053](https://github.com/matrix-org/matrix-react-sdk/pull/1053)
|
||||||
|
* Add some logging around switching rooms
|
||||||
|
[\#1054](https://github.com/matrix-org/matrix-react-sdk/pull/1054)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1052](https://github.com/matrix-org/matrix-react-sdk/pull/1052)
|
||||||
|
* Use user_directory endpoint to populate ChatInviteDialog
|
||||||
|
[\#1050](https://github.com/matrix-org/matrix-react-sdk/pull/1050)
|
||||||
|
* Various Analytics changes/fixes/improvements
|
||||||
|
[\#1046](https://github.com/matrix-org/matrix-react-sdk/pull/1046)
|
||||||
|
* Use an arrow function to allow `this`
|
||||||
|
[\#1051](https://github.com/matrix-org/matrix-react-sdk/pull/1051)
|
||||||
|
* New guest access
|
||||||
|
[\#937](https://github.com/matrix-org/matrix-react-sdk/pull/937)
|
||||||
|
* Translate src/components/structures
|
||||||
|
[\#1048](https://github.com/matrix-org/matrix-react-sdk/pull/1048)
|
||||||
|
* Cancel 'join room' action if 'log in' is clicked
|
||||||
|
[\#1049](https://github.com/matrix-org/matrix-react-sdk/pull/1049)
|
||||||
|
* fix copy and paste derp and rip out unused imports
|
||||||
|
[\#1015](https://github.com/matrix-org/matrix-react-sdk/pull/1015)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1042](https://github.com/matrix-org/matrix-react-sdk/pull/1042)
|
||||||
|
* Reset 'first sync' flag / promise on log in
|
||||||
|
[\#1041](https://github.com/matrix-org/matrix-react-sdk/pull/1041)
|
||||||
|
* Remove DM-guessing code (again)
|
||||||
|
[\#1036](https://github.com/matrix-org/matrix-react-sdk/pull/1036)
|
||||||
|
* Cancel deferred actions
|
||||||
|
[\#1039](https://github.com/matrix-org/matrix-react-sdk/pull/1039)
|
||||||
|
* Merge develop, add i18n for SetMxIdDialog
|
||||||
|
[\#1034](https://github.com/matrix-org/matrix-react-sdk/pull/1034)
|
||||||
|
* Defer an intention for creating a room
|
||||||
|
[\#1038](https://github.com/matrix-org/matrix-react-sdk/pull/1038)
|
||||||
|
* Fix 'create room' button
|
||||||
|
[\#1037](https://github.com/matrix-org/matrix-react-sdk/pull/1037)
|
||||||
|
* Always show the spinner during the first sync
|
||||||
|
[\#1033](https://github.com/matrix-org/matrix-react-sdk/pull/1033)
|
||||||
|
* Only view welcome user if we are not looking at a room
|
||||||
|
[\#1032](https://github.com/matrix-org/matrix-react-sdk/pull/1032)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1030](https://github.com/matrix-org/matrix-react-sdk/pull/1030)
|
||||||
|
* Keep deferred actions for view_user_settings and view_create_chat
|
||||||
|
[\#1031](https://github.com/matrix-org/matrix-react-sdk/pull/1031)
|
||||||
|
* Don't do a deferred start chat if user is welcome user
|
||||||
|
[\#1029](https://github.com/matrix-org/matrix-react-sdk/pull/1029)
|
||||||
|
* Introduce state `peekLoading` to avoid collision with `roomLoading`
|
||||||
|
[\#1028](https://github.com/matrix-org/matrix-react-sdk/pull/1028)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1016](https://github.com/matrix-org/matrix-react-sdk/pull/1016)
|
||||||
|
* Fix accepting a 3pid invite
|
||||||
|
[\#1013](https://github.com/matrix-org/matrix-react-sdk/pull/1013)
|
||||||
|
* Propagate room join errors to the UI
|
||||||
|
[\#1007](https://github.com/matrix-org/matrix-react-sdk/pull/1007)
|
||||||
|
* Implement /user/@userid:domain?action=chat
|
||||||
|
[\#1006](https://github.com/matrix-org/matrix-react-sdk/pull/1006)
|
||||||
|
* Show People/Rooms emptySubListTip even when total rooms !== 0
|
||||||
|
[\#967](https://github.com/matrix-org/matrix-react-sdk/pull/967)
|
||||||
|
* Fix to show the correct room
|
||||||
|
[\#995](https://github.com/matrix-org/matrix-react-sdk/pull/995)
|
||||||
|
* Remove cachedPassword from localStorage on_logged_out
|
||||||
|
[\#977](https://github.com/matrix-org/matrix-react-sdk/pull/977)
|
||||||
|
* Add /start to show the setMxId above HomePage
|
||||||
|
[\#964](https://github.com/matrix-org/matrix-react-sdk/pull/964)
|
||||||
|
* Allow pressing Enter to submit setMxId
|
||||||
|
[\#961](https://github.com/matrix-org/matrix-react-sdk/pull/961)
|
||||||
|
* add login link to SetMxIdDialog
|
||||||
|
[\#954](https://github.com/matrix-org/matrix-react-sdk/pull/954)
|
||||||
|
* Block user settings with view_set_mxid
|
||||||
|
[\#936](https://github.com/matrix-org/matrix-react-sdk/pull/936)
|
||||||
|
* Show "Something went wrong!" when errcode undefined
|
||||||
|
[\#935](https://github.com/matrix-org/matrix-react-sdk/pull/935)
|
||||||
|
* Reset store state when logging out
|
||||||
|
[\#930](https://github.com/matrix-org/matrix-react-sdk/pull/930)
|
||||||
|
* Set the displayname to the mxid once PWLU
|
||||||
|
[\#933](https://github.com/matrix-org/matrix-react-sdk/pull/933)
|
||||||
|
* Fix view_next_room, view_previous_room and view_indexed_room
|
||||||
|
[\#929](https://github.com/matrix-org/matrix-react-sdk/pull/929)
|
||||||
|
* Use RVS to indicate "joining" when setting a mxid
|
||||||
|
[\#928](https://github.com/matrix-org/matrix-react-sdk/pull/928)
|
||||||
|
* Don't show notif nag bar if guest
|
||||||
|
[\#932](https://github.com/matrix-org/matrix-react-sdk/pull/932)
|
||||||
|
* Show "Password" instead of "New Password"
|
||||||
|
[\#927](https://github.com/matrix-org/matrix-react-sdk/pull/927)
|
||||||
|
* Remove warm-fuzzy after setting mxid
|
||||||
|
[\#926](https://github.com/matrix-org/matrix-react-sdk/pull/926)
|
||||||
|
* Allow teamServerConfig to be missing
|
||||||
|
[\#925](https://github.com/matrix-org/matrix-react-sdk/pull/925)
|
||||||
|
* Remove GuestWarningBar
|
||||||
|
[\#923](https://github.com/matrix-org/matrix-react-sdk/pull/923)
|
||||||
|
* Make left panel better for new users (mk III)
|
||||||
|
[\#924](https://github.com/matrix-org/matrix-react-sdk/pull/924)
|
||||||
|
* Implement default welcome page and allow custom URL /w config
|
||||||
|
[\#922](https://github.com/matrix-org/matrix-react-sdk/pull/922)
|
||||||
|
* Implement a store for RoomView
|
||||||
|
[\#921](https://github.com/matrix-org/matrix-react-sdk/pull/921)
|
||||||
|
* Add prop to toggle whether new password input is autoFocused
|
||||||
|
[\#915](https://github.com/matrix-org/matrix-react-sdk/pull/915)
|
||||||
|
* Implement warm-fuzzy success dialog for SetMxIdDialog
|
||||||
|
[\#905](https://github.com/matrix-org/matrix-react-sdk/pull/905)
|
||||||
|
* Write some tests for the RTS UI
|
||||||
|
[\#893](https://github.com/matrix-org/matrix-react-sdk/pull/893)
|
||||||
|
* Make confirmation optional on ChangePassword
|
||||||
|
[\#890](https://github.com/matrix-org/matrix-react-sdk/pull/890)
|
||||||
|
* Remove "Current Password" input if mx_pass exists
|
||||||
|
[\#881](https://github.com/matrix-org/matrix-react-sdk/pull/881)
|
||||||
|
* Replace NeedToRegisterDialog /w SetMxIdDialog
|
||||||
|
[\#889](https://github.com/matrix-org/matrix-react-sdk/pull/889)
|
||||||
|
* Invite the welcome user after registration if configured
|
||||||
|
[\#882](https://github.com/matrix-org/matrix-react-sdk/pull/882)
|
||||||
|
* Prevent ROUs from creating new chats/new rooms
|
||||||
|
[\#879](https://github.com/matrix-org/matrix-react-sdk/pull/879)
|
||||||
|
* Redesign mxID chooser, add availability checking
|
||||||
|
[\#877](https://github.com/matrix-org/matrix-react-sdk/pull/877)
|
||||||
|
* Show password nag bar when user is PWLU
|
||||||
|
[\#864](https://github.com/matrix-org/matrix-react-sdk/pull/864)
|
||||||
|
* fix typo
|
||||||
|
[\#858](https://github.com/matrix-org/matrix-react-sdk/pull/858)
|
||||||
|
* Initial implementation: SetDisplayName -> SetMxIdDialog
|
||||||
|
[\#849](https://github.com/matrix-org/matrix-react-sdk/pull/849)
|
||||||
|
|
||||||
|
Changes in [0.9.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.2) (2017-06-06)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.1...v0.9.2)
|
||||||
|
|
||||||
|
* Hotfix: Allow password reset when logged in
|
||||||
|
[\#1044](https://github.com/matrix-org/matrix-react-sdk/pull/1044)
|
||||||
|
|
||||||
|
Changes in [0.9.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.1) (2017-06-02)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.0...v0.9.1)
|
||||||
|
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1012](https://github.com/matrix-org/matrix-react-sdk/pull/1012)
|
||||||
|
* typo, missing import and mis-casing
|
||||||
|
[\#1014](https://github.com/matrix-org/matrix-react-sdk/pull/1014)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1010](https://github.com/matrix-org/matrix-react-sdk/pull/1010)
|
||||||
|
|
||||||
|
Changes in [0.9.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.0) (2017-06-02)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.0-rc.2...v0.9.0)
|
||||||
|
|
||||||
|
* sync pt with pt_BR
|
||||||
|
[\#1009](https://github.com/matrix-org/matrix-react-sdk/pull/1009)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1008](https://github.com/matrix-org/matrix-react-sdk/pull/1008)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1003](https://github.com/matrix-org/matrix-react-sdk/pull/1003)
|
||||||
|
* allow hiding redactions, restoring old behaviour
|
||||||
|
[\#1004](https://github.com/matrix-org/matrix-react-sdk/pull/1004)
|
||||||
|
* Add missing translations
|
||||||
|
[\#1005](https://github.com/matrix-org/matrix-react-sdk/pull/1005)
|
||||||
|
|
||||||
|
Changes in [0.9.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.0-rc.2) (2017-06-02)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.0-rc.1...v0.9.0-rc.2)
|
||||||
|
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1002](https://github.com/matrix-org/matrix-react-sdk/pull/1002)
|
||||||
|
* webrtc config electron
|
||||||
|
[\#850](https://github.com/matrix-org/matrix-react-sdk/pull/850)
|
||||||
|
* enable useCompactLayout user setting an add a class when it's enabled
|
||||||
|
[\#986](https://github.com/matrix-org/matrix-react-sdk/pull/986)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#987](https://github.com/matrix-org/matrix-react-sdk/pull/987)
|
||||||
|
* Translation fixes for everything but src/components
|
||||||
|
[\#990](https://github.com/matrix-org/matrix-react-sdk/pull/990)
|
||||||
|
* Fix tests
|
||||||
|
[\#1001](https://github.com/matrix-org/matrix-react-sdk/pull/1001)
|
||||||
|
* Fix tests for PR #989
|
||||||
|
[\#999](https://github.com/matrix-org/matrix-react-sdk/pull/999)
|
||||||
|
* Revert "Revert "add labels to language picker""
|
||||||
|
[\#1000](https://github.com/matrix-org/matrix-react-sdk/pull/1000)
|
||||||
|
* maybe fixxy [Electron] external thing?
|
||||||
|
[\#997](https://github.com/matrix-org/matrix-react-sdk/pull/997)
|
||||||
|
* travisci: Don't run the riot-web tests if the react-sdk tests fail
|
||||||
|
[\#992](https://github.com/matrix-org/matrix-react-sdk/pull/992)
|
||||||
|
* Support 12hr time on DateSeparator
|
||||||
|
[\#991](https://github.com/matrix-org/matrix-react-sdk/pull/991)
|
||||||
|
* Revert "add labels to language picker"
|
||||||
|
[\#994](https://github.com/matrix-org/matrix-react-sdk/pull/994)
|
||||||
|
* Call MatrixClient.clearStores on logout
|
||||||
|
[\#983](https://github.com/matrix-org/matrix-react-sdk/pull/983)
|
||||||
|
* Matthew/room avatar event
|
||||||
|
[\#988](https://github.com/matrix-org/matrix-react-sdk/pull/988)
|
||||||
|
* add labels to language picker
|
||||||
|
[\#989](https://github.com/matrix-org/matrix-react-sdk/pull/989)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#981](https://github.com/matrix-org/matrix-react-sdk/pull/981)
|
||||||
|
|
||||||
|
Changes in [0.9.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.0-rc.1) (2017-06-01)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.9...v0.9.0-rc.1)
|
||||||
|
|
||||||
|
* Fix rare case where presence duration is undefined
|
||||||
|
[\#982](https://github.com/matrix-org/matrix-react-sdk/pull/982)
|
||||||
|
* add concept of platform handling loudNotifications (bings/pings/whatHaveYou)
|
||||||
|
[\#985](https://github.com/matrix-org/matrix-react-sdk/pull/985)
|
||||||
|
* Fixes to i18n code
|
||||||
|
[\#984](https://github.com/matrix-org/matrix-react-sdk/pull/984)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#978](https://github.com/matrix-org/matrix-react-sdk/pull/978)
|
||||||
|
* Add partial support for RTL languages
|
||||||
|
[\#955](https://github.com/matrix-org/matrix-react-sdk/pull/955)
|
||||||
|
* Added two strings to translate
|
||||||
|
[\#975](https://github.com/matrix-org/matrix-react-sdk/pull/975)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#976](https://github.com/matrix-org/matrix-react-sdk/pull/976)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#974](https://github.com/matrix-org/matrix-react-sdk/pull/974)
|
||||||
|
* Initial Electron Settings - for Auto Launch
|
||||||
|
[\#920](https://github.com/matrix-org/matrix-react-sdk/pull/920)
|
||||||
|
* Fix missing string in the room settings
|
||||||
|
[\#973](https://github.com/matrix-org/matrix-react-sdk/pull/973)
|
||||||
|
* fix error in i18n string
|
||||||
|
[\#972](https://github.com/matrix-org/matrix-react-sdk/pull/972)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#970](https://github.com/matrix-org/matrix-react-sdk/pull/970)
|
||||||
|
* Support 12hr time in full date
|
||||||
|
[\#971](https://github.com/matrix-org/matrix-react-sdk/pull/971)
|
||||||
|
* Add _tJsx()
|
||||||
|
[\#968](https://github.com/matrix-org/matrix-react-sdk/pull/968)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#966](https://github.com/matrix-org/matrix-react-sdk/pull/966)
|
||||||
|
* Remove space between time and AM/PM
|
||||||
|
[\#969](https://github.com/matrix-org/matrix-react-sdk/pull/969)
|
||||||
|
* Piwik Analytics
|
||||||
|
[\#948](https://github.com/matrix-org/matrix-react-sdk/pull/948)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#965](https://github.com/matrix-org/matrix-react-sdk/pull/965)
|
||||||
|
* Improve ChatInviteDialog perf by ditching fuse, using indexOf and
|
||||||
|
lastActiveTs()
|
||||||
|
[\#960](https://github.com/matrix-org/matrix-react-sdk/pull/960)
|
||||||
|
* Say "X removed the room name" instead of showing nothing
|
||||||
|
[\#958](https://github.com/matrix-org/matrix-react-sdk/pull/958)
|
||||||
|
* roomview/roomheader fixes
|
||||||
|
[\#959](https://github.com/matrix-org/matrix-react-sdk/pull/959)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#953](https://github.com/matrix-org/matrix-react-sdk/pull/953)
|
||||||
|
* fix i18n in a situation where navigator.languages=[]
|
||||||
|
[\#956](https://github.com/matrix-org/matrix-react-sdk/pull/956)
|
||||||
|
* `t_` -> `_t` fix typo
|
||||||
|
[\#957](https://github.com/matrix-org/matrix-react-sdk/pull/957)
|
||||||
|
* Change redact -> remove for clarity
|
||||||
|
[\#831](https://github.com/matrix-org/matrix-react-sdk/pull/831)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#950](https://github.com/matrix-org/matrix-react-sdk/pull/950)
|
||||||
|
* fix mis-linting - missed it in code review :(
|
||||||
|
[\#952](https://github.com/matrix-org/matrix-react-sdk/pull/952)
|
||||||
|
* i18n fixes
|
||||||
|
[\#951](https://github.com/matrix-org/matrix-react-sdk/pull/951)
|
||||||
|
* Message Forwarding
|
||||||
|
[\#812](https://github.com/matrix-org/matrix-react-sdk/pull/812)
|
||||||
|
* don't focus_composer on window focus
|
||||||
|
[\#944](https://github.com/matrix-org/matrix-react-sdk/pull/944)
|
||||||
|
* Fix vector-im/riot-web#4042
|
||||||
|
[\#947](https://github.com/matrix-org/matrix-react-sdk/pull/947)
|
||||||
|
* import _t, drop two unused imports
|
||||||
|
[\#946](https://github.com/matrix-org/matrix-react-sdk/pull/946)
|
||||||
|
* Fix punctuation in TextForEvent to be i18n'd consistently
|
||||||
|
[\#945](https://github.com/matrix-org/matrix-react-sdk/pull/945)
|
||||||
|
* actually wire up alwaysShowTimestamps
|
||||||
|
[\#940](https://github.com/matrix-org/matrix-react-sdk/pull/940)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#943](https://github.com/matrix-org/matrix-react-sdk/pull/943)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#942](https://github.com/matrix-org/matrix-react-sdk/pull/942)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#941](https://github.com/matrix-org/matrix-react-sdk/pull/941)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#938](https://github.com/matrix-org/matrix-react-sdk/pull/938)
|
||||||
|
* Fix PM being AM
|
||||||
|
[\#939](https://github.com/matrix-org/matrix-react-sdk/pull/939)
|
||||||
|
* pass call state through dispatcher, for poor electron
|
||||||
|
[\#918](https://github.com/matrix-org/matrix-react-sdk/pull/918)
|
||||||
|
* Translations!
|
||||||
|
[\#934](https://github.com/matrix-org/matrix-react-sdk/pull/934)
|
||||||
|
* Remove suffix and prefix from login input username
|
||||||
|
[\#906](https://github.com/matrix-org/matrix-react-sdk/pull/906)
|
||||||
|
* Kierangould/12hourtimestamp
|
||||||
|
[\#903](https://github.com/matrix-org/matrix-react-sdk/pull/903)
|
||||||
|
* Don't include src in the test resolve root
|
||||||
|
[\#931](https://github.com/matrix-org/matrix-react-sdk/pull/931)
|
||||||
|
* Make the linked versions open a new tab, turt2live complained :P
|
||||||
|
[\#910](https://github.com/matrix-org/matrix-react-sdk/pull/910)
|
||||||
|
* Fix lint errors in SlashCommands
|
||||||
|
[\#919](https://github.com/matrix-org/matrix-react-sdk/pull/919)
|
||||||
|
* autoFocus input box
|
||||||
|
[\#911](https://github.com/matrix-org/matrix-react-sdk/pull/911)
|
||||||
|
* Make travis test against riot-web new-guest-access
|
||||||
|
[\#917](https://github.com/matrix-org/matrix-react-sdk/pull/917)
|
||||||
|
* Add right-branch logic to travis test script
|
||||||
|
[\#916](https://github.com/matrix-org/matrix-react-sdk/pull/916)
|
||||||
|
* Group e2e keys into blocks of 4 characters
|
||||||
|
[\#914](https://github.com/matrix-org/matrix-react-sdk/pull/914)
|
||||||
|
* Factor out DeviceVerifyDialog
|
||||||
|
[\#913](https://github.com/matrix-org/matrix-react-sdk/pull/913)
|
||||||
|
* Fix 'missing page_type' error
|
||||||
|
[\#909](https://github.com/matrix-org/matrix-react-sdk/pull/909)
|
||||||
|
* code style update
|
||||||
|
[\#904](https://github.com/matrix-org/matrix-react-sdk/pull/904)
|
||||||
|
|
||||||
|
Changes in [0.8.9](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9) (2017-05-22)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.9-rc.1...v0.8.9)
|
||||||
|
|
||||||
|
* No changes
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [0.8.9-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9-rc.1) (2017-05-19)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8...v0.8.9-rc.1)
|
||||||
|
|
||||||
|
* Prevent an exception getting scroll node
|
||||||
|
[\#902](https://github.com/matrix-org/matrix-react-sdk/pull/902)
|
||||||
|
* Fix a few remaining snags with country dd
|
||||||
|
[\#901](https://github.com/matrix-org/matrix-react-sdk/pull/901)
|
||||||
|
* Add left_aligned class to CountryDropdown
|
||||||
|
[\#900](https://github.com/matrix-org/matrix-react-sdk/pull/900)
|
||||||
|
* Swap to new flag files (which are stored as GB.png)
|
||||||
|
[\#899](https://github.com/matrix-org/matrix-react-sdk/pull/899)
|
||||||
|
* Improve phone number country dropdown for registration and login (Act. 2,
|
||||||
|
Return of the Prefix)
|
||||||
|
[\#897](https://github.com/matrix-org/matrix-react-sdk/pull/897)
|
||||||
|
* Support for pasting files into normal composer
|
||||||
|
[\#892](https://github.com/matrix-org/matrix-react-sdk/pull/892)
|
||||||
|
* tell guests they can't use filepanel until they register
|
||||||
|
[\#887](https://github.com/matrix-org/matrix-react-sdk/pull/887)
|
||||||
|
* Prevent reskindex -w from running when file names have not changed
|
||||||
|
[\#888](https://github.com/matrix-org/matrix-react-sdk/pull/888)
|
||||||
|
* I broke UserSettings for webpack-dev-server
|
||||||
|
[\#884](https://github.com/matrix-org/matrix-react-sdk/pull/884)
|
||||||
|
* various fixes to RoomHeader
|
||||||
|
[\#880](https://github.com/matrix-org/matrix-react-sdk/pull/880)
|
||||||
|
* remove /me whether or not it has a space after it
|
||||||
|
[\#885](https://github.com/matrix-org/matrix-react-sdk/pull/885)
|
||||||
|
* show error if we can't set a filter because no room
|
||||||
|
[\#883](https://github.com/matrix-org/matrix-react-sdk/pull/883)
|
||||||
|
* Fix RM not updating if RR event unpaginated
|
||||||
|
[\#874](https://github.com/matrix-org/matrix-react-sdk/pull/874)
|
||||||
|
* change roomsettings wording
|
||||||
|
[\#878](https://github.com/matrix-org/matrix-react-sdk/pull/878)
|
||||||
|
* make reskindex windows friendly
|
||||||
|
[\#875](https://github.com/matrix-org/matrix-react-sdk/pull/875)
|
||||||
|
* Fixes 2 issues with Dialog closing
|
||||||
|
[\#867](https://github.com/matrix-org/matrix-react-sdk/pull/867)
|
||||||
|
* Automatic Reskindex
|
||||||
|
[\#871](https://github.com/matrix-org/matrix-react-sdk/pull/871)
|
||||||
|
* Put room name in 'leave room' confirmation dialog
|
||||||
|
[\#873](https://github.com/matrix-org/matrix-react-sdk/pull/873)
|
||||||
|
* Fix this/self fail in LeftPanel
|
||||||
|
[\#872](https://github.com/matrix-org/matrix-react-sdk/pull/872)
|
||||||
|
* Don't show null URL previews
|
||||||
|
[\#870](https://github.com/matrix-org/matrix-react-sdk/pull/870)
|
||||||
|
* Fix keys for AddressSelector
|
||||||
|
[\#869](https://github.com/matrix-org/matrix-react-sdk/pull/869)
|
||||||
|
* Make left panel better for new users (mk II)
|
||||||
|
[\#859](https://github.com/matrix-org/matrix-react-sdk/pull/859)
|
||||||
|
* Explicitly save composer content onUnload
|
||||||
|
[\#866](https://github.com/matrix-org/matrix-react-sdk/pull/866)
|
||||||
|
* Warn on unload
|
||||||
|
[\#851](https://github.com/matrix-org/matrix-react-sdk/pull/851)
|
||||||
|
* Log deviceid at login
|
||||||
|
[\#862](https://github.com/matrix-org/matrix-react-sdk/pull/862)
|
||||||
|
* Guests can't send RR so no point trying
|
||||||
|
[\#860](https://github.com/matrix-org/matrix-react-sdk/pull/860)
|
||||||
|
* Remove babelcheck
|
||||||
|
[\#861](https://github.com/matrix-org/matrix-react-sdk/pull/861)
|
||||||
|
* T3chguy/settings versions improvements
|
||||||
|
[\#857](https://github.com/matrix-org/matrix-react-sdk/pull/857)
|
||||||
|
* Change max-len 90->120
|
||||||
|
[\#852](https://github.com/matrix-org/matrix-react-sdk/pull/852)
|
||||||
|
* Remove DM-guessing code
|
||||||
|
[\#829](https://github.com/matrix-org/matrix-react-sdk/pull/829)
|
||||||
|
* Fix jumping to an unread event when in MELS
|
||||||
|
[\#855](https://github.com/matrix-org/matrix-react-sdk/pull/855)
|
||||||
|
* Validate phone number on login
|
||||||
|
[\#856](https://github.com/matrix-org/matrix-react-sdk/pull/856)
|
||||||
|
* Failed to enable HTML5 Notifications Error Dialogs
|
||||||
|
[\#827](https://github.com/matrix-org/matrix-react-sdk/pull/827)
|
||||||
|
* Pin filesize ver to fix break upstream
|
||||||
|
[\#854](https://github.com/matrix-org/matrix-react-sdk/pull/854)
|
||||||
|
* Improve RoomDirectory Look & Feel
|
||||||
|
[\#848](https://github.com/matrix-org/matrix-react-sdk/pull/848)
|
||||||
|
* Only show jumpToReadMarker bar when RM !== RR
|
||||||
|
[\#845](https://github.com/matrix-org/matrix-react-sdk/pull/845)
|
||||||
|
* Allow MELS to have its own RM
|
||||||
|
[\#846](https://github.com/matrix-org/matrix-react-sdk/pull/846)
|
||||||
|
* Use document.onkeydown instead of onkeypress
|
||||||
|
[\#844](https://github.com/matrix-org/matrix-react-sdk/pull/844)
|
||||||
|
* (Room)?Avatar: Request 96x96 avatars on high DPI screens
|
||||||
|
[\#808](https://github.com/matrix-org/matrix-react-sdk/pull/808)
|
||||||
|
* Add mx_EventTile_emote class
|
||||||
|
[\#842](https://github.com/matrix-org/matrix-react-sdk/pull/842)
|
||||||
|
* Fix dialog reappearing after hitting Enter
|
||||||
|
[\#841](https://github.com/matrix-org/matrix-react-sdk/pull/841)
|
||||||
|
* Fix spinner that shows until the first sync
|
||||||
|
[\#840](https://github.com/matrix-org/matrix-react-sdk/pull/840)
|
||||||
|
* Show spinner until first sync has completed
|
||||||
|
[\#839](https://github.com/matrix-org/matrix-react-sdk/pull/839)
|
||||||
|
* Style fixes for LoggedInView
|
||||||
|
[\#838](https://github.com/matrix-org/matrix-react-sdk/pull/838)
|
||||||
|
* Fix specifying custom server for registration
|
||||||
|
[\#834](https://github.com/matrix-org/matrix-react-sdk/pull/834)
|
||||||
|
* Improve country dropdown UX and expose +prefix
|
||||||
|
[\#833](https://github.com/matrix-org/matrix-react-sdk/pull/833)
|
||||||
|
* Fix user settings store
|
||||||
|
[\#836](https://github.com/matrix-org/matrix-react-sdk/pull/836)
|
||||||
|
* show the room name in the UDE Dialog
|
||||||
|
[\#832](https://github.com/matrix-org/matrix-react-sdk/pull/832)
|
||||||
|
* summarise profile changes in MELS
|
||||||
|
[\#826](https://github.com/matrix-org/matrix-react-sdk/pull/826)
|
||||||
|
* Transform h1 and h2 tags to h3 tags
|
||||||
|
[\#820](https://github.com/matrix-org/matrix-react-sdk/pull/820)
|
||||||
|
* limit our keyboard shortcut modifiers correctly
|
||||||
|
[\#825](https://github.com/matrix-org/matrix-react-sdk/pull/825)
|
||||||
|
* Specify cross platform regexes and add olm to noParse
|
||||||
|
[\#823](https://github.com/matrix-org/matrix-react-sdk/pull/823)
|
||||||
|
* Remember element that was in focus before rendering dialog
|
||||||
|
[\#822](https://github.com/matrix-org/matrix-react-sdk/pull/822)
|
||||||
|
* move user settings outward and use built in read receipts disabling
|
||||||
|
[\#824](https://github.com/matrix-org/matrix-react-sdk/pull/824)
|
||||||
|
* File Download Consistency
|
||||||
|
[\#802](https://github.com/matrix-org/matrix-react-sdk/pull/802)
|
||||||
|
* Show Access Token under Advanced in Settings
|
||||||
|
[\#806](https://github.com/matrix-org/matrix-react-sdk/pull/806)
|
||||||
|
* Link tags/commit hashes in the UserSettings version section
|
||||||
|
[\#810](https://github.com/matrix-org/matrix-react-sdk/pull/810)
|
||||||
|
* On return to RoomView from auxPanel, send focus back to Composer
|
||||||
|
[\#813](https://github.com/matrix-org/matrix-react-sdk/pull/813)
|
||||||
|
* Change presence status labels to 'for' instead of 'ago'
|
||||||
|
[\#817](https://github.com/matrix-org/matrix-react-sdk/pull/817)
|
||||||
|
* Disable Scalar Integrations if urls passed to it are falsey
|
||||||
|
[\#816](https://github.com/matrix-org/matrix-react-sdk/pull/816)
|
||||||
|
* Add option to hide other people's read receipts.
|
||||||
|
[\#818](https://github.com/matrix-org/matrix-react-sdk/pull/818)
|
||||||
|
* Add option to not send typing notifications
|
||||||
|
[\#819](https://github.com/matrix-org/matrix-react-sdk/pull/819)
|
||||||
|
* Sync RM across instances of Riot
|
||||||
|
[\#805](https://github.com/matrix-org/matrix-react-sdk/pull/805)
|
||||||
|
* First iteration on improving login UI
|
||||||
|
[\#811](https://github.com/matrix-org/matrix-react-sdk/pull/811)
|
||||||
|
* focus on composer after jumping to bottom
|
||||||
|
[\#809](https://github.com/matrix-org/matrix-react-sdk/pull/809)
|
||||||
|
* Improve RoomList performance via side-stepping React
|
||||||
|
[\#807](https://github.com/matrix-org/matrix-react-sdk/pull/807)
|
||||||
|
* Don't show link preview when link is inside of a quote
|
||||||
|
[\#762](https://github.com/matrix-org/matrix-react-sdk/pull/762)
|
||||||
|
* Escape closes UserSettings
|
||||||
|
[\#765](https://github.com/matrix-org/matrix-react-sdk/pull/765)
|
||||||
|
* Implement user power-level changes in timeline
|
||||||
|
[\#794](https://github.com/matrix-org/matrix-react-sdk/pull/794)
|
||||||
|
|
||||||
Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25)
|
Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25)
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8)
|
||||||
|
|
|
@ -24,6 +24,10 @@ In the interim, `vector-im/riot-web` and `matrix-org/matrix-react-sdk` should
|
||||||
be considered as a single project (for instance, matrix-react-sdk bugs
|
be considered as a single project (for instance, matrix-react-sdk bugs
|
||||||
are currently filed against vector-im/riot-web rather than this project).
|
are currently filed against vector-im/riot-web rather than this project).
|
||||||
|
|
||||||
|
Translation Status
|
||||||
|
==================
|
||||||
|
[![translationsstatus](https://translate.nordgedanken.de/widgets/riot-web/-/multi-auto.svg)](https://translate.nordgedanken.de/engage/riot-web/?utm_source=widget)
|
||||||
|
|
||||||
Developer Guide
|
Developer Guide
|
||||||
===============
|
===============
|
||||||
|
|
||||||
|
@ -190,4 +194,3 @@ Alternative instructions:
|
||||||
* Create an index.html file pulling in your compiled javascript and the
|
* Create an index.html file pulling in your compiled javascript and the
|
||||||
CSS bundle from the skin you use. For now, you'll also need to manually
|
CSS bundle from the skin you use. For now, you'll also need to manually
|
||||||
import CSS from any skins that your skin inherts from.
|
import CSS from any skins that your skin inherts from.
|
||||||
|
|
||||||
|
|
|
@ -69,25 +69,41 @@ General Style
|
||||||
console.log("I am a fish"); // Bad
|
console.log("I am a fish"); // Bad
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
- No new line before else, catch, finally, etc:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (x) {
|
||||||
|
console.log("I am a fish");
|
||||||
|
} else {
|
||||||
|
console.log("I am a chimp"); // Good
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x) {
|
||||||
|
console.log("I am a fish");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("I am a chimp"); // Bad
|
||||||
|
}
|
||||||
|
```
|
||||||
- Declare one variable per var statement (consistent with Node). Unless they
|
- Declare one variable per var statement (consistent with Node). Unless they
|
||||||
are simple and closely related. If you put the next declaration on a new line,
|
are simple and closely related. If you put the next declaration on a new line,
|
||||||
treat yourself to another `var`:
|
treat yourself to another `var`:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var key = "foo",
|
const key = "foo",
|
||||||
comparator = function(x, y) {
|
comparator = function(x, y) {
|
||||||
return x - y;
|
return x - y;
|
||||||
}; // Bad
|
}; // Bad
|
||||||
|
|
||||||
var key = "foo";
|
const key = "foo";
|
||||||
var comparator = function(x, y) {
|
const comparator = function(x, y) {
|
||||||
return x - y;
|
return x - y;
|
||||||
}; // Good
|
}; // Good
|
||||||
|
|
||||||
var x = 0, y = 0; // Fine
|
let x = 0, y = 0; // Fine
|
||||||
|
|
||||||
var x = 0;
|
let x = 0;
|
||||||
var y = 0; // Also fine
|
let y = 0; // Also fine
|
||||||
```
|
```
|
||||||
- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.:
|
- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.:
|
||||||
|
|
||||||
|
|
1
header
1
header
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
export KARMAFLAGS="--no-colors"
|
|
||||||
export NVM_DIR="$HOME/.nvm"
|
export NVM_DIR="$HOME/.nvm"
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||||
nvm use 4
|
nvm use 4
|
||||||
|
@ -16,11 +15,16 @@ npm install
|
||||||
(cd node_modules/matrix-js-sdk && npm install)
|
(cd node_modules/matrix-js-sdk && npm install)
|
||||||
|
|
||||||
# run the mocha tests
|
# run the mocha tests
|
||||||
npm run test
|
npm run test -- --no-colors
|
||||||
|
|
||||||
# run eslint
|
# run eslint
|
||||||
npm run lintall -- -f checkstyle -o eslint.xml || true
|
npm run lintall -- -f checkstyle -o eslint.xml || true
|
||||||
|
|
||||||
|
# re-run the linter, excluding any files known to have errors or warnings.
|
||||||
|
./node_modules/.bin/eslint --max-warnings 0 \
|
||||||
|
--ignore-path .eslintignore.errorfiles \
|
||||||
|
src test
|
||||||
|
|
||||||
# delete the old tarball, if it exists
|
# delete the old tarball, if it exists
|
||||||
rm -f matrix-react-sdk-*.tgz
|
rm -f matrix-react-sdk-*.tgz
|
||||||
|
|
||||||
|
|
|
@ -55,11 +55,18 @@ module.exports = function (config) {
|
||||||
// some images to reduce noise from the tests
|
// some images to reduce noise from the tests
|
||||||
{pattern: 'test/img/*', watched: false, included: false,
|
{pattern: 'test/img/*', watched: false, included: false,
|
||||||
served: true, nocache: false},
|
served: true, nocache: false},
|
||||||
|
// translation files
|
||||||
|
{pattern: 'src/i18n/strings/*', watcheed: false, included: false, served: true},
|
||||||
|
{pattern: 'test/i18n/*', watched: false, included: false, served: true},
|
||||||
],
|
],
|
||||||
|
|
||||||
// redirect img links to the karma server
|
|
||||||
proxies: {
|
proxies: {
|
||||||
|
// redirect img links to the karma server
|
||||||
"/img/": "/base/test/img/",
|
"/img/": "/base/test/img/",
|
||||||
|
// special languages.json file for the tests
|
||||||
|
"/i18n/languages.json": "/base/test/i18n/languages.json",
|
||||||
|
// and redirect i18n requests
|
||||||
|
"/i18n/": "/base/src/i18n/strings/",
|
||||||
},
|
},
|
||||||
|
|
||||||
// list of files to exclude
|
// list of files to exclude
|
||||||
|
@ -86,7 +93,18 @@ module.exports = function (config) {
|
||||||
// test results reporter to use
|
// test results reporter to use
|
||||||
// possible values: 'dots', 'progress'
|
// possible values: 'dots', 'progress'
|
||||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
// 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
|
// web server port
|
||||||
port: 9876,
|
port: 9876,
|
||||||
|
@ -97,7 +115,10 @@ module.exports = function (config) {
|
||||||
// level of logging
|
// level of logging
|
||||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR ||
|
// possible values: config.LOG_DISABLE || config.LOG_ERROR ||
|
||||||
// config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
// 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
|
// enable / disable watching file and executing tests whenever any file
|
||||||
// changes
|
// changes
|
||||||
|
@ -109,11 +130,25 @@ module.exports = function (config) {
|
||||||
browsers: [
|
browsers: [
|
||||||
'Chrome',
|
'Chrome',
|
||||||
//'PhantomJS',
|
//'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
|
// Continuous Integration mode
|
||||||
// if true, Karma captures browsers, runs the tests and exits
|
// if true, Karma captures browsers, runs the tests and exits
|
||||||
singleRun: true,
|
// singleRun: false,
|
||||||
|
|
||||||
// Concurrency level
|
// Concurrency level
|
||||||
// how many browser should be started simultaneous
|
// how many browser should be started simultaneous
|
||||||
|
@ -135,17 +170,24 @@ module.exports = function (config) {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
noParse: [
|
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
|
// don't parse the languages within highlight.js. They
|
||||||
// cause stack overflows
|
// cause stack overflows
|
||||||
// (https://github.com/webpack/webpack/issues/1721), and
|
// (https://github.com/webpack/webpack/issues/1721), and
|
||||||
// there is no need for webpack to parse them - they can
|
// there is no need for webpack to parse them - they can
|
||||||
// just be included as-is.
|
// 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
|
// also disable parsing for sinon, because it
|
||||||
// tries to do voodoo with 'require' which upsets
|
// tries to do voodoo with 'require' which upsets
|
||||||
// webpack (https://github.com/webpack/webpack/issues/304)
|
// webpack (https://github.com/webpack/webpack/issues/304)
|
||||||
/sinon\/pkg\/sinon\.js$/,
|
/sinon[\\\/]pkg[\\\/]sinon\.js$/,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
@ -159,11 +201,15 @@ module.exports = function (config) {
|
||||||
'sinon': 'sinon/pkg/sinon.js',
|
'sinon': 'sinon/pkg/sinon.js',
|
||||||
},
|
},
|
||||||
root: [
|
root: [
|
||||||
path.resolve('./src'),
|
|
||||||
path.resolve('./test'),
|
path.resolve('./test'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
devtool: 'inline-source-map',
|
devtool: 'inline-source-map',
|
||||||
|
externals: {
|
||||||
|
// Don't try to bundle electron: leave it as a commonjs dependency
|
||||||
|
// (the 'commonjs' here means it will output a 'require')
|
||||||
|
"electron": "commonjs electron",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
webpackMiddleware: {
|
webpackMiddleware: {
|
||||||
|
|
44
package.json
44
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.8.8",
|
"version": "0.9.7",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -31,45 +31,51 @@
|
||||||
"reskindex": "scripts/reskindex.js"
|
"reskindex": "scripts/reskindex.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"reskindex": "scripts/reskindex.js -h header",
|
"reskindex": "node scripts/reskindex.js -h header",
|
||||||
"build": "node scripts/babelcheck.js && babel src -d lib --source-maps",
|
"reskindex:watch": "node scripts/reskindex.js -h header -w",
|
||||||
"start": "node scripts/babelcheck.js && babel src -w -d lib --source-maps",
|
"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/",
|
"lint": "eslint src/",
|
||||||
"lintall": "eslint src/ test/",
|
"lintall": "eslint src/ test/",
|
||||||
"clean": "rimraf lib",
|
"clean": "rimraf lib",
|
||||||
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt",
|
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt",
|
||||||
"test": "karma start $KARMAFLAGS --browsers PhantomJS",
|
"test": "karma start --single-run=true --browsers ChromeHeadless",
|
||||||
"test-multi": "karma start $KARMAFLAGS --single-run=false"
|
"test-multi": "karma start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-runtime": "^6.11.6",
|
"babel-runtime": "^6.11.6",
|
||||||
|
"bluebird": "^3.5.0",
|
||||||
"blueimp-canvas-to-blob": "^3.5.0",
|
"blueimp-canvas-to-blob": "^3.5.0",
|
||||||
"browser-encrypt-attachment": "^0.3.0",
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
"browser-request": "^0.3.3",
|
"browser-request": "^0.3.3",
|
||||||
"classnames": "^2.1.2",
|
"classnames": "^2.1.2",
|
||||||
"commonmark": "^0.27.0",
|
"commonmark": "^0.27.0",
|
||||||
"draft-js": "^0.8.1",
|
"counterpart": "^0.18.0",
|
||||||
|
"draft-js": "^0.10.1",
|
||||||
"draft-js-export-html": "^0.5.0",
|
"draft-js-export-html": "^0.5.0",
|
||||||
"draft-js-export-markdown": "^0.2.0",
|
"draft-js-export-markdown": "^0.2.0",
|
||||||
"emojione": "2.2.3",
|
"emojione": "2.2.7",
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
"filesize": "^3.1.2",
|
"filesize": "3.5.6",
|
||||||
"flux": "^2.0.3",
|
"flux": "2.1.1",
|
||||||
"fuse.js": "^2.2.0",
|
"fuse.js": "^2.2.0",
|
||||||
"glob": "^5.0.14",
|
"glob": "^5.0.14",
|
||||||
"highlight.js": "^8.9.1",
|
"highlight.js": "^8.9.1",
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.3",
|
"linkifyjs": "^2.1.3",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"matrix-js-sdk": "0.7.7",
|
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"q": "^1.4.1",
|
"prop-types": "^15.5.8",
|
||||||
"react": "^15.4.0",
|
"react": "^15.4.0",
|
||||||
"react-addons-css-transition-group": "15.3.2",
|
"react-addons-css-transition-group": "15.3.2",
|
||||||
"react-dom": "^15.4.0",
|
"react-dom": "^15.4.0",
|
||||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
"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",
|
"text-encoding-utf-8": "^1.0.1",
|
||||||
|
"url": "^0.11.0",
|
||||||
"velocity-vector": "vector-im/velocity#059e3b2",
|
"velocity-vector": "vector-im/velocity#059e3b2",
|
||||||
"whatwg-fetch": "^1.0.0"
|
"whatwg-fetch": "^1.0.0"
|
||||||
},
|
},
|
||||||
|
@ -79,7 +85,7 @@
|
||||||
"babel-eslint": "^6.1.2",
|
"babel-eslint": "^6.1.2",
|
||||||
"babel-loader": "^6.2.5",
|
"babel-loader": "^6.2.5",
|
||||||
"babel-plugin-add-module-exports": "^0.2.1",
|
"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-class-properties": "^6.16.0",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
||||||
"babel-plugin-transform-runtime": "^6.15.0",
|
"babel-plugin-transform-runtime": "^6.15.0",
|
||||||
|
@ -88,6 +94,7 @@
|
||||||
"babel-preset-es2016": "^6.11.3",
|
"babel-preset-es2016": "^6.11.3",
|
||||||
"babel-preset-es2017": "^6.14.0",
|
"babel-preset-es2017": "^6.14.0",
|
||||||
"babel-preset-react": "^6.11.1",
|
"babel-preset-react": "^6.11.1",
|
||||||
|
"chokidar": "^1.6.1",
|
||||||
"eslint": "^3.13.1",
|
"eslint": "^3.13.1",
|
||||||
"eslint-config-google": "^0.7.1",
|
"eslint-config-google": "^0.7.1",
|
||||||
"eslint-plugin-babel": "^4.0.1",
|
"eslint-plugin-babel": "^4.0.1",
|
||||||
|
@ -95,16 +102,19 @@
|
||||||
"eslint-plugin-react": "^6.9.0",
|
"eslint-plugin-react": "^6.9.0",
|
||||||
"expect": "^1.16.0",
|
"expect": "^1.16.0",
|
||||||
"json-loader": "^0.5.3",
|
"json-loader": "^0.5.3",
|
||||||
"karma": "^0.13.22",
|
"karma": "^1.7.0",
|
||||||
"karma-chrome-launcher": "^0.2.3",
|
"karma-chrome-launcher": "^0.2.3",
|
||||||
"karma-cli": "^0.1.2",
|
"karma-cli": "^0.1.2",
|
||||||
"karma-junit-reporter": "^0.4.1",
|
"karma-junit-reporter": "^0.4.1",
|
||||||
|
"karma-logcapture-reporter": "0.0.1",
|
||||||
"karma-mocha": "^0.2.2",
|
"karma-mocha": "^0.2.2",
|
||||||
"karma-phantomjs-launcher": "^1.0.0",
|
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"karma-sourcemap-loader": "^0.3.7",
|
||||||
|
"karma-spec-reporter": "^0.0.31",
|
||||||
|
"karma-summary-reporter": "^1.3.3",
|
||||||
"karma-webpack": "^1.7.0",
|
"karma-webpack": "^1.7.0",
|
||||||
|
"matrix-react-test-utils": "^0.1.1",
|
||||||
"mocha": "^2.4.5",
|
"mocha": "^2.4.5",
|
||||||
"phantomjs-prebuilt": "^2.1.7",
|
"parallelshell": "^1.2.0",
|
||||||
"react-addons-test-utils": "^15.4.0",
|
"react-addons-test-utils": "^15.4.0",
|
||||||
"require-json": "0.0.1",
|
"require-json": "0.0.1",
|
||||||
"rimraf": "^2.4.3",
|
"rimraf": "^2.4.3",
|
||||||
|
|
|
@ -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
192
scripts/check-i18n.pl
Executable 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
47
scripts/copy-i18n.py
Executable 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)
|
26
scripts/emoji-data-strip.js
Normal file
26
scripts/emoji-data-strip.js
Normal 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
114
scripts/fix-i18n.pl
Executable 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 $_;
|
||||||
|
}
|
21
scripts/generate-eslint-error-ignore-file
Executable file
21
scripts/generate-eslint-error-ignore-file
Executable 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"
|
|
@ -1,20 +1,27 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var glob = require('glob');
|
var glob = require('glob');
|
||||||
|
|
||||||
var args = require('optimist').argv;
|
var args = require('optimist').argv;
|
||||||
|
var chokidar = require('chokidar');
|
||||||
var header = args.h || args.header;
|
|
||||||
|
|
||||||
var componentsDir = path.join('src', 'components');
|
|
||||||
|
|
||||||
var componentIndex = path.join('src', 'component-index.js');
|
var componentIndex = path.join('src', 'component-index.js');
|
||||||
|
var componentIndexTmp = componentIndex+".tmp";
|
||||||
|
var componentsDir = path.join('src', 'components');
|
||||||
|
var componentGlob = '**/*.js';
|
||||||
|
var prevFiles = [];
|
||||||
|
|
||||||
|
function reskindex() {
|
||||||
|
var files = glob.sync(componentGlob, {cwd: componentsDir}).sort();
|
||||||
|
if (!filesHaveChanged(files, prevFiles)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prevFiles = files;
|
||||||
|
|
||||||
|
var header = args.h || args.header;
|
||||||
var packageJson = JSON.parse(fs.readFileSync('./package.json'));
|
var packageJson = JSON.parse(fs.readFileSync('./package.json'));
|
||||||
|
|
||||||
var strm = fs.createWriteStream(componentIndex);
|
var strm = fs.createWriteStream(componentIndexTmp);
|
||||||
|
|
||||||
if (header) {
|
if (header) {
|
||||||
strm.write(fs.readFileSync(header));
|
strm.write(fs.readFileSync(header));
|
||||||
|
@ -26,18 +33,21 @@ strm.write(" * THIS FILE IS AUTO-GENERATED\n");
|
||||||
strm.write(" * You can edit it you like, but your changes will be overwritten,\n");
|
strm.write(" * You can edit it you like, but your changes will be overwritten,\n");
|
||||||
strm.write(" * so you'd just be trying to swim upstream like a salmon.\n");
|
strm.write(" * so you'd just be trying to swim upstream like a salmon.\n");
|
||||||
strm.write(" * You are not a salmon.\n");
|
strm.write(" * You are not a salmon.\n");
|
||||||
strm.write(" *\n");
|
|
||||||
strm.write(" * To update it, run:\n");
|
|
||||||
strm.write(" * ./reskindex.js -h header\n");
|
|
||||||
strm.write(" */\n\n");
|
strm.write(" */\n\n");
|
||||||
|
|
||||||
if (packageJson['matrix-react-parent']) {
|
if (packageJson['matrix-react-parent']) {
|
||||||
strm.write("module.exports.components = require('"+packageJson['matrix-react-parent']+"/lib/component-index').components;\n\n");
|
const parentIndex = packageJson['matrix-react-parent'] +
|
||||||
|
'/lib/component-index';
|
||||||
|
strm.write(
|
||||||
|
`let components = require('${parentIndex}').components;
|
||||||
|
if (!components) {
|
||||||
|
throw new Error("'${parentIndex}' didn't export components");
|
||||||
|
}
|
||||||
|
`);
|
||||||
} else {
|
} else {
|
||||||
strm.write("module.exports.components = {};\n");
|
strm.write("let components = {};\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = glob.sync('**/*.js', {cwd: componentsDir}).sort();
|
|
||||||
for (var i = 0; i < files.length; ++i) {
|
for (var i = 0; i < files.length; ++i) {
|
||||||
var file = files[i].replace('.js', '');
|
var file = files[i].replace('.js', '');
|
||||||
|
|
||||||
|
@ -45,9 +55,45 @@ for (var i = 0; i < files.length; ++i) {
|
||||||
var importName = moduleName.replace(/\./g, "$");
|
var importName = moduleName.replace(/\./g, "$");
|
||||||
|
|
||||||
strm.write("import " + importName + " from './components/" + file + "';\n");
|
strm.write("import " + importName + " from './components/" + file + "';\n");
|
||||||
strm.write(importName + " && (module.exports.components['"+moduleName+"'] = " + importName + ");");
|
strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");");
|
||||||
strm.write('\n');
|
strm.write('\n');
|
||||||
strm.uncork();
|
strm.uncork();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strm.write("export {components};\n");
|
||||||
strm.end();
|
strm.end();
|
||||||
|
fs.rename(componentIndexTmp, componentIndex, function(err) {
|
||||||
|
if(err) {
|
||||||
|
console.error("Error moving new index into place: " + err);
|
||||||
|
} else {
|
||||||
|
console.log('Reskindex: completed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expects both arrays of file names to be sorted
|
||||||
|
function filesHaveChanged(files, prevFiles) {
|
||||||
|
if (files.length !== prevFiles.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Check for name changes
|
||||||
|
for (var i = 0; i < files.length; i++) {
|
||||||
|
if (prevFiles[i] !== files[i]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -w indicates watch mode where any FS events will trigger reskindex
|
||||||
|
if (!args.w) {
|
||||||
|
reskindex();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var watchDebouncer = null;
|
||||||
|
chokidar.watch(path.join(componentsDir, componentGlob)).on('all', (event, path) => {
|
||||||
|
if (path === componentIndex) return;
|
||||||
|
if (watchDebouncer) clearTimeout(watchDebouncer);
|
||||||
|
watchDebouncer = setTimeout(reskindex, 1000);
|
||||||
|
});
|
||||||
|
|
11
scripts/travis.sh
Executable file
11
scripts/travis.sh
Executable 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
|
|
@ -15,7 +15,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows a user to add a third party identifier to their Home Server and,
|
* Allows a user to add a third party identifier to their Home Server and,
|
||||||
|
@ -43,8 +44,8 @@ class AddThreepid {
|
||||||
this.sessionId = res.sid;
|
this.sessionId = res.sid;
|
||||||
return res;
|
return res;
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
if (err.errcode == 'M_THREEPID_IN_USE') {
|
if (err.errcode === 'M_THREEPID_IN_USE') {
|
||||||
err.message = "This email address is already in use";
|
err.message = _t('This email address is already in use');
|
||||||
} else if (err.httpStatus) {
|
} else if (err.httpStatus) {
|
||||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||||
}
|
}
|
||||||
|
@ -68,8 +69,8 @@ class AddThreepid {
|
||||||
this.sessionId = res.sid;
|
this.sessionId = res.sid;
|
||||||
return res;
|
return res;
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
if (err.errcode == 'M_THREEPID_IN_USE') {
|
if (err.errcode === 'M_THREEPID_IN_USE') {
|
||||||
err.message = "This phone number is already in use";
|
err.message = _t('This phone number is already in use');
|
||||||
} else if (err.httpStatus) {
|
} else if (err.httpStatus) {
|
||||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||||
}
|
}
|
||||||
|
@ -84,16 +85,15 @@ class AddThreepid {
|
||||||
* the request failed.
|
* the request failed.
|
||||||
*/
|
*/
|
||||||
checkEmailLinkClicked() {
|
checkEmailLinkClicked() {
|
||||||
var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||||
return MatrixClientPeg.get().addThreePid({
|
return MatrixClientPeg.get().addThreePid({
|
||||||
sid: this.sessionId,
|
sid: this.sessionId,
|
||||||
client_secret: this.clientSecret,
|
client_secret: this.clientSecret,
|
||||||
id_server: identityServerDomain
|
id_server: identityServerDomain,
|
||||||
}, this.bind).catch(function(err) {
|
}, this.bind).catch(function(err) {
|
||||||
if (err.httpStatus === 401) {
|
if (err.httpStatus === 401) {
|
||||||
err.message = "Failed to verify email address: make sure you clicked the link in the email";
|
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
|
||||||
}
|
} else if (err.httpStatus) {
|
||||||
else if (err.httpStatus) {
|
|
||||||
err.message += ` (Status ${err.httpStatus})`;
|
err.message += ` (Status ${err.httpStatus})`;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -103,6 +103,7 @@ class AddThreepid {
|
||||||
/**
|
/**
|
||||||
* Takes a phone number verification code as entered by the user and validates
|
* 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.
|
* 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
|
* @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
|
* with a "message" property which contains a human-readable message detailing why
|
||||||
* the request failed.
|
* the request failed.
|
||||||
|
@ -118,7 +119,7 @@ class AddThreepid {
|
||||||
return MatrixClientPeg.get().addThreePid({
|
return MatrixClientPeg.get().addThreePid({
|
||||||
sid: this.sessionId,
|
sid: this.sessionId,
|
||||||
client_secret: this.clientSecret,
|
client_secret: this.clientSecret,
|
||||||
id_server: identityServerDomain
|
id_server: identityServerDomain,
|
||||||
}, this.bind);
|
}, this.bind);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
153
src/Analytics.js
Normal file
153
src/Analytics.js
Normal 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;
|
|
@ -15,18 +15,18 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
var ContentRepo = require("matrix-js-sdk").ContentRepo;
|
import {ContentRepo} from 'matrix-js-sdk';
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
avatarUrlForMember: function(member, width, height, resizeMethod) {
|
avatarUrlForMember: function(member, width, height, resizeMethod) {
|
||||||
var url = member.getAvatarUrl(
|
let url = member.getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
width,
|
Math.floor(width * window.devicePixelRatio),
|
||||||
height,
|
Math.floor(height * window.devicePixelRatio),
|
||||||
resizeMethod,
|
resizeMethod,
|
||||||
false,
|
false,
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
if (!url) {
|
if (!url) {
|
||||||
// member can be null here currently since on invites, the JS SDK
|
// member can be null here currently since on invites, the JS SDK
|
||||||
|
@ -38,9 +38,11 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
avatarUrlForUser: function(user, width, height, resizeMethod) {
|
avatarUrlForUser: function(user, width, height, resizeMethod) {
|
||||||
var url = ContentRepo.getHttpUriForMxc(
|
const url = ContentRepo.getHttpUriForMxc(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
||||||
width, height, resizeMethod
|
Math.floor(width * window.devicePixelRatio),
|
||||||
|
Math.floor(height * window.devicePixelRatio),
|
||||||
|
resizeMethod,
|
||||||
);
|
);
|
||||||
if (!url || url.length === 0) {
|
if (!url || url.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -49,12 +51,11 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultAvatarUrlForString: function(s) {
|
defaultAvatarUrlForString: function(s) {
|
||||||
var images = ['76cfa6', '50e2c2', 'f4c371'];
|
const images = ['76cfa6', '50e2c2', 'f4c371'];
|
||||||
var total = 0;
|
let total = 0;
|
||||||
for (var i = 0; i < s.length; ++i) {
|
for (let i = 0; i < s.length; ++i) {
|
||||||
total += s.charCodeAt(i);
|
total += s.charCodeAt(i);
|
||||||
}
|
}
|
||||||
return 'img/' + images[total % images.length] + '.png';
|
return 'img/' + images[total % images.length] + '.png';
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import dis from './dispatcher';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for classes that provide platform-specific functionality
|
* Base class for classes that provide platform-specific functionality
|
||||||
* eg. Setting an application badge or displaying notifications
|
* eg. Setting an application badge or displaying notifications
|
||||||
|
@ -27,6 +29,21 @@ export default class BasePlatform {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.notificationCount = 0;
|
this.notificationCount = 0;
|
||||||
this.errorDidOccur = false;
|
this.errorDidOccur = false;
|
||||||
|
|
||||||
|
dis.register(this._onAction.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
_onAction(payload: Object) {
|
||||||
|
switch (payload.action) {
|
||||||
|
case 'on_logged_out':
|
||||||
|
this.setNotificationCount(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used primarily for Analytics
|
||||||
|
getHumanReadableName(): string {
|
||||||
|
return 'Base Platform';
|
||||||
}
|
}
|
||||||
|
|
||||||
setNotificationCount(count: number) {
|
setNotificationCount(count: number) {
|
||||||
|
@ -40,6 +57,7 @@ export default class BasePlatform {
|
||||||
/**
|
/**
|
||||||
* Returns true if the platform supports displaying
|
* Returns true if the platform supports displaying
|
||||||
* notifications, otherwise false.
|
* notifications, otherwise false.
|
||||||
|
* @returns {boolean} whether the platform supports displaying notifications
|
||||||
*/
|
*/
|
||||||
supportsNotifications(): boolean {
|
supportsNotifications(): boolean {
|
||||||
return false;
|
return false;
|
||||||
|
@ -48,6 +66,7 @@ export default class BasePlatform {
|
||||||
/**
|
/**
|
||||||
* Returns true if the application currently has permission
|
* Returns true if the application currently has permission
|
||||||
* to display notifications. Otherwise false.
|
* to display notifications. Otherwise false.
|
||||||
|
* @returns {boolean} whether the application has permission to display notifications
|
||||||
*/
|
*/
|
||||||
maySendNotifications(): boolean {
|
maySendNotifications(): boolean {
|
||||||
return false;
|
return false;
|
||||||
|
@ -66,11 +85,14 @@ export default class BasePlatform {
|
||||||
displayNotification(title: string, msg: string, avatarUrl: string, room: Object) {
|
displayNotification(title: string, msg: string, avatarUrl: string, room: Object) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loudNotification(ev: Event, room: Object) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a promise that resolves to a string representing
|
* Returns a promise that resolves to a string representing
|
||||||
* the current version of the application.
|
* the current version of the application.
|
||||||
*/
|
*/
|
||||||
getAppVersion() {
|
getAppVersion(): Promise<string> {
|
||||||
throw new Error("getAppVersion not implemented!");
|
throw new Error("getAppVersion not implemented!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,10 +101,12 @@ export default class BasePlatform {
|
||||||
* with getUserMedia, return a string explaining why not.
|
* with getUserMedia, return a string explaining why not.
|
||||||
* Otherwise, return null.
|
* Otherwise, return null.
|
||||||
*/
|
*/
|
||||||
screenCaptureErrorString() {
|
screenCaptureErrorString(): string {
|
||||||
return "Not implemented";
|
return "Not implemented";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isElectron(): boolean { return false; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restarts the application, without neccessarily reloading
|
* Restarts the application, without neccessarily reloading
|
||||||
* any application code
|
* any application code
|
||||||
|
|
|
@ -51,12 +51,14 @@ limitations under the License.
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
var PlatformPeg = require("./PlatformPeg");
|
import UserSettingsStore from './UserSettingsStore';
|
||||||
var Modal = require('./Modal');
|
import PlatformPeg from './PlatformPeg';
|
||||||
var sdk = require('./index');
|
import Modal from './Modal';
|
||||||
var Matrix = require("matrix-js-sdk");
|
import sdk from './index';
|
||||||
var dis = require("./dispatcher");
|
import { _t } from './languageHandler';
|
||||||
|
import Matrix from 'matrix-js-sdk';
|
||||||
|
import dis from './dispatcher';
|
||||||
|
|
||||||
global.mxCalls = {
|
global.mxCalls = {
|
||||||
//room_id: MatrixCall
|
//room_id: MatrixCall
|
||||||
|
@ -142,8 +144,8 @@ function _setCallListeners(call) {
|
||||||
play("busyAudio");
|
play("busyAudio");
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Call Timeout",
|
title: _t('Call Timeout'),
|
||||||
description: "The remote side failed to pick up."
|
description: _t('The remote side failed to pick up') + '.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (oldState === "invite_sent") {
|
else if (oldState === "invite_sent") {
|
||||||
|
@ -179,7 +181,8 @@ function _setCallState(call, roomId, status) {
|
||||||
}
|
}
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'call_state',
|
action: 'call_state',
|
||||||
room_id: roomId
|
room_id: roomId,
|
||||||
|
state: status,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,8 +206,8 @@ function _onAction(payload) {
|
||||||
console.log("Can't capture screen: " + screenCapErrorString);
|
console.log("Can't capture screen: " + screenCapErrorString);
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Unable to capture screen",
|
title: _t('Unable to capture screen'),
|
||||||
description: screenCapErrorString
|
description: screenCapErrorString,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -223,8 +226,8 @@ function _onAction(payload) {
|
||||||
if (module.exports.getAnyActiveCall()) {
|
if (module.exports.getAnyActiveCall()) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Existing Call",
|
title: _t('Existing Call'),
|
||||||
description: "You are already in a call."
|
description: _t('You are already in a call.'),
|
||||||
});
|
});
|
||||||
return; // don't allow >1 call to be placed.
|
return; // don't allow >1 call to be placed.
|
||||||
}
|
}
|
||||||
|
@ -233,8 +236,8 @@ function _onAction(payload) {
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "VoIP is unsupported",
|
title: _t('VoIP is unsupported'),
|
||||||
description: "You cannot place VoIP calls in this browser."
|
description: _t('You cannot place VoIP calls in this browser.'),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -249,15 +252,15 @@ function _onAction(payload) {
|
||||||
if (members.length <= 1) {
|
if (members.length <= 1) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
description: "You cannot place a call with yourself."
|
description: _t('You cannot place a call with yourself.'),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (members.length === 2) {
|
else if (members.length === 2) {
|
||||||
console.log("Place %s call in %s", payload.type, payload.room_id);
|
console.log("Place %s call in %s", payload.type, payload.room_id);
|
||||||
var call = Matrix.createNewMatrixCall(
|
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, {
|
||||||
MatrixClientPeg.get(), payload.room_id
|
forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false),
|
||||||
);
|
});
|
||||||
placeCall(call);
|
placeCall(call);
|
||||||
}
|
}
|
||||||
else { // > 2
|
else { // > 2
|
||||||
|
@ -275,14 +278,14 @@ function _onAction(payload) {
|
||||||
if (!ConferenceHandler) {
|
if (!ConferenceHandler) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
description: "Conference calls are not supported in this client"
|
description: _t('Conference calls are not supported in this client'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (!MatrixClientPeg.get().supportsVoip()) {
|
else if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "VoIP is unsupported",
|
title: _t('VoIP is unsupported'),
|
||||||
description: "You cannot place VoIP calls in this browser."
|
description: _t('You cannot place VoIP calls in this browser.'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) {
|
else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) {
|
||||||
|
@ -294,14 +297,14 @@ function _onAction(payload) {
|
||||||
// Therefore we disable conference calling in E2E rooms.
|
// Therefore we disable conference calling in E2E rooms.
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
description: "Conference calls are not supported in encrypted rooms",
|
description: _t('Conference calls are not supported in encrypted rooms'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createDialog(QuestionDialog, {
|
||||||
title: "Warning!",
|
title: _t('Warning!'),
|
||||||
description: "Conference calling is in development and may not be reliable.",
|
description: _t('Conference calling is in development and may not be reliable.'),
|
||||||
onFinished: confirm=>{
|
onFinished: confirm=>{
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
ConferenceHandler.createNewMatrixCall(
|
ConferenceHandler.createNewMatrixCall(
|
||||||
|
@ -312,8 +315,8 @@ function _onAction(payload) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Conference call failed: " + err);
|
console.error("Conference call failed: " + err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failed to set up conference call",
|
title: _t('Failed to set up conference call'),
|
||||||
description: "Conference call failed.",
|
description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
64
src/CallMediaHandler.js
Normal file
64
src/CallMediaHandler.js
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
82
src/ComposerHistoryManager.js
Normal file
82
src/ComposerHistoryManager.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,11 +16,12 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var q = require('q');
|
import Promise from 'bluebird';
|
||||||
var extend = require('./extend');
|
var extend = require('./extend');
|
||||||
var dis = require('./dispatcher');
|
var dis = require('./dispatcher');
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
var sdk = require('./index');
|
var sdk = require('./index');
|
||||||
|
import { _t } from './languageHandler';
|
||||||
var Modal = require('./Modal');
|
var Modal = require('./Modal');
|
||||||
|
|
||||||
var encrypt = require("browser-encrypt-attachment");
|
var encrypt = require("browser-encrypt-attachment");
|
||||||
|
@ -51,7 +52,7 @@ const MAX_HEIGHT = 600;
|
||||||
* and a thumbnail key.
|
* and a thumbnail key.
|
||||||
*/
|
*/
|
||||||
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
||||||
const deferred = q.defer();
|
const deferred = Promise.defer();
|
||||||
|
|
||||||
var targetWidth = inputWidth;
|
var targetWidth = inputWidth;
|
||||||
var targetHeight = inputHeight;
|
var targetHeight = inputHeight;
|
||||||
|
@ -94,7 +95,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
||||||
* @return {Promise} A promise that resolves with the html image element.
|
* @return {Promise} A promise that resolves with the html image element.
|
||||||
*/
|
*/
|
||||||
function loadImageElement(imageFile) {
|
function loadImageElement(imageFile) {
|
||||||
const deferred = q.defer();
|
const deferred = Promise.defer();
|
||||||
|
|
||||||
// Load the file into an html element
|
// Load the file into an html element
|
||||||
const img = document.createElement("img");
|
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.
|
* @return {Promise} A promise that resolves with the video image element.
|
||||||
*/
|
*/
|
||||||
function loadVideoElement(videoFile) {
|
function loadVideoElement(videoFile) {
|
||||||
const deferred = q.defer();
|
const deferred = Promise.defer();
|
||||||
|
|
||||||
// Load the file into an html element
|
// Load the file into an html element
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
|
@ -209,7 +210,7 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
|
||||||
* is read.
|
* is read.
|
||||||
*/
|
*/
|
||||||
function readFileAsArrayBuffer(file) {
|
function readFileAsArrayBuffer(file) {
|
||||||
const deferred = q.defer();
|
const deferred = Promise.defer();
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(e) {
|
reader.onload = function(e) {
|
||||||
deferred.resolve(e.target.result);
|
deferred.resolve(e.target.result);
|
||||||
|
@ -228,11 +229,13 @@ function readFileAsArrayBuffer(file) {
|
||||||
* @param {MatrixClient} matrixClient The matrix client to upload the file with.
|
* @param {MatrixClient} matrixClient The matrix client to upload the file with.
|
||||||
* @param {String} roomId The ID of the room being uploaded to.
|
* @param {String} roomId The ID of the room being uploaded to.
|
||||||
* @param {File} file The file to upload.
|
* @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.
|
* @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 unencrypted then the object will have a "url" key.
|
||||||
* If the file is encrypted then the object will have a "file" 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 (matrixClient.isRoomEncrypted(roomId)) {
|
||||||
// If the room is encrypted then encrypt the file before uploading it.
|
// If the room is encrypted then encrypt the file before uploading it.
|
||||||
// First read the file into memory.
|
// First read the file into memory.
|
||||||
|
@ -244,7 +247,9 @@ function uploadFile(matrixClient, roomId, file) {
|
||||||
const encryptInfo = encryptResult.info;
|
const encryptInfo = encryptResult.info;
|
||||||
// Pass the encrypted data as a Blob to the uploader.
|
// Pass the encrypted data as a Blob to the uploader.
|
||||||
const blob = new Blob([encryptResult.data]);
|
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
|
// If the attachment is encrypted then bundle the URL along
|
||||||
// with the information needed to decrypt the attachment and
|
// with the information needed to decrypt the attachment and
|
||||||
// add it under a file key.
|
// add it under a file key.
|
||||||
|
@ -256,7 +261,9 @@ function uploadFile(matrixClient, roomId, file) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const basePromise = matrixClient.uploadContent(file);
|
const basePromise = matrixClient.uploadContent(file, {
|
||||||
|
progressHandler: progressHandler,
|
||||||
|
});
|
||||||
const promise1 = basePromise.then(function(url) {
|
const promise1 = basePromise.then(function(url) {
|
||||||
// If the attachment isn't encrypted then include the URL directly.
|
// If the attachment isn't encrypted then include the URL directly.
|
||||||
return {"url": url};
|
return {"url": url};
|
||||||
|
@ -287,7 +294,7 @@ class ContentMessages {
|
||||||
content.info.mimetype = file.type;
|
content.info.mimetype = file.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
const def = q.defer();
|
const def = Promise.defer();
|
||||||
if (file.type.indexOf('image/') == 0) {
|
if (file.type.indexOf('image/') == 0) {
|
||||||
content.msgtype = 'm.image';
|
content.msgtype = 'm.image';
|
||||||
infoForImageFile(matrixClient, roomId, file).then(imageInfo=>{
|
infoForImageFile(matrixClient, roomId, file).then(imageInfo=>{
|
||||||
|
@ -325,36 +332,37 @@ class ContentMessages {
|
||||||
dis.dispatch({action: 'upload_started'});
|
dis.dispatch({action: 'upload_started'});
|
||||||
|
|
||||||
var error;
|
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() {
|
return def.promise.then(function() {
|
||||||
// XXX: upload.promise must be the promise that
|
// XXX: upload.promise must be the promise that
|
||||||
// is returned by uploadFile as it has an abort()
|
// is returned by uploadFile as it has an abort()
|
||||||
// method hacked onto it.
|
// method hacked onto it.
|
||||||
upload.promise = uploadFile(
|
upload.promise = uploadFile(
|
||||||
matrixClient, roomId, file
|
matrixClient, roomId, file, onProgress,
|
||||||
);
|
);
|
||||||
return upload.promise.then(function(result) {
|
return upload.promise.then(function(result) {
|
||||||
content.file = result.file;
|
content.file = result.file;
|
||||||
content.url = result.url;
|
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) {
|
}).then(function(url) {
|
||||||
return matrixClient.sendMessage(roomId, content);
|
return matrixClient.sendMessage(roomId, content);
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
error = err;
|
error = err;
|
||||||
if (!upload.canceled) {
|
if (!upload.canceled) {
|
||||||
var desc = "The file '"+upload.fileName+"' failed to upload.";
|
var desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.';
|
||||||
if (err.http_status == 413) {
|
if (err.http_status == 413) {
|
||||||
desc = "The file '"+upload.fileName+"' exceeds this home server's size limit for uploads";
|
desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
|
||||||
}
|
}
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Upload Failed",
|
title: _t('Upload Failed'),
|
||||||
description: desc
|
description: desc,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|
101
src/DateUtils.js
101
src/DateUtils.js
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,38 +16,90 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
function getDaysArray() {
|
||||||
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
return [
|
||||||
|
_t('Sun'),
|
||||||
|
_t('Mon'),
|
||||||
|
_t('Tue'),
|
||||||
|
_t('Wed'),
|
||||||
|
_t('Thu'),
|
||||||
|
_t('Fri'),
|
||||||
|
_t('Sat'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonthsArray() {
|
||||||
|
return [
|
||||||
|
_t('Jan'),
|
||||||
|
_t('Feb'),
|
||||||
|
_t('Mar'),
|
||||||
|
_t('Apr'),
|
||||||
|
_t('May'),
|
||||||
|
_t('Jun'),
|
||||||
|
_t('Jul'),
|
||||||
|
_t('Aug'),
|
||||||
|
_t('Sep'),
|
||||||
|
_t('Oct'),
|
||||||
|
_t('Nov'),
|
||||||
|
_t('Dec'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
formatDate: function(date) {
|
|
||||||
// date.toLocaleTimeString is completely system dependent.
|
|
||||||
// just go 24h for now
|
|
||||||
function pad(n) {
|
function pad(n) {
|
||||||
return (n < 10 ? '0' : '') + n;
|
return (n < 10 ? '0' : '') + n;
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = new Date();
|
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, showTwelveHour=false) {
|
||||||
|
const now = new Date();
|
||||||
|
const days = getDaysArray();
|
||||||
|
const months = getMonthsArray();
|
||||||
if (date.toDateString() === now.toDateString()) {
|
if (date.toDateString() === now.toDateString()) {
|
||||||
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
return this.formatTime(date);
|
||||||
|
} else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
||||||
|
// 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 this.formatFullDate(date, showTwelveHour);
|
||||||
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());
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
},
|
},
|
||||||
|
|
||||||
formatTime: function(date) {
|
formatFullDate: function(date, showTwelveHour=false) {
|
||||||
//return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
const days = getDaysArray();
|
||||||
return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2);
|
const months = getMonthsArray();
|
||||||
}
|
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', {
|
||||||
};
|
weekDayName: days[date.getDay()],
|
||||||
|
monthName: months[date.getMonth()],
|
||||||
|
day: date.getDate(),
|
||||||
|
fullYear: date.getFullYear(),
|
||||||
|
time: showTwelveHour ? twelveHourTime(date) : this.formatTime(date),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime: function(date, showTwelveHour=false) {
|
||||||
|
if (showTwelveHour) {
|
||||||
|
return twelveHourTime(date);
|
||||||
|
}
|
||||||
|
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require('react');
|
import sdk from './index';
|
||||||
var sdk = require('./index');
|
|
||||||
|
|
||||||
function isMatch(query, name, uid) {
|
function isMatch(query, name, uid) {
|
||||||
query = query.toLowerCase();
|
query = query.toLowerCase();
|
||||||
|
@ -33,8 +32,8 @@ function isMatch(query, name, uid) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// split spaces in name and try matching constituent parts
|
// split spaces in name and try matching constituent parts
|
||||||
var parts = name.split(" ");
|
const parts = name.split(" ");
|
||||||
for (var i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
if (parts[i].indexOf(query) === 0) {
|
if (parts[i].indexOf(query) === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -67,7 +66,7 @@ class Entity {
|
||||||
|
|
||||||
class MemberEntity extends Entity {
|
class MemberEntity extends Entity {
|
||||||
getJsx() {
|
getJsx() {
|
||||||
var MemberTile = sdk.getComponent("rooms.MemberTile");
|
const MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||||
return (
|
return (
|
||||||
<MemberTile key={this.model.userId} member={this.model} />
|
<MemberTile key={this.model.userId} member={this.model} />
|
||||||
);
|
);
|
||||||
|
@ -84,6 +83,7 @@ class UserEntity extends Entity {
|
||||||
super(model);
|
super(model);
|
||||||
this.showInviteButton = Boolean(showInviteButton);
|
this.showInviteButton = Boolean(showInviteButton);
|
||||||
this.inviteFn = inviteFn;
|
this.inviteFn = inviteFn;
|
||||||
|
this.onClick = this.onClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick() {
|
onClick() {
|
||||||
|
@ -93,15 +93,15 @@ class UserEntity extends Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
getJsx() {
|
getJsx() {
|
||||||
var UserTile = sdk.getComponent("rooms.UserTile");
|
const UserTile = sdk.getComponent("rooms.UserTile");
|
||||||
return (
|
return (
|
||||||
<UserTile key={this.model.userId} user={this.model}
|
<UserTile key={this.model.userId} user={this.model}
|
||||||
showInviteButton={this.showInviteButton} onClick={this.onClick.bind(this)} />
|
showInviteButton={this.showInviteButton} onClick={this.onClick} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
matches(queryString) {
|
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);
|
return isMatch(queryString, name, this.model.userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ class UserEntity extends Entity {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
newEntity: function(jsx, matchFn) {
|
newEntity: function(jsx, matchFn) {
|
||||||
var entity = new Entity();
|
const entity = new Entity();
|
||||||
entity.getJsx = function() {
|
entity.getJsx = function() {
|
||||||
return jsx;
|
return jsx;
|
||||||
};
|
};
|
||||||
|
@ -137,5 +137,5 @@ module.exports = {
|
||||||
return users.map(function(u) {
|
return users.map(function(u) {
|
||||||
return new UserEntity(u, showInviteButton, inviteFn);
|
return new UserEntity(u, showInviteButton, inviteFn);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
114
src/HtmlUtils.js
114
src/HtmlUtils.js
|
@ -23,8 +23,12 @@ var linkifyMatrix = require('./linkify-matrix');
|
||||||
import escape from 'lodash/escape';
|
import escape from 'lodash/escape';
|
||||||
import emojione from 'emojione';
|
import emojione from 'emojione';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
|
||||||
emojione.imagePathSVG = 'emojione/svg/';
|
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';
|
emojione.imageType = 'svg';
|
||||||
|
|
||||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
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
|
* because we want to include emoji shortnames in title text
|
||||||
*/
|
*/
|
||||||
export function unicodeToImage(str) {
|
export function unicodeToImage(str) {
|
||||||
let replaceWith, unicode, alt;
|
let replaceWith, unicode, alt, short, fname;
|
||||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||||
|
|
||||||
str = str.replace(emojione.regUnicode, function(unicodeChar) {
|
str = str.replace(emojione.regUnicode, function(unicodeChar) {
|
||||||
|
@ -46,11 +50,14 @@ export function unicodeToImage(str) {
|
||||||
// get the unicode codepoint from the actual char
|
// get the unicode codepoint from the actual char
|
||||||
unicode = emojione.jsEscapeMap[unicodeChar];
|
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
|
// 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];
|
alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
|
||||||
const title = 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;
|
return replaceWith;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -64,17 +71,24 @@ export function unicodeToImage(str) {
|
||||||
* emoji.
|
* emoji.
|
||||||
*
|
*
|
||||||
* @param alt {string} String to use for the image alt text
|
* @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
|
* @param unicode {integer} One or more integers representing unicode characters
|
||||||
* @returns A img node with the corresponding emoji
|
* @returns A img node with the corresponding emoji
|
||||||
*/
|
*/
|
||||||
export function charactersToImageNode(alt, ...unicode) {
|
export function charactersToImageNode(alt, useSvg, ...unicode) {
|
||||||
const fileName = unicode.map((u) => {
|
const fileName = unicode.map((u) => {
|
||||||
return u.toString(16);
|
return u.toString(16);
|
||||||
}).join('-');
|
}).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');
|
const contentDiv = document.createElement('div');
|
||||||
contentDiv.innerHTML = html;
|
contentDiv.innerHTML = html;
|
||||||
|
|
||||||
|
@ -86,7 +100,18 @@ export function stripParagraphs(html: string): string {
|
||||||
for (let i=0; i < contentDiv.children.length; i++) {
|
for (let i=0; i < contentDiv.children.length; i++) {
|
||||||
const element = contentDiv.children[i];
|
const element = contentDiv.children[i];
|
||||||
if (element.tagName.toLowerCase() === 'p') {
|
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 {
|
} else {
|
||||||
const temp = document.createElement('div');
|
const temp = document.createElement('div');
|
||||||
temp.appendChild(element.cloneNode(true));
|
temp.appendChild(element.cloneNode(true));
|
||||||
|
@ -97,34 +122,39 @@ export function stripParagraphs(html: string): string {
|
||||||
return contentHTML;
|
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: [
|
allowedTags: [
|
||||||
'font', // custom to matrix for IRC-style font coloring
|
'font', // custom to matrix for IRC-style font coloring
|
||||||
'del', // for markdown
|
'del', // for markdown
|
||||||
// deliberately no h1/h2 to stop people shouting.
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||||
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
|
||||||
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
'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: {
|
allowedAttributes: {
|
||||||
// custom ones first:
|
// custom ones first:
|
||||||
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
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
|
span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||||
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
||||||
// We don't currently allow img itself by default, but this
|
img: ['src', 'width', 'height', 'alt', 'title'],
|
||||||
// would make sense if we did
|
|
||||||
img: ['src'],
|
|
||||||
ol: ['start'],
|
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
|
// 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'],
|
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
||||||
// URL schemes we permit
|
// URL schemes we permit
|
||||||
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
|
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
|
||||||
|
|
||||||
// DO NOT USE. sanitize-html allows all URL starting with '//'
|
allowProtocolRelative: false,
|
||||||
// so this will always allow links to whatever scheme the
|
|
||||||
// host page is served over.
|
|
||||||
allowedSchemesByTag: {},
|
|
||||||
|
|
||||||
transformTags: { // custom to matrix
|
transformTags: { // custom to matrix
|
||||||
// add blank targets to all hyperlinks except vector URLs
|
// add blank targets to all hyperlinks except vector URLs
|
||||||
|
@ -139,7 +169,7 @@ var sanitizeHtmlParams = {
|
||||||
attribs.href = m[1];
|
attribs.href = m[1];
|
||||||
delete attribs.target;
|
delete attribs.target;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
|
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
|
||||||
if (m) {
|
if (m) {
|
||||||
var entity = m[1];
|
var entity = m[1];
|
||||||
|
@ -152,9 +182,37 @@ var sanitizeHtmlParams = {
|
||||||
delete attribs.target;
|
delete attribs.target;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||||
return { tagName: tagName, attribs : attribs };
|
return { tagName: tagName, attribs : attribs };
|
||||||
},
|
},
|
||||||
|
'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) {
|
'*': function(tagName, attribs) {
|
||||||
// Delete any style previously assigned, style is an allowedTag for font and span
|
// Delete any style previously assigned, style is an allowedTag for font and span
|
||||||
// because attributes are stripped after transforming
|
// because attributes are stripped after transforming
|
||||||
|
@ -335,6 +393,7 @@ export function bodyToHtml(content, highlights, opts) {
|
||||||
}
|
}
|
||||||
safeBody = sanitizeHtml(body, sanitizeHtmlParams);
|
safeBody = sanitizeHtml(body, sanitizeHtmlParams);
|
||||||
safeBody = unicodeToImage(safeBody);
|
safeBody = unicodeToImage(safeBody);
|
||||||
|
safeBody = addCodeCopyButton(safeBody);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
delete sanitizeHtmlParams.textFilter;
|
delete sanitizeHtmlParams.textFilter;
|
||||||
|
@ -350,7 +409,24 @@ export function bodyToHtml(content, highlights, opts) {
|
||||||
'mx_EventTile_bigEmoji': emojiBody,
|
'mx_EventTile_bigEmoji': emojiBody,
|
||||||
'markdown-body': isHtml,
|
'markdown-body': isHtml,
|
||||||
});
|
});
|
||||||
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} />;
|
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCodeCopyButton(safeBody) {
|
||||||
|
// Adds 'copy' buttons to pre blocks
|
||||||
|
// Note that this only manipulates the markup to add the buttons:
|
||||||
|
// we need to add the event handlers once the nodes are in the DOM
|
||||||
|
// since we can't save functions in the markup.
|
||||||
|
// This is done in TextualBody
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.innerHTML = safeBody;
|
||||||
|
const codeBlocks = Array.from(el.getElementsByTagName("pre"));
|
||||||
|
codeBlocks.forEach(p => {
|
||||||
|
const button = document.createElement("span");
|
||||||
|
button.className = "mx_EventTile_copyButton";
|
||||||
|
p.appendChild(button);
|
||||||
|
});
|
||||||
|
return el.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emojifyText(text) {
|
export function emojifyText(text) {
|
||||||
|
|
|
@ -21,6 +21,7 @@ module.exports = {
|
||||||
ENTER: 13,
|
ENTER: 13,
|
||||||
SHIFT: 16,
|
SHIFT: 16,
|
||||||
ESCAPE: 27,
|
ESCAPE: 27,
|
||||||
|
SPACE: 32,
|
||||||
PAGE_UP: 33,
|
PAGE_UP: 33,
|
||||||
PAGE_DOWN: 34,
|
PAGE_DOWN: 34,
|
||||||
END: 35,
|
END: 35,
|
||||||
|
@ -30,6 +31,30 @@ module.exports = {
|
||||||
RIGHT: 39,
|
RIGHT: 39,
|
||||||
DOWN: 40,
|
DOWN: 40,
|
||||||
DELETE: 46,
|
DELETE: 46,
|
||||||
|
KEY_A: 65,
|
||||||
|
KEY_B: 66,
|
||||||
|
KEY_C: 67,
|
||||||
KEY_D: 68,
|
KEY_D: 68,
|
||||||
KEY_E: 69,
|
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
138
src/KeyRequestHandler.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
326
src/Lifecycle.js
326
src/Lifecycle.js
|
@ -15,10 +15,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
|
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
import createMatrixClient from './utils/createMatrixClient';
|
||||||
|
import Analytics from './Analytics';
|
||||||
import Notifier from './Notifier';
|
import Notifier from './Notifier';
|
||||||
import UserActivity from './UserActivity';
|
import UserActivity from './UserActivity';
|
||||||
import Presence from './Presence';
|
import Presence from './Presence';
|
||||||
|
@ -32,28 +34,19 @@ import sdk from './index';
|
||||||
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
||||||
* a number of things:
|
* a number of things:
|
||||||
*
|
*
|
||||||
* 0. if it looks like we are in the middle of a registration process, it does
|
|
||||||
* nothing.
|
|
||||||
*
|
*
|
||||||
* 1. if we have a loginToken in the (real) query params, it uses that to log
|
* 1. if we have a guest access token in the fragment query params, it uses
|
||||||
* in.
|
|
||||||
*
|
|
||||||
* 2. if we have a guest access token in the fragment query params, it uses
|
|
||||||
* that.
|
* that.
|
||||||
*
|
*
|
||||||
* 3. if an access token is stored in local storage (from a previous session),
|
* 2. if an access token is stored in local storage (from a previous session),
|
||||||
* it uses that.
|
* it uses that.
|
||||||
*
|
*
|
||||||
* 4. it attempts to auto-register as a guest user.
|
* 3. it attempts to auto-register as a guest user.
|
||||||
*
|
*
|
||||||
* If any of steps 1-4 are successful, it will call {setLoggedIn}, which in
|
* If any of steps 1-4 are successful, it will call {_doSetLoggedIn}, which in
|
||||||
* turn will raise on_logged_in and will_start_client events.
|
* turn will raise on_logged_in and will_start_client events.
|
||||||
*
|
*
|
||||||
* It returns a promise which resolves when the above process completes.
|
* @param {object} opts
|
||||||
*
|
|
||||||
* @param {object} opts.realQueryParams: string->string map of the
|
|
||||||
* query-parameters extracted from the real query-string of the starting
|
|
||||||
* URI.
|
|
||||||
*
|
*
|
||||||
* @param {object} opts.fragmentQueryParams: string->string map of the
|
* @param {object} opts.fragmentQueryParams: string->string map of the
|
||||||
* query-parameters extracted from the #-fragment of the starting URI.
|
* query-parameters extracted from the #-fragment of the starting URI.
|
||||||
|
@ -67,54 +60,39 @@ import sdk from './index';
|
||||||
* @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is
|
* @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is
|
||||||
* true; defines the IS to use.
|
* 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) {
|
export function loadSession(opts) {
|
||||||
const realQueryParams = opts.realQueryParams || {};
|
|
||||||
const fragmentQueryParams = opts.fragmentQueryParams || {};
|
const fragmentQueryParams = opts.fragmentQueryParams || {};
|
||||||
let enableGuest = opts.enableGuest || false;
|
let enableGuest = opts.enableGuest || false;
|
||||||
const guestHsUrl = opts.guestHsUrl;
|
const guestHsUrl = opts.guestHsUrl;
|
||||||
const guestIsUrl = opts.guestIsUrl;
|
const guestIsUrl = opts.guestIsUrl;
|
||||||
const defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
const defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
||||||
|
|
||||||
if (fragmentQueryParams.client_secret && fragmentQueryParams.sid) {
|
|
||||||
// this happens during email validation: the email contains a link to the
|
|
||||||
// IS, which in turn redirects back to vector. We let MatrixChat create a
|
|
||||||
// Registration component which completes the next stage of registration.
|
|
||||||
console.log("Not registering as guest: registration already in progress.");
|
|
||||||
return q();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!guestHsUrl) {
|
if (!guestHsUrl) {
|
||||||
console.warn("Cannot enable guest access: can't determine HS URL to use");
|
console.warn("Cannot enable guest access: can't determine HS URL to use");
|
||||||
enableGuest = false;
|
enableGuest = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (realQueryParams.loginToken) {
|
|
||||||
if (!realQueryParams.homeserver) {
|
|
||||||
console.warn("Cannot log in with token: can't determine HS URL to use");
|
|
||||||
} else {
|
|
||||||
return _loginWithToken(realQueryParams, defaultDeviceDisplayName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableGuest &&
|
if (enableGuest &&
|
||||||
fragmentQueryParams.guest_user_id &&
|
fragmentQueryParams.guest_user_id &&
|
||||||
fragmentQueryParams.guest_access_token
|
fragmentQueryParams.guest_access_token
|
||||||
) {
|
) {
|
||||||
console.log("Using guest access credentials");
|
console.log("Using guest access credentials");
|
||||||
setLoggedIn({
|
return _doSetLoggedIn({
|
||||||
userId: fragmentQueryParams.guest_user_id,
|
userId: fragmentQueryParams.guest_user_id,
|
||||||
accessToken: fragmentQueryParams.guest_access_token,
|
accessToken: fragmentQueryParams.guest_access_token,
|
||||||
homeserverUrl: guestHsUrl,
|
homeserverUrl: guestHsUrl,
|
||||||
identityServerUrl: guestIsUrl,
|
identityServerUrl: guestIsUrl,
|
||||||
guest: true,
|
guest: true,
|
||||||
});
|
}, true).then(() => true);
|
||||||
return q();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _restoreFromLocalStorage().then((success) => {
|
return _restoreFromLocalStorage().then((success) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableGuest) {
|
if (enableGuest) {
|
||||||
|
@ -122,12 +100,32 @@ export function loadSession(opts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// fall back to login screen
|
// fall back to login screen
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _loginWithToken(queryParams, defaultDeviceDisplayName) {
|
/**
|
||||||
|
* @param {Object} queryParams string->string map of the
|
||||||
|
* query-parameters extracted from the real query-string of the starting
|
||||||
|
* URI.
|
||||||
|
*
|
||||||
|
* @param {String} defaultDeviceDisplayName
|
||||||
|
*
|
||||||
|
* @returns {Promise} promise which resolves to true if we completed the token
|
||||||
|
* login, else false
|
||||||
|
*/
|
||||||
|
export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
||||||
|
if (!queryParams.loginToken) {
|
||||||
|
return 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
|
// create a temporary MatrixClient to do the login
|
||||||
var client = Matrix.createClient({
|
const client = Matrix.createClient({
|
||||||
baseUrl: queryParams.homeserver,
|
baseUrl: queryParams.homeserver,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -138,7 +136,8 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
|
||||||
},
|
},
|
||||||
).then(function(data) {
|
).then(function(data) {
|
||||||
console.log("Logged in with token");
|
console.log("Logged in with token");
|
||||||
setLoggedIn({
|
return _clearStorage().then(() => {
|
||||||
|
_persistCredentialsToLocalStorage({
|
||||||
userId: data.user_id,
|
userId: data.user_id,
|
||||||
deviceId: data.device_id,
|
deviceId: data.device_id,
|
||||||
accessToken: data.access_token,
|
accessToken: data.access_token,
|
||||||
|
@ -146,20 +145,23 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
|
||||||
identityServerUrl: queryParams.identityServer,
|
identityServerUrl: queryParams.identityServer,
|
||||||
guest: false,
|
guest: false,
|
||||||
});
|
});
|
||||||
}, (err) => {
|
return true;
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
console.error("Failed to log in with login token: " + err + " " +
|
console.error("Failed to log in with login token: " + err + " " +
|
||||||
err.data);
|
err.data);
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||||
console.log("Doing guest login on %s", hsUrl);
|
console.log(`Doing guest login on ${hsUrl}`);
|
||||||
|
|
||||||
// TODO: we should probably de-duplicate this and Login.loginAsGuest.
|
// TODO: we should probably de-duplicate this and Login.loginAsGuest.
|
||||||
// Not really sure where the right home for it is.
|
// Not really sure where the right home for it is.
|
||||||
|
|
||||||
// create a temporary MatrixClient to do the login
|
// create a temporary MatrixClient to do the login
|
||||||
var client = Matrix.createClient({
|
const client = Matrix.createClient({
|
||||||
baseUrl: hsUrl,
|
baseUrl: hsUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -168,83 +170,78 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||||
initial_device_display_name: defaultDeviceDisplayName,
|
initial_device_display_name: defaultDeviceDisplayName,
|
||||||
},
|
},
|
||||||
}).then((creds) => {
|
}).then((creds) => {
|
||||||
console.log("Registered as guest: %s", creds.user_id);
|
console.log(`Registered as guest: ${creds.user_id}`);
|
||||||
setLoggedIn({
|
return _doSetLoggedIn({
|
||||||
userId: creds.user_id,
|
userId: creds.user_id,
|
||||||
deviceId: creds.device_id,
|
deviceId: creds.device_id,
|
||||||
accessToken: creds.access_token,
|
accessToken: creds.access_token,
|
||||||
homeserverUrl: hsUrl,
|
homeserverUrl: hsUrl,
|
||||||
identityServerUrl: isUrl,
|
identityServerUrl: isUrl,
|
||||||
guest: true,
|
guest: true,
|
||||||
});
|
}, true).then(() => true);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.error("Failed to register as guest: " + err + " " + err.data);
|
console.error("Failed to register as guest: " + err + " " + err.data);
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns a promise which resolves to true if a session is found in
|
// returns a promise which resolves to true if a session is found in
|
||||||
// localstorage
|
// localstorage
|
||||||
|
//
|
||||||
|
// N.B. Lifecycle.js should not maintain any further localStorage state, we
|
||||||
|
// are moving towards using SessionStore to keep track of state related
|
||||||
|
// to the current session (which is typically backed by localStorage).
|
||||||
|
//
|
||||||
|
// The plan is to gradually move the localStorage access done here into
|
||||||
|
// SessionStore to avoid bugs where the view becomes out-of-sync with
|
||||||
|
// localStorage (e.g. teamToken, isGuest etc.)
|
||||||
function _restoreFromLocalStorage() {
|
function _restoreFromLocalStorage() {
|
||||||
if (!localStorage) {
|
if (!localStorage) {
|
||||||
return q(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
const hs_url = localStorage.getItem("mx_hs_url");
|
const hsUrl = localStorage.getItem("mx_hs_url");
|
||||||
const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
||||||
const access_token = localStorage.getItem("mx_access_token");
|
const accessToken = localStorage.getItem("mx_access_token");
|
||||||
const user_id = localStorage.getItem("mx_user_id");
|
const userId = localStorage.getItem("mx_user_id");
|
||||||
const device_id = localStorage.getItem("mx_device_id");
|
const deviceId = localStorage.getItem("mx_device_id");
|
||||||
|
|
||||||
let is_guest;
|
let isGuest;
|
||||||
if (localStorage.getItem("mx_is_guest") !== null) {
|
if (localStorage.getItem("mx_is_guest") !== null) {
|
||||||
is_guest = localStorage.getItem("mx_is_guest") === "true";
|
isGuest = localStorage.getItem("mx_is_guest") === "true";
|
||||||
} else {
|
} else {
|
||||||
// legacy key name
|
// 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) {
|
if (accessToken && userId && hsUrl) {
|
||||||
console.log("Restoring session for %s", user_id);
|
console.log(`Restoring session for ${userId}`);
|
||||||
try {
|
try {
|
||||||
setLoggedIn({
|
return _doSetLoggedIn({
|
||||||
userId: user_id,
|
userId: userId,
|
||||||
deviceId: device_id,
|
deviceId: deviceId,
|
||||||
accessToken: access_token,
|
accessToken: accessToken,
|
||||||
homeserverUrl: hs_url,
|
homeserverUrl: hsUrl,
|
||||||
identityServerUrl: is_url,
|
identityServerUrl: isUrl,
|
||||||
guest: is_guest,
|
guest: isGuest,
|
||||||
});
|
}, false).then(() => true);
|
||||||
return q(true);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return _handleRestoreFailure(e);
|
return _handleRestoreFailure(e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("No previous session found.");
|
console.log("No previous session found.");
|
||||||
return q(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _handleRestoreFailure(e) {
|
function _handleRestoreFailure(e) {
|
||||||
console.log("Unable to restore session", e);
|
console.log("Unable to restore session", e);
|
||||||
|
|
||||||
let msg = e.message;
|
const def = Promise.defer();
|
||||||
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 SessionRestoreErrorDialog =
|
const SessionRestoreErrorDialog =
|
||||||
sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
|
sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
|
||||||
|
|
||||||
Modal.createDialog(SessionRestoreErrorDialog, {
|
Modal.createDialog(SessionRestoreErrorDialog, {
|
||||||
error: msg,
|
error: e.message,
|
||||||
onFinished: (success) => {
|
onFinished: (success) => {
|
||||||
def.resolve(success);
|
def.resolve(success);
|
||||||
},
|
},
|
||||||
|
@ -253,7 +250,7 @@ function _handleRestoreFailure(e) {
|
||||||
return def.promise.then((success) => {
|
return def.promise.then((success) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
// user clicked continue.
|
// user clicked continue.
|
||||||
_clearLocalStorage();
|
_clearStorage();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,30 +261,112 @@ function _handleRestoreFailure(e) {
|
||||||
|
|
||||||
let rtsClient = null;
|
let rtsClient = null;
|
||||||
export function initRtsClient(url) {
|
export function initRtsClient(url) {
|
||||||
|
if (url) {
|
||||||
rtsClient = new RtsClient(url);
|
rtsClient = new RtsClient(url);
|
||||||
|
} else {
|
||||||
|
rtsClient = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transitions to a logged-in state using the given credentials
|
* Transitions to a logged-in state using the given credentials.
|
||||||
|
*
|
||||||
|
* Starts the matrix client and all other react-sdk services that
|
||||||
|
* listen for events while a session is logged in.
|
||||||
|
*
|
||||||
|
* Also stops the old MatrixClient and clears old credentials/etc out of
|
||||||
|
* storage before starting the new client.
|
||||||
|
*
|
||||||
* @param {MatrixClientCreds} credentials The credentials to use
|
* @param {MatrixClientCreds} credentials The credentials to use
|
||||||
|
*
|
||||||
|
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
|
||||||
*/
|
*/
|
||||||
export function setLoggedIn(credentials) {
|
export function setLoggedIn(credentials) {
|
||||||
|
stopMatrixClient();
|
||||||
|
return _doSetLoggedIn(credentials, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fires on_logging_in, optionally clears localstorage, persists new credentials
|
||||||
|
* to localstorage, starts the new client.
|
||||||
|
*
|
||||||
|
* @param {MatrixClientCreds} credentials
|
||||||
|
* @param {Boolean} clearStorage
|
||||||
|
*
|
||||||
|
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
|
||||||
|
*/
|
||||||
|
async function _doSetLoggedIn(credentials, clearStorage) {
|
||||||
credentials.guest = Boolean(credentials.guest);
|
credentials.guest = Boolean(credentials.guest);
|
||||||
console.log("setLoggedIn => %s (guest=%s) hs=%s",
|
|
||||||
credentials.userId, credentials.guest,
|
console.log(
|
||||||
credentials.homeserverUrl);
|
"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
|
// This is dispatched to indicate that the user is still in the process of logging in
|
||||||
// because `teamPromise` may take some time to resolve, breaking the assumption that
|
// because `teamPromise` may take some time to resolve, breaking the assumption that
|
||||||
// `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms
|
// `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms
|
||||||
// later than MatrixChat might assume.
|
// later than MatrixChat might assume.
|
||||||
dis.dispatch({action: 'on_logging_in'});
|
//
|
||||||
|
// 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
|
// Resolves by default
|
||||||
let teamPromise = Promise.resolve(null);
|
let teamPromise = Promise.resolve(null);
|
||||||
|
|
||||||
// persist the session
|
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
try {
|
try {
|
||||||
|
_persistCredentialsToLocalStorage(credentials);
|
||||||
|
|
||||||
|
// The user registered as a PWLU (PassWord-Less User), the generated password
|
||||||
|
// is cached here such that the user can change it at a later time.
|
||||||
|
if (credentials.password) {
|
||||||
|
// Update SessionStore
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'cached_password',
|
||||||
|
cachedPassword: credentials.password,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error using local storage: can't persist session!", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rtsClient && !credentials.guest) {
|
||||||
|
teamPromise = rtsClient.login(credentials.userId).then((body) => {
|
||||||
|
if (body.team_token) {
|
||||||
|
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!");
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixClientPeg.replaceUsingCreds(credentials);
|
||||||
|
|
||||||
|
teamPromise.then((teamToken) => {
|
||||||
|
dis.dispatch({action: 'on_logged_in', teamToken: teamToken});
|
||||||
|
});
|
||||||
|
|
||||||
|
startMatrixClient();
|
||||||
|
return MatrixClientPeg.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _persistCredentialsToLocalStorage(credentials) {
|
||||||
localStorage.setItem("mx_hs_url", credentials.homeserverUrl);
|
localStorage.setItem("mx_hs_url", credentials.homeserverUrl);
|
||||||
localStorage.setItem("mx_is_url", credentials.identityServerUrl);
|
localStorage.setItem("mx_is_url", credentials.identityServerUrl);
|
||||||
localStorage.setItem("mx_user_id", credentials.userId);
|
localStorage.setItem("mx_user_id", credentials.userId);
|
||||||
|
@ -303,36 +382,7 @@ export function setLoggedIn(credentials) {
|
||||||
localStorage.setItem("mx_device_id", credentials.deviceId);
|
localStorage.setItem("mx_device_id", credentials.deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Session persisted for %s", credentials.userId);
|
console.log(`Session persisted for ${credentials.userId}`);
|
||||||
} catch (e) {
|
|
||||||
console.warn("Error using local storage: can't persist session!", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rtsClient && !credentials.guest) {
|
|
||||||
teamPromise = rtsClient.login(credentials.userId).then((body) => {
|
|
||||||
if (body.team_token) {
|
|
||||||
localStorage.setItem("mx_team_token", body.team_token);
|
|
||||||
}
|
|
||||||
return body.team_token;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -352,7 +402,7 @@ export function logout() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MatrixClientPeg.get().logout().then(onLoggedOut,
|
MatrixClientPeg.get().logout().then(onLoggedOut,
|
||||||
(err) => {
|
(err) => {
|
||||||
// Just throwing an error here is going to be very unhelpful
|
// Just throwing an error here is going to be very unhelpful
|
||||||
// if you're trying to log out because your server's down and
|
// if you're trying to log out because your server's down and
|
||||||
|
@ -363,15 +413,17 @@ export function logout() {
|
||||||
// change your password).
|
// change your password).
|
||||||
console.log("Failed to call logout API: token will not be invalidated");
|
console.log("Failed to call logout API: token will not be invalidated");
|
||||||
onLoggedOut();
|
onLoggedOut();
|
||||||
}
|
},
|
||||||
);
|
).done();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the matrix client and all other react-sdk services that
|
* Starts the matrix client and all other react-sdk services that
|
||||||
* listen for events while a session is logged in.
|
* listen for events while a session is logged in.
|
||||||
*/
|
*/
|
||||||
export function startMatrixClient() {
|
function startMatrixClient() {
|
||||||
|
console.log(`Lifecycle: Starting MatrixClient`);
|
||||||
|
|
||||||
// dispatch this before starting the matrix client: it's used
|
// dispatch this before starting the matrix client: it's used
|
||||||
// to add listeners for the 'sync' event so otherwise we'd have
|
// to add listeners for the 'sync' event so otherwise we'd have
|
||||||
// a race condition (and we need to dispatch synchronously for this
|
// a race condition (and we need to dispatch synchronously for this
|
||||||
|
@ -387,19 +439,22 @@ export function startMatrixClient() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Stops a running client and all related services, used after
|
* Stops a running client and all related services, and clears persistent
|
||||||
* a session has been logged out / ended.
|
* storage. Used after a session has been logged out.
|
||||||
*/
|
*/
|
||||||
export function onLoggedOut() {
|
export function onLoggedOut() {
|
||||||
_clearLocalStorage();
|
|
||||||
stopMatrixClient();
|
stopMatrixClient();
|
||||||
|
_clearStorage().done();
|
||||||
dis.dispatch({action: 'on_logged_out'});
|
dis.dispatch({action: 'on_logged_out'});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _clearLocalStorage() {
|
/**
|
||||||
if (!window.localStorage) {
|
* @returns {Promise} promise which resolves once the stores have been cleared
|
||||||
return;
|
*/
|
||||||
}
|
function _clearStorage() {
|
||||||
|
Analytics.logout();
|
||||||
|
|
||||||
|
if (window.localStorage) {
|
||||||
const hsUrl = window.localStorage.getItem("mx_hs_url");
|
const hsUrl = window.localStorage.getItem("mx_hs_url");
|
||||||
const isUrl = window.localStorage.getItem("mx_is_url");
|
const isUrl = window.localStorage.getItem("mx_is_url");
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
|
@ -412,19 +467,26 @@ function _clearLocalStorage() {
|
||||||
if (isUrl) window.localStorage.setItem("mx_is_url", isUrl);
|
if (isUrl) window.localStorage.setItem("mx_is_url", isUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create a temporary client to clear out the persistent stores.
|
||||||
|
const cli = createMatrixClient({
|
||||||
|
// we'll never make any requests, so can pass a bogus HS URL
|
||||||
|
baseUrl: "",
|
||||||
|
});
|
||||||
|
return cli.clearStores();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop all the background processes related to the current client
|
* Stop all the background processes related to the current client.
|
||||||
*/
|
*/
|
||||||
export function stopMatrixClient() {
|
export function stopMatrixClient() {
|
||||||
Notifier.stop();
|
Notifier.stop();
|
||||||
UserActivity.stop();
|
UserActivity.stop();
|
||||||
Presence.stop();
|
Presence.stop();
|
||||||
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
|
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
|
||||||
var cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.stopClient();
|
cli.stopClient();
|
||||||
cli.removeAllListeners();
|
cli.removeAllListeners();
|
||||||
cli.store.deleteAllData();
|
|
||||||
MatrixClientPeg.unset();
|
MatrixClientPeg.unset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
40
src/Login.js
40
src/Login.js
|
@ -16,8 +16,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Matrix from "matrix-js-sdk";
|
import Matrix from "matrix-js-sdk";
|
||||||
|
import { _t } from "./languageHandler";
|
||||||
|
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
|
||||||
export default class Login {
|
export default class Login {
|
||||||
|
@ -96,11 +97,6 @@ export default class Login {
|
||||||
guest: true
|
guest: true
|
||||||
};
|
};
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
if (error.httpStatus === 403) {
|
|
||||||
error.friendlyText = "Guest access is disabled on this Home Server.";
|
|
||||||
} else {
|
|
||||||
error.friendlyText = "Failed to register as guest: " + error.data;
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -148,7 +144,7 @@ export default class Login {
|
||||||
|
|
||||||
const client = this._createTemporaryClient();
|
const client = this._createTemporaryClient();
|
||||||
return client.login('m.login.password', loginParams).then(function(data) {
|
return client.login('m.login.password', loginParams).then(function(data) {
|
||||||
return q({
|
return Promise.resolve({
|
||||||
homeserverUrl: self._hsUrl,
|
homeserverUrl: self._hsUrl,
|
||||||
identityServerUrl: self._isUrl,
|
identityServerUrl: self._isUrl,
|
||||||
userId: data.user_id,
|
userId: data.user_id,
|
||||||
|
@ -156,15 +152,7 @@ export default class Login {
|
||||||
accessToken: data.access_token
|
accessToken: data.access_token
|
||||||
});
|
});
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
if (error.httpStatus == 400 && loginParams.medium) {
|
if (error.httpStatus === 403) {
|
||||||
error.friendlyText = (
|
|
||||||
'This Home Server does not support login using email address.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else if (error.httpStatus === 403) {
|
|
||||||
error.friendlyText = (
|
|
||||||
'Incorrect username and/or password.'
|
|
||||||
);
|
|
||||||
if (self._fallbackHsUrl) {
|
if (self._fallbackHsUrl) {
|
||||||
var fbClient = Matrix.createClient({
|
var fbClient = Matrix.createClient({
|
||||||
baseUrl: self._fallbackHsUrl,
|
baseUrl: self._fallbackHsUrl,
|
||||||
|
@ -172,7 +160,7 @@ export default class Login {
|
||||||
});
|
});
|
||||||
|
|
||||||
return fbClient.login('m.login.password', loginParams).then(function(data) {
|
return fbClient.login('m.login.password', loginParams).then(function(data) {
|
||||||
return q({
|
return Promise.resolve({
|
||||||
homeserverUrl: self._fallbackHsUrl,
|
homeserverUrl: self._fallbackHsUrl,
|
||||||
identityServerUrl: self._isUrl,
|
identityServerUrl: self._isUrl,
|
||||||
userId: data.user_id,
|
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;
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectToCas() {
|
redirectToCas() {
|
||||||
var client = this._createTemporaryClient();
|
const client = this._createTemporaryClient();
|
||||||
var parsedUrl = url.parse(window.location.href, true);
|
const parsedUrl = url.parse(window.location.href, true);
|
||||||
|
|
||||||
|
// XXX: at this point, the fragment will always be #/login, which is no
|
||||||
|
// use to anyone. Ideally, we would get the intended fragment from
|
||||||
|
// MatrixChat.screenAfterLogin so that you could follow #/room links etc
|
||||||
|
// through a CAS login.
|
||||||
|
parsedUrl.hash = "";
|
||||||
|
|
||||||
parsedUrl.query["homeserver"] = client.getHomeserverUrl();
|
parsedUrl.query["homeserver"] = client.getHomeserverUrl();
|
||||||
parsedUrl.query["identityServer"] = client.getIdentityServerUrl();
|
parsedUrl.query["identityServer"] = client.getIdentityServerUrl();
|
||||||
var casUrl = client.getCasLoginUrl(url.format(parsedUrl));
|
const casUrl = client.getCasLoginUrl(url.format(parsedUrl));
|
||||||
window.location.href = casUrl;
|
window.location.href = casUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import commonmark from 'commonmark';
|
import commonmark from 'commonmark';
|
||||||
import escape from 'lodash/escape';
|
import escape from 'lodash/escape';
|
||||||
|
|
||||||
const ALLOWED_HTML_TAGS = ['del'];
|
const ALLOWED_HTML_TAGS = ['del', 'u'];
|
||||||
|
|
||||||
// These types of node are definitely text
|
// These types of node are definitely text
|
||||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,13 +17,10 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import q from "q";
|
|
||||||
import Matrix from 'matrix-js-sdk';
|
|
||||||
import utils from 'matrix-js-sdk/lib/utils';
|
import utils from 'matrix-js-sdk/lib/utils';
|
||||||
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
|
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
|
||||||
import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
|
import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
|
||||||
|
import createMatrixClient from './utils/createMatrixClient';
|
||||||
const localStorage = window.localStorage;
|
|
||||||
|
|
||||||
interface MatrixClientCreds {
|
interface MatrixClientCreds {
|
||||||
homeserverUrl: string,
|
homeserverUrl: string,
|
||||||
|
@ -50,7 +48,6 @@ class MatrixClientPeg {
|
||||||
this.opts = {
|
this.opts = {
|
||||||
initialSyncLimit: 20,
|
initialSyncLimit: 20,
|
||||||
};
|
};
|
||||||
this.indexedDbWorkerScript = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,7 +58,7 @@ class MatrixClientPeg {
|
||||||
* @param {string} script href to the script to be passed to the web worker
|
* @param {string} script href to the script to be passed to the web worker
|
||||||
*/
|
*/
|
||||||
setIndexedDbWorkerScript(script) {
|
setIndexedDbWorkerScript(script) {
|
||||||
this.indexedDbWorkerScript = script;
|
createMatrixClient.indexedDbWorkerScript = script;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(): MatrixClient {
|
get(): MatrixClient {
|
||||||
|
@ -80,20 +77,38 @@ class MatrixClientPeg {
|
||||||
this._createClient(creds);
|
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);
|
const opts = utils.deepCopy(this.opts);
|
||||||
// the react sdk doesn't work without this, so don't allow
|
// the react sdk doesn't work without this, so don't allow
|
||||||
opts.pendingEventOrdering = "detached";
|
opts.pendingEventOrdering = "detached";
|
||||||
|
|
||||||
|
try {
|
||||||
let promise = this.matrixClient.store.startup();
|
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)
|
// log any errors when starting up the database (if one exists)
|
||||||
promise.catch((err) => { console.error(err); });
|
console.error(`Error starting matrixclient store: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
// regardless of errors, start the client. If we did error out, we'll
|
// regardless of errors, start the client. If we did error out, we'll
|
||||||
// just end up doing a full initial /sync.
|
// just end up doing a full initial /sync.
|
||||||
promise.finally(() => {
|
|
||||||
|
console.log(`MatrixClientPeg: really starting MatrixClient`);
|
||||||
this.get().startClient(opts);
|
this.get().startClient(opts);
|
||||||
});
|
console.log(`MatrixClientPeg: MatrixClient started`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCredentials(): MatrixClientCreds {
|
getCredentials(): MatrixClientCreds {
|
||||||
|
@ -130,22 +145,7 @@ class MatrixClientPeg {
|
||||||
timelineSupport: true,
|
timelineSupport: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (localStorage) {
|
this.matrixClient = createMatrixClient(opts, this.indexedDbWorkerScript);
|
||||||
opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
|
|
||||||
}
|
|
||||||
if (window.indexedDB && localStorage) {
|
|
||||||
// FIXME: bodge to remove old database. Remove this after a few weeks.
|
|
||||||
window.indexedDB.deleteDatabase("matrix-js-sdk:default");
|
|
||||||
|
|
||||||
opts.store = new Matrix.IndexedDBStore({
|
|
||||||
indexedDB: window.indexedDB,
|
|
||||||
dbName: "riot-web-sync",
|
|
||||||
localStorage: localStorage,
|
|
||||||
workerScript: this.indexedDbWorkerScript,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.matrixClient = Matrix.createClient(opts);
|
|
||||||
|
|
||||||
// we're going to add eventlisteners for each matrix event tile, so the
|
// we're going to add eventlisteners for each matrix event tile, so the
|
||||||
// potential number of event listeners is quite high.
|
// potential number of event listeners is quite high.
|
||||||
|
|
10
src/Modal.js
10
src/Modal.js
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var ReactDOM = require('react-dom');
|
var ReactDOM = require('react-dom');
|
||||||
|
import Analytics from './Analytics';
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
|
|
||||||
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
||||||
|
@ -63,7 +64,6 @@ const AsyncWrapper = React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const {loader, ...otherProps} = this.props;
|
const {loader, ...otherProps} = this.props;
|
||||||
|
|
||||||
if (this.state.component) {
|
if (this.state.component) {
|
||||||
const Component = this.state.component;
|
const Component = this.state.component;
|
||||||
return <Component {...otherProps} />;
|
return <Component {...otherProps} />;
|
||||||
|
@ -104,6 +104,9 @@ class ModalManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
createDialog(Element, props, className) {
|
createDialog(Element, props, className) {
|
||||||
|
if (props && props.title) {
|
||||||
|
Analytics.trackEvent('Modal', props.title, 'createDialog');
|
||||||
|
}
|
||||||
return this.createDialogAsync((cb) => {cb(Element);}, props, className);
|
return this.createDialogAsync((cb) => {cb(Element);}, props, className);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,4 +198,7 @@ class ModalManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ModalManager();
|
if (!global.singletonModalManager) {
|
||||||
|
global.singletonModalManager = new ModalManager();
|
||||||
|
}
|
||||||
|
export default global.singletonModalManager;
|
||||||
|
|
|
@ -15,11 +15,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
var PlatformPeg = require("./PlatformPeg");
|
import PlatformPeg from './PlatformPeg';
|
||||||
var TextForEvent = require('./TextForEvent');
|
import TextForEvent from './TextForEvent';
|
||||||
var Avatar = require('./Avatar');
|
import Analytics from './Analytics';
|
||||||
var dis = require("./dispatcher");
|
import Avatar from './Avatar';
|
||||||
|
import dis from './dispatcher';
|
||||||
|
import sdk from './index';
|
||||||
|
import { _t } from './languageHandler';
|
||||||
|
import Modal from './Modal';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Dispatches:
|
* Dispatches:
|
||||||
|
@ -29,7 +33,7 @@ var dis = require("./dispatcher");
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Notifier = {
|
const Notifier = {
|
||||||
notifsByRoom: {},
|
notifsByRoom: {},
|
||||||
|
|
||||||
notificationMessageForEvent: function(ev) {
|
notificationMessageForEvent: function(ev) {
|
||||||
|
@ -48,16 +52,16 @@ var Notifier = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var msg = this.notificationMessageForEvent(ev);
|
let msg = this.notificationMessageForEvent(ev);
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
|
||||||
var title;
|
let title;
|
||||||
if (!ev.sender || room.name == ev.sender.name) {
|
if (!ev.sender || room.name === ev.sender.name) {
|
||||||
title = room.name;
|
title = room.name;
|
||||||
// notificationMessageForEvent includes sender,
|
// notificationMessageForEvent includes sender,
|
||||||
// but we already have the sender here
|
// but we already have the sender here
|
||||||
if (ev.getContent().body) msg = ev.getContent().body;
|
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
|
// context is all in the message here, we don't need
|
||||||
// to display sender info
|
// to display sender info
|
||||||
title = room.name;
|
title = room.name;
|
||||||
|
@ -68,7 +72,7 @@ var Notifier = {
|
||||||
if (ev.getContent().body) msg = ev.getContent().body;
|
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'
|
ev.sender, 40, 40, 'crop'
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
@ -83,7 +87,7 @@ var Notifier = {
|
||||||
},
|
},
|
||||||
|
|
||||||
_playAudioNotification: function(ev, room) {
|
_playAudioNotification: function(ev, room) {
|
||||||
var e = document.getElementById("messageAudio");
|
const e = document.getElementById("messageAudio");
|
||||||
if (e) {
|
if (e) {
|
||||||
e.load();
|
e.load();
|
||||||
e.play();
|
e.play();
|
||||||
|
@ -95,7 +99,7 @@ var Notifier = {
|
||||||
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
|
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
|
||||||
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
|
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
|
||||||
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
|
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);
|
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
|
||||||
this.toolbarHidden = false;
|
this.toolbarHidden = false;
|
||||||
this.isSyncing = false;
|
this.isSyncing = false;
|
||||||
|
@ -104,7 +108,7 @@ var Notifier = {
|
||||||
stop: function() {
|
stop: function() {
|
||||||
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
|
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
|
||||||
MatrixClientPeg.get().removeListener('Room.timeline', 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);
|
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
||||||
}
|
}
|
||||||
this.isSyncing = false;
|
this.isSyncing = false;
|
||||||
|
@ -118,10 +122,13 @@ var Notifier = {
|
||||||
setEnabled: function(enable, callback) {
|
setEnabled: function(enable, callback) {
|
||||||
const plaf = PlatformPeg.get();
|
const plaf = PlatformPeg.get();
|
||||||
if (!plaf) return;
|
if (!plaf) return;
|
||||||
|
|
||||||
|
Analytics.trackEvent('Notifier', 'Set Enabled', enable);
|
||||||
|
|
||||||
// make sure that we persist the current setting audio_enabled setting
|
// make sure that we persist the current setting audio_enabled setting
|
||||||
// before changing anything
|
// before changing anything
|
||||||
if (global.localStorage) {
|
if (global.localStorage) {
|
||||||
if(global.localStorage.getItem('audio_notifications_enabled') == null) {
|
if (global.localStorage.getItem('audio_notifications_enabled') === null) {
|
||||||
this.setAudioEnabled(this.isEnabled());
|
this.setAudioEnabled(this.isEnabled());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,6 +138,14 @@ var Notifier = {
|
||||||
plaf.requestNotificationPermission().done((result) => {
|
plaf.requestNotificationPermission().done((result) => {
|
||||||
if (result !== 'granted') {
|
if (result !== 'granted') {
|
||||||
// The permission request was dismissed or denied
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +156,7 @@ var Notifier = {
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "notifier_enabled",
|
action: "notifier_enabled",
|
||||||
value: true
|
value: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// clear the notifications_hidden flag, so that if notifications are
|
// clear the notifications_hidden flag, so that if notifications are
|
||||||
|
@ -152,7 +167,7 @@ var Notifier = {
|
||||||
global.localStorage.setItem('notifications_enabled', 'false');
|
global.localStorage.setItem('notifications_enabled', 'false');
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "notifier_enabled",
|
action: "notifier_enabled",
|
||||||
value: false
|
value: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -165,7 +180,7 @@ var Notifier = {
|
||||||
|
|
||||||
if (!global.localStorage) return true;
|
if (!global.localStorage) return true;
|
||||||
|
|
||||||
var enabled = global.localStorage.getItem('notifications_enabled');
|
const enabled = global.localStorage.getItem('notifications_enabled');
|
||||||
if (enabled === null) return true;
|
if (enabled === null) return true;
|
||||||
return enabled === 'true';
|
return enabled === 'true';
|
||||||
},
|
},
|
||||||
|
@ -178,7 +193,7 @@ var Notifier = {
|
||||||
|
|
||||||
isAudioEnabled: function(enable) {
|
isAudioEnabled: function(enable) {
|
||||||
if (!global.localStorage) return true;
|
if (!global.localStorage) return true;
|
||||||
var enabled = global.localStorage.getItem(
|
const enabled = global.localStorage.getItem(
|
||||||
'audio_notifications_enabled');
|
'audio_notifications_enabled');
|
||||||
// default to true if the popups are enabled
|
// default to true if the popups are enabled
|
||||||
if (enabled === null) return this.isEnabled();
|
if (enabled === null) return this.isEnabled();
|
||||||
|
@ -188,11 +203,13 @@ var Notifier = {
|
||||||
setToolbarHidden: function(hidden, persistent = true) {
|
setToolbarHidden: function(hidden, persistent = true) {
|
||||||
this.toolbarHidden = hidden;
|
this.toolbarHidden = hidden;
|
||||||
|
|
||||||
|
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
|
||||||
|
|
||||||
// XXX: why are we dispatching this here?
|
// XXX: why are we dispatching this here?
|
||||||
// this is nothing to do with notifier_enabled
|
// this is nothing to do with notifier_enabled
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "notifier_enabled",
|
action: "notifier_enabled",
|
||||||
value: this.isEnabled()
|
value: this.isEnabled(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// update the info to localStorage for persistent settings
|
// update the info to localStorage for persistent settings
|
||||||
|
@ -215,8 +232,7 @@ var Notifier = {
|
||||||
onSyncStateChange: function(state) {
|
onSyncStateChange: function(state) {
|
||||||
if (state === "SYNCING") {
|
if (state === "SYNCING") {
|
||||||
this.isSyncing = true;
|
this.isSyncing = true;
|
||||||
}
|
} else if (state === "STOPPED" || state === "ERROR") {
|
||||||
else if (state === "STOPPED" || state === "ERROR") {
|
|
||||||
this.isSyncing = false;
|
this.isSyncing = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -225,22 +241,23 @@ var Notifier = {
|
||||||
if (toStartOfTimeline) return;
|
if (toStartOfTimeline) return;
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
if (!this.isSyncing) return; // don't alert for any messages initially
|
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;
|
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
||||||
|
|
||||||
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
||||||
if (actions && actions.notify) {
|
if (actions && actions.notify) {
|
||||||
if (this.isEnabled()) {
|
if (this.isEnabled()) {
|
||||||
this._displayPopupNotification(ev, room);
|
this._displayPopupNotification(ev, room);
|
||||||
}
|
}
|
||||||
if (actions.tweaks.sound && this.isAudioEnabled()) {
|
if (actions.tweaks.sound && this.isAudioEnabled()) {
|
||||||
|
PlatformPeg.get().loudNotification(ev, room);
|
||||||
this._playAudioNotification(ev, room);
|
this._playAudioNotification(ev, room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomReceipt: function(ev, room) {
|
onRoomReceipt: function(ev, room) {
|
||||||
if (room.getUnreadNotificationCount() == 0) {
|
if (room.getUnreadNotificationCount() === 0) {
|
||||||
// ideally we would clear each notification when it was read,
|
// ideally we would clear each notification when it was read,
|
||||||
// but we have no way, given a read receipt, to know whether
|
// but we have no way, given a read receipt, to know whether
|
||||||
// the receipt comes before or after an event, so we can't
|
// the receipt comes before or after an event, so we can't
|
||||||
|
@ -255,7 +272,7 @@ var Notifier = {
|
||||||
}
|
}
|
||||||
delete this.notifsByRoom[room.roomId];
|
delete this.notifsByRoom[room.roomId];
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!global.mxNotifier) {
|
if (!global.mxNotifier) {
|
||||||
|
|
|
@ -23,8 +23,8 @@ limitations under the License.
|
||||||
* { key: $KEY, val: $VALUE, place: "add|del" }
|
* { key: $KEY, val: $VALUE, place: "add|del" }
|
||||||
*/
|
*/
|
||||||
module.exports.getKeyValueArrayDiffs = function(before, after) {
|
module.exports.getKeyValueArrayDiffs = function(before, after) {
|
||||||
var results = [];
|
const results = [];
|
||||||
var delta = {};
|
const delta = {};
|
||||||
Object.keys(before).forEach(function(beforeKey) {
|
Object.keys(before).forEach(function(beforeKey) {
|
||||||
delta[beforeKey] = delta[beforeKey] || 0; // init to 0 initially
|
delta[beforeKey] = delta[beforeKey] || 0; // init to 0 initially
|
||||||
delta[beforeKey]--; // keys present in the past have -ve values
|
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 });
|
results.push({ place: "del", key: muxedKey, val: beforeVal });
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 0: // A mix of added/removed keys
|
case 0: {// A mix of added/removed keys
|
||||||
// compare old & new vals
|
// compare old & new vals
|
||||||
var itemDelta = {};
|
const itemDelta = {};
|
||||||
before[muxedKey].forEach(function(beforeVal) {
|
before[muxedKey].forEach(function(beforeVal) {
|
||||||
itemDelta[beforeVal] = itemDelta[beforeVal] || 0;
|
itemDelta[beforeVal] = itemDelta[beforeVal] || 0;
|
||||||
itemDelta[beforeVal]--;
|
itemDelta[beforeVal]--;
|
||||||
|
@ -68,9 +68,9 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
console.error("Calculated key delta of " + delta[muxedKey] +
|
console.error("Calculated key delta of " + delta[muxedKey] + " - this should never happen!");
|
||||||
" - this should never happen!");
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -79,8 +79,10 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shallow-compare two objects for equality: each key and value must be
|
* Shallow-compare two objects for equality: each key and value must be identical
|
||||||
* 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) {
|
module.exports.shallowEqual = function(objA, objB) {
|
||||||
if (objA === objB) {
|
if (objA === objB) {
|
||||||
|
@ -92,15 +94,15 @@ module.exports.shallowEqual = function(objA, objB) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var keysA = Object.keys(objA);
|
const keysA = Object.keys(objA);
|
||||||
var keysB = Object.keys(objB);
|
const keysB = Object.keys(objB);
|
||||||
|
|
||||||
if (keysA.length !== keysB.length) {
|
if (keysA.length !== keysB.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < keysA.length; i++) {
|
for (let i = 0; i < keysA.length; i++) {
|
||||||
var key = keysA[i];
|
const key = keysA[i];
|
||||||
if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
|
if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -22,4 +23,6 @@ export default {
|
||||||
CreateRoom: "create_room",
|
CreateRoom: "create_room",
|
||||||
RoomDirectory: "room_directory",
|
RoomDirectory: "room_directory",
|
||||||
UserView: "user_view",
|
UserView: "user_view",
|
||||||
|
GroupView: "group_view",
|
||||||
|
MyGroups: "my_groups",
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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.
|
* Allows a user to reset their password on a homeserver.
|
||||||
|
@ -33,7 +34,7 @@ class PasswordReset {
|
||||||
constructor(homeserverUrl, identityUrl) {
|
constructor(homeserverUrl, identityUrl) {
|
||||||
this.client = Matrix.createClient({
|
this.client = Matrix.createClient({
|
||||||
baseUrl: homeserverUrl,
|
baseUrl: homeserverUrl,
|
||||||
idBaseUrl: identityUrl
|
idBaseUrl: identityUrl,
|
||||||
});
|
});
|
||||||
this.clientSecret = this.client.generateClientSecret();
|
this.clientSecret = this.client.generateClientSecret();
|
||||||
this.identityServerDomain = identityUrl.split("://")[1];
|
this.identityServerDomain = identityUrl.split("://")[1];
|
||||||
|
@ -52,8 +53,8 @@ class PasswordReset {
|
||||||
this.sessionId = res.sid;
|
this.sessionId = res.sid;
|
||||||
return res;
|
return res;
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
if (err.errcode == 'M_THREEPID_NOT_FOUND') {
|
if (err.errcode === 'M_THREEPID_NOT_FOUND') {
|
||||||
err.message = "This email address was not found";
|
err.message = _t('This email address was not found');
|
||||||
} else if (err.httpStatus) {
|
} else if (err.httpStatus) {
|
||||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||||
}
|
}
|
||||||
|
@ -74,16 +75,15 @@ class PasswordReset {
|
||||||
threepid_creds: {
|
threepid_creds: {
|
||||||
sid: this.sessionId,
|
sid: this.sessionId,
|
||||||
client_secret: this.clientSecret,
|
client_secret: this.clientSecret,
|
||||||
id_server: this.identityServerDomain
|
id_server: this.identityServerDomain,
|
||||||
}
|
},
|
||||||
}, this.password).catch(function(err) {
|
}, this.password).catch(function(err) {
|
||||||
if (err.httpStatus === 401) {
|
if (err.httpStatus === 401) {
|
||||||
err.message = "Failed to verify email address: make sure you clicked the link in the email";
|
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
|
||||||
}
|
} else if (err.httpStatus === 404) {
|
||||||
else if (err.httpStatus === 404) {
|
err.message =
|
||||||
err.message = "Your email address does not appear to be associated with a Matrix ID on this Homeserver.";
|
_t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.');
|
||||||
}
|
} else if (err.httpStatus) {
|
||||||
else if (err.httpStatus) {
|
|
||||||
err.message += ` (Status ${err.httpStatus})`;
|
err.message += ` (Status ${err.httpStatus})`;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
var dis = require('./dispatcher');
|
import dis from './dispatcher';
|
||||||
var sdk = require('./index');
|
|
||||||
var Modal = require('./Modal');
|
|
||||||
import { EventStatus } from 'matrix-js-sdk';
|
import { EventStatus } from 'matrix-js-sdk';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -37,12 +35,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
resend: function(event) {
|
resend: function(event) {
|
||||||
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||||
MatrixClientPeg.get().resendEvent(
|
MatrixClientPeg.get().resendEvent(event, room).done(function(res) {
|
||||||
event, room
|
|
||||||
).done(function(res) {
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent',
|
action: 'message_sent',
|
||||||
event: event
|
event: event,
|
||||||
});
|
});
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
|
@ -58,7 +54,7 @@ module.exports = {
|
||||||
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_send_failed',
|
action: 'message_send_failed',
|
||||||
event: event
|
event: event,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -66,7 +62,7 @@ module.exports = {
|
||||||
MatrixClientPeg.get().cancelPendingEvent(event);
|
MatrixClientPeg.get().cancelPendingEvent(event);
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_send_cancelled',
|
action: 'message_send_cancelled',
|
||||||
event: event
|
event: event,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ import * as sdk from './index';
|
||||||
import * as emojione from 'emojione';
|
import * as emojione from 'emojione';
|
||||||
import {stateToHTML} from 'draft-js-export-html';
|
import {stateToHTML} from 'draft-js-export-html';
|
||||||
import {SelectionRange} from "./autocomplete/Autocompleter";
|
import {SelectionRange} from "./autocomplete/Autocompleter";
|
||||||
|
import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown';
|
||||||
|
|
||||||
const MARKDOWN_REGEX = {
|
const MARKDOWN_REGEX = {
|
||||||
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
|
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
|
||||||
|
@ -30,9 +31,26 @@ const USERNAME_REGEX = /@\S+:\S+/g;
|
||||||
const ROOM_REGEX = /#\S+:\S+/g;
|
const ROOM_REGEX = /#\S+:\S+/g;
|
||||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, '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));
|
return ContentState.createFromBlockArray(convertFromHTML(html));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,31 +113,6 @@ let emojiDecorator = {
|
||||||
* Returns a composite decorator which has access to provided scope.
|
* Returns a composite decorator which has access to provided scope.
|
||||||
*/
|
*/
|
||||||
export function getScopedRTDecorators(scope: any): CompositeDecorator {
|
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];
|
return [emojiDecorator];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,9 +139,9 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
markdownDecorators.push(emojiDecorator);
|
// markdownDecorators.push(emojiDecorator);
|
||||||
|
// TODO Consider renabling "syntax highlighting" when we can do it properly
|
||||||
return markdownDecorators;
|
return [emojiDecorator];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -286,3 +279,14 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor
|
||||||
|
|
||||||
return editorState;
|
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
34
src/Roles.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,8 +19,7 @@ limitations under the License.
|
||||||
function tsOfNewestEvent(room) {
|
function tsOfNewestEvent(room) {
|
||||||
if (room.timeline.length) {
|
if (room.timeline.length) {
|
||||||
return room.timeline[room.timeline.length - 1].getTs();
|
return room.timeline[room.timeline.length - 1].getTs();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return Number.MAX_SAFE_INTEGER;
|
return Number.MAX_SAFE_INTEGER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,5 +31,5 @@ function mostRecentActivityFirst(roomList) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mostRecentActivityFirst: mostRecentActivityFirst
|
mostRecentActivityFirst,
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
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_LOUD = 'all_messages_loud';
|
||||||
export const ALL_MESSAGES = 'all_messages';
|
export const ALL_MESSAGES = 'all_messages';
|
||||||
|
@ -52,7 +52,7 @@ export function getRoomNotifsState(roomId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setRoomNotifsState(roomId, newState) {
|
export function setRoomNotifsState(roomId, newState) {
|
||||||
if (newState == MUTE) {
|
if (newState === MUTE) {
|
||||||
return setRoomNotifsStateMuted(roomId);
|
return setRoomNotifsStateMuted(roomId);
|
||||||
} else {
|
} else {
|
||||||
return setRoomNotifsStateUnmuted(roomId, newState);
|
return setRoomNotifsStateUnmuted(roomId, newState);
|
||||||
|
@ -80,14 +80,14 @@ function setRoomNotifsStateMuted(roomId) {
|
||||||
kind: 'event_match',
|
kind: 'event_match',
|
||||||
key: 'room_id',
|
key: 'room_id',
|
||||||
pattern: roomId,
|
pattern: roomId,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
'dont_notify',
|
'dont_notify',
|
||||||
]
|
],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return q.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRoomNotifsStateUnmuted(roomId, newState) {
|
function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||||
|
@ -99,16 +99,16 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||||
promises.push(cli.deletePushRule('global', 'override', overrideMuteRule.rule_id));
|
promises.push(cli.deletePushRule('global', 'override', overrideMuteRule.rule_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newState == 'all_messages') {
|
if (newState === 'all_messages') {
|
||||||
const roomRule = cli.getRoomPushRule('global', roomId);
|
const roomRule = cli.getRoomPushRule('global', roomId);
|
||||||
if (roomRule) {
|
if (roomRule) {
|
||||||
promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id));
|
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, {
|
promises.push(cli.addPushRule('global', 'room', roomId, {
|
||||||
actions: [
|
actions: [
|
||||||
'dont_notify',
|
'dont_notify',
|
||||||
]
|
],
|
||||||
}));
|
}));
|
||||||
// https://matrix.org/jira/browse/SPEC-400
|
// https://matrix.org/jira/browse/SPEC-400
|
||||||
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
||||||
|
@ -119,14 +119,14 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||||
{
|
{
|
||||||
set_tweak: 'sound',
|
set_tweak: 'sound',
|
||||||
value: 'default',
|
value: 'default',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}));
|
}));
|
||||||
// https://matrix.org/jira/browse/SPEC-400
|
// https://matrix.org/jira/browse/SPEC-400
|
||||||
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
return q.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findOverrideMuteRule(roomId) {
|
function findOverrideMuteRule(roomId) {
|
||||||
|
@ -145,20 +145,10 @@ function isRuleForRoom(roomId, rule) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const cond = rule.conditions[0];
|
const cond = rule.conditions[0];
|
||||||
if (
|
return (cond.kind === 'event_match' && cond.key === 'room_id' && cond.pattern === roomId);
|
||||||
cond.kind == 'event_match' &&
|
|
||||||
cond.key == 'room_id' &&
|
|
||||||
cond.pattern == roomId
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMuteRule(rule) {
|
function isMuteRule(rule) {
|
||||||
return (
|
return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify');
|
||||||
rule.actions.length == 1 &&
|
|
||||||
rule.actions[0] == 'dont_notify'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
18
src/Rooms.js
18
src/Rooms.js
|
@ -15,8 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import DMRoomMap from './utils/DMRoomMap';
|
import Promise from 'bluebird';
|
||||||
import q from 'q';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a room object, return the alias we should use for it,
|
* 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) {
|
export function setDMRoom(roomId, userId) {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
return q();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct');
|
const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct');
|
||||||
|
@ -145,7 +144,18 @@ export function guessDMRoomTarget(room, me) {
|
||||||
let oldestTs;
|
let oldestTs;
|
||||||
let oldestUser;
|
let oldestUser;
|
||||||
|
|
||||||
// Pick the user who's been here longest (and isn't us)
|
// Pick the joined user who's been here longest (and isn't us),
|
||||||
|
for (const user of room.getJoinedMembers()) {
|
||||||
|
if (user.userId == me.userId) continue;
|
||||||
|
|
||||||
|
if (oldestTs === undefined || user.events.member.getTs() < oldestTs) {
|
||||||
|
oldestUser = user;
|
||||||
|
oldestTs = user.events.member.getTs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldestUser) return oldestUser;
|
||||||
|
|
||||||
|
// if there are no joined members other than us, use the oldest member
|
||||||
for (const user of room.currentState.getMembers()) {
|
for (const user of room.currentState.getMembers()) {
|
||||||
if (user.userId == me.userId) continue;
|
if (user.userId == me.userId) continue;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import 'whatwg-fetch';
|
import 'whatwg-fetch';
|
||||||
|
|
||||||
|
let fetchFunction = fetch;
|
||||||
|
|
||||||
function checkStatus(response) {
|
function checkStatus(response) {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return response.text().then((text) => {
|
return response.text().then((text) => {
|
||||||
|
@ -31,7 +33,7 @@ const request = (url, opts) => {
|
||||||
opts.body = JSON.stringify(opts.body);
|
opts.body = JSON.stringify(opts.body);
|
||||||
opts.headers['Content-Type'] = 'application/json';
|
opts.headers['Content-Type'] = 'application/json';
|
||||||
}
|
}
|
||||||
return fetch(url, opts)
|
return fetchFunction(url, opts)
|
||||||
.then(checkStatus)
|
.then(checkStatus)
|
||||||
.then(parseJson);
|
.then(parseJson);
|
||||||
};
|
};
|
||||||
|
@ -64,7 +66,7 @@ export default class RtsClient {
|
||||||
client_secret: clientSecret,
|
client_secret: clientSecret,
|
||||||
},
|
},
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +76,7 @@ export default class RtsClient {
|
||||||
qs: {
|
qs: {
|
||||||
team_token: teamToken,
|
team_token: teamToken,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +93,12 @@ export default class RtsClient {
|
||||||
qs: {
|
qs: {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allow fetch to be replaced, for testing.
|
||||||
|
static setFetch(fn) {
|
||||||
|
fetchFunction = fn;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var q = require("q");
|
import Promise from 'bluebird';
|
||||||
var request = require('browser-request');
|
var request = require('browser-request');
|
||||||
|
|
||||||
var SdkConfig = require('./SdkConfig');
|
var SdkConfig = require('./SdkConfig');
|
||||||
|
@ -39,7 +39,7 @@ class ScalarAuthClient {
|
||||||
// Returns a scalar_token string
|
// Returns a scalar_token string
|
||||||
getScalarToken() {
|
getScalarToken() {
|
||||||
var tok = window.localStorage.getItem("mx_scalar_token");
|
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
|
// No saved token, so do the dance to get one. First, we
|
||||||
// need an openid bearer token from the HS.
|
// need an openid bearer token from the HS.
|
||||||
|
@ -53,7 +53,7 @@ class ScalarAuthClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
exchangeForScalarToken(openid_token_object) {
|
exchangeForScalarToken(openid_token_object) {
|
||||||
var defer = q.defer();
|
var defer = Promise.defer();
|
||||||
|
|
||||||
var scalar_rest_url = SdkConfig.get().integrations_rest_url;
|
var scalar_rest_url = SdkConfig.get().integrations_rest_url;
|
||||||
request({
|
request({
|
||||||
|
@ -76,10 +76,13 @@ class ScalarAuthClient {
|
||||||
return defer.promise;
|
return defer.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
getScalarInterfaceUrlForRoom(roomId) {
|
getScalarInterfaceUrlForRoom(roomId, screen) {
|
||||||
var url = SdkConfig.get().integrations_ui_url;
|
var url = SdkConfig.get().integrations_ui_url;
|
||||||
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
||||||
url += "&room_id=" + encodeURIComponent(roomId);
|
url += "&room_id=" + encodeURIComponent(roomId);
|
||||||
|
if (screen) {
|
||||||
|
url += '&screen=' + encodeURIComponent(screen);
|
||||||
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,4 +92,3 @@ class ScalarAuthClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ScalarAuthClient;
|
module.exports = ScalarAuthClient;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,7 +18,7 @@ limitations under the License.
|
||||||
/*
|
/*
|
||||||
Listens for incoming postMessage requests from the integrations UI URL. The following API is exposed:
|
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,
|
room_id: $ROOM_ID,
|
||||||
user_id: $USER_ID
|
user_id: $USER_ID
|
||||||
// additional request fields
|
// 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
|
membership_state AND bot_options
|
||||||
--------------------------------
|
--------------------------------
|
||||||
Get the content of the "m.room.member" or "m.room.bot.options" state event respectively.
|
Get the content of the "m.room.member" or "m.room.bot.options" state event respectively.
|
||||||
|
@ -125,6 +235,7 @@ const SdkConfig = require('./SdkConfig');
|
||||||
const MatrixClientPeg = require("./MatrixClientPeg");
|
const MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
|
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
|
||||||
const dis = require("./dispatcher");
|
const dis = require("./dispatcher");
|
||||||
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
function sendResponse(event, res) {
|
function sendResponse(event, res) {
|
||||||
const data = JSON.parse(JSON.stringify(event.data));
|
const data = JSON.parse(JSON.stringify(event.data));
|
||||||
|
@ -150,7 +261,7 @@ function inviteUser(event, roomId, userId) {
|
||||||
console.log(`Received request to invite ${userId} into room ${roomId}`);
|
console.log(`Received request to invite ${userId} into room ${roomId}`);
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
sendError(event, "You need to be logged in.");
|
sendError(event, _t('You need to be logged in.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
|
@ -170,10 +281,91 @@ function inviteUser(event, roomId, userId) {
|
||||||
success: true,
|
success: true,
|
||||||
});
|
});
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
sendError(event, "You need to be able to invite users to do that.", err);
|
sendError(event, _t('You need to be able to invite users to do that.'), err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function setPlumbingState(event, roomId, status) {
|
||||||
if (typeof status !== 'string') {
|
if (typeof status !== 'string') {
|
||||||
throw new Error('Plumbing state status should be a 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}`);
|
console.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`);
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
sendError(event, "You need to be logged in.");
|
sendError(event, _t('You need to be logged in.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.sendStateEvent(roomId, "m.room.plumbing", { status : status }).done(() => {
|
client.sendStateEvent(roomId, "m.room.plumbing", { status : status }).done(() => {
|
||||||
|
@ -189,7 +381,7 @@ function setPlumbingState(event, roomId, status) {
|
||||||
success: true,
|
success: true,
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
sendError(event, err.message ? err.message : "Failed to send request.", err);
|
sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +389,7 @@ function setBotOptions(event, roomId, userId) {
|
||||||
console.log(`Received request to set options for bot ${userId} in room ${roomId}`);
|
console.log(`Received request to set options for bot ${userId} in room ${roomId}`);
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
sendError(event, "You need to be logged in.");
|
sendError(event, _t('You need to be logged in.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => {
|
client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => {
|
||||||
|
@ -205,20 +397,20 @@ function setBotOptions(event, roomId, userId) {
|
||||||
success: true,
|
success: true,
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
sendError(event, err.message ? err.message : "Failed to send request.", err);
|
sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBotPower(event, roomId, userId, level) {
|
function setBotPower(event, roomId, userId, level) {
|
||||||
if (!(Number.isInteger(level) && level >= 0)) {
|
if (!(Number.isInteger(level) && level >= 0)) {
|
||||||
sendError(event, "Power level must be positive integer.");
|
sendError(event, _t('Power level must be positive integer.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`);
|
console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`);
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
sendError(event, "You need to be logged in.");
|
sendError(event, _t('You need to be logged in.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,7 +427,7 @@ function setBotPower(event, roomId, userId, level) {
|
||||||
success: true,
|
success: true,
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
sendError(event, err.message ? err.message : "Failed to send request.", err);
|
sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -255,15 +447,66 @@ function botOptions(event, roomId, userId) {
|
||||||
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
|
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function returnStateEvent(event, roomId, eventType, stateKey) {
|
function getMembershipCount(event, roomId) {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
sendError(event, "You need to be logged in.");
|
sendError(event, _t('You need to be logged in.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
sendError(event, "This room is not recognised.");
|
sendError(event, _t('This room is not recognised.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const count = room.getJoinedMembers().length;
|
||||||
|
sendResponse(event, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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;
|
return;
|
||||||
}
|
}
|
||||||
const stateEvent = room.currentState.getStateEvents(eventType, stateKey);
|
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
|
// All strings start with the empty string, so for sanity return if the length
|
||||||
// of the event origin is 0.
|
// of the event origin is 0.
|
||||||
let url = SdkConfig.get().integrations_ui_url;
|
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
|
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 roomId = event.data.room_id;
|
||||||
const userId = event.data.user_id;
|
const userId = event.data.user_id;
|
||||||
if (!roomId) {
|
if (!roomId) {
|
||||||
sendError(event, "Missing room_id in request");
|
sendError(event, _t('Missing room_id in request'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let promise = Promise.resolve(currentRoomId);
|
let promise = Promise.resolve(currentRoomId);
|
||||||
if (!currentRoomId) {
|
if (!currentRoomId) {
|
||||||
if (!currentRoomAlias) {
|
if (!currentRoomAlias) {
|
||||||
sendError(event, "Must be viewing a room");
|
sendError(event, _t('Must be viewing a room'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// no room ID but there is an alias, look it up.
|
// no room ID but there is an alias, look it up.
|
||||||
|
@ -331,21 +574,33 @@ const onMessage = function(event) {
|
||||||
|
|
||||||
promise.then((viewingRoomId) => {
|
promise.then((viewingRoomId) => {
|
||||||
if (roomId !== viewingRoomId) {
|
if (roomId !== viewingRoomId) {
|
||||||
sendError(event, "Room " + roomId + " not visible");
|
sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getting join rules does not require userId
|
// These APIs don't require userId
|
||||||
if (event.data.action === "join_rules_state") {
|
if (event.data.action === "join_rules_state") {
|
||||||
getJoinRules(event, roomId);
|
getJoinRules(event, roomId);
|
||||||
return;
|
return;
|
||||||
} else if (event.data.action === "set_plumbing_state") {
|
} else if (event.data.action === "set_plumbing_state") {
|
||||||
setPlumbingState(event, roomId, event.data.status);
|
setPlumbingState(event, roomId, event.data.status);
|
||||||
return;
|
return;
|
||||||
|
} else if (event.data.action === "get_membership_count") {
|
||||||
|
getMembershipCount(event, roomId);
|
||||||
|
return;
|
||||||
|
} 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) {
|
if (!userId) {
|
||||||
sendError(event, "Missing user_id in request");
|
sendError(event, _t('Missing user_id in request'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (event.data.action) {
|
switch (event.data.action) {
|
||||||
|
@ -370,16 +625,31 @@ const onMessage = function(event) {
|
||||||
}
|
}
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
sendError(event, "Failed to lookup current room.");
|
sendError(event, _t('Failed to lookup current room') + '.');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let listenerCount = 0;
|
||||||
module.exports = {
|
module.exports = {
|
||||||
startListening: function() {
|
startListening: function() {
|
||||||
|
if (listenerCount === 0) {
|
||||||
window.addEventListener("message", onMessage, false);
|
window.addEventListener("message", onMessage, false);
|
||||||
|
}
|
||||||
|
listenerCount += 1;
|
||||||
},
|
},
|
||||||
|
|
||||||
stopListening: function() {
|
stopListening: function() {
|
||||||
|
listenerCount -= 1;
|
||||||
|
if (listenerCount === 0) {
|
||||||
window.removeEventListener("message", onMessage);
|
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);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var DEFAULTS = {
|
const DEFAULTS = {
|
||||||
// URL to a page we show in an iframe to configure integrations
|
// URL to a page we show in an iframe to configure integrations
|
||||||
integrations_ui_url: "https://scalar.vector.im/",
|
integrations_ui_url: "https://scalar.vector.im/",
|
||||||
// Base URL to the REST interface of the integrations server
|
// Base URL to the REST interface of the integrations server
|
||||||
|
@ -30,8 +30,8 @@ class SdkConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
static put(cfg) {
|
static put(cfg) {
|
||||||
var defaultKeys = Object.keys(DEFAULTS);
|
const defaultKeys = Object.keys(DEFAULTS);
|
||||||
for (var i = 0; i < defaultKeys.length; ++i) {
|
for (let i = 0; i < defaultKeys.length; ++i) {
|
||||||
if (cfg[defaultKeys[i]] === undefined) {
|
if (cfg[defaultKeys[i]] === undefined) {
|
||||||
cfg[defaultKeys[i]] = DEFAULTS[defaultKeys[i]];
|
cfg[defaultKeys[i]] = DEFAULTS[defaultKeys[i]];
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,39 +25,44 @@ class Skinner {
|
||||||
"Attempted to get a component before a skin has been loaded."+
|
"Attempted to get a component before a skin has been loaded."+
|
||||||
" This is probably because either:"+
|
" This is probably because either:"+
|
||||||
" a) Your app has not called sdk.loadSkin(), or"+
|
" a) Your app has not called sdk.loadSkin(), or"+
|
||||||
" b) A component has called getComponent at the root level"
|
" b) A component has called getComponent at the root level",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
var comp = this.components[name];
|
let comp = this.components[name];
|
||||||
if (comp) {
|
|
||||||
return comp;
|
|
||||||
}
|
|
||||||
// XXX: Temporarily also try 'views.' as we're currently
|
// XXX: Temporarily also try 'views.' as we're currently
|
||||||
// leaving the 'views.' off views.
|
// leaving the 'views.' off views.
|
||||||
var comp = this.components['views.'+name];
|
if (!comp) {
|
||||||
if (comp) {
|
comp = this.components['views.'+name];
|
||||||
return comp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!comp) {
|
||||||
throw new Error("No such component: "+name);
|
throw new Error("No such component: "+name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// components have to be functions.
|
||||||
|
const validType = typeof comp === 'function';
|
||||||
|
if (!validType) {
|
||||||
|
throw new Error(`Not a valid component: ${name}.`);
|
||||||
|
}
|
||||||
|
return comp;
|
||||||
|
}
|
||||||
|
|
||||||
load(skinObject) {
|
load(skinObject) {
|
||||||
if (this.components !== null) {
|
if (this.components !== null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempted to load a skin while a skin is already loaded"+
|
"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 = {};
|
this.components = {};
|
||||||
var compKeys = Object.keys(skinObject.components);
|
const compKeys = Object.keys(skinObject.components);
|
||||||
for (var i = 0; i < compKeys.length; ++i) {
|
for (let i = 0; i < compKeys.length; ++i) {
|
||||||
var comp = skinObject.components[compKeys[i]];
|
const comp = skinObject.components[compKeys[i]];
|
||||||
this.addComponent(compKeys[i], comp);
|
this.addComponent(compKeys[i], comp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addComponent(name, comp) {
|
addComponent(name, comp) {
|
||||||
var slot = name;
|
let slot = name;
|
||||||
if (comp.replaces !== undefined) {
|
if (comp.replaces !== undefined) {
|
||||||
if (comp.replaces.indexOf('.') > -1) {
|
if (comp.replaces.indexOf('.') > -1) {
|
||||||
slot = comp.replaces;
|
slot = comp.replaces;
|
||||||
|
|
|
@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
import MatrixClientPeg from "./MatrixClientPeg";
|
||||||
var dis = require("./dispatcher");
|
import dis from "./dispatcher";
|
||||||
var Tinter = require("./Tinter");
|
import Tinter from "./Tinter";
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
|
import { _t } from './languageHandler';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,58 +42,64 @@ class Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
getUsage() {
|
getUsage() {
|
||||||
return "Usage: " + this.getCommandWithArgs();
|
return _t('Usage') + ': ' + this.getCommandWithArgs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var reject = function(msg) {
|
function reject(msg) {
|
||||||
return {
|
return {
|
||||||
error: msg
|
error: msg,
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var success = function(promise) {
|
function success(promise) {
|
||||||
return {
|
return {
|
||||||
promise: promise
|
promise: promise,
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var commands = {
|
/* Disable the "unexpected this" error for these commands - all of the run
|
||||||
|
* functions are called with `this` bound to the Command instance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable babel/no-invalid-this */
|
||||||
|
|
||||||
|
const commands = {
|
||||||
ddg: new Command("ddg", "<query>", function(roomId, args) {
|
ddg: new Command("ddg", "<query>", function(roomId, args) {
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
// TODO Don't explain this away, actually show a search UI here.
|
// TODO Don't explain this away, actually show a search UI here.
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "/ddg is not a command",
|
title: _t('/ddg is not a command'),
|
||||||
description: "To use it, just wait for autocomplete results to load and tab through them.",
|
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
|
||||||
});
|
});
|
||||||
return success();
|
return success();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Change your nickname
|
// Change your nickname
|
||||||
nick: new Command("nick", "<display_name>", function(room_id, args) {
|
nick: new Command("nick", "<display_name>", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().setDisplayName(args)
|
MatrixClientPeg.get().setDisplayName(args),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Changes the colorscheme of your current room
|
// Changes the colorscheme of your current room
|
||||||
tint: new Command("tint", "<color1> [<color2>]", function(room_id, args) {
|
tint: new Command("tint", "<color1> [<color2>]", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
|
const matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
Tinter.tint(matches[1], matches[4]);
|
Tinter.tint(matches[1], matches[4]);
|
||||||
var colorScheme = {};
|
const colorScheme = {};
|
||||||
colorScheme.primary_color = matches[1];
|
colorScheme.primary_color = matches[1];
|
||||||
if (matches[4]) {
|
if (matches[4]) {
|
||||||
colorScheme.secondary_color = matches[4];
|
colorScheme.secondary_color = matches[4];
|
||||||
}
|
}
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().setRoomAccountData(
|
MatrixClientPeg.get().setRoomAccountData(
|
||||||
room_id, "org.matrix.room.color_scheme", colorScheme
|
roomId, "org.matrix.room.color_scheme", colorScheme,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,22 +107,22 @@ var commands = {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Change the room topic
|
// Change the room topic
|
||||||
topic: new Command("topic", "<topic>", function(room_id, args) {
|
topic: new Command("topic", "<topic>", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().setRoomTopic(room_id, args)
|
MatrixClientPeg.get().setRoomTopic(roomId, args),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Invite a user
|
// Invite a user
|
||||||
invite: new Command("invite", "<userId>", function(room_id, args) {
|
invite: new Command("invite", "<userId>", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+)$/);
|
const matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().invite(room_id, matches[1])
|
MatrixClientPeg.get().invite(roomId, matches[1]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,21 +130,21 @@ var commands = {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Join a room
|
// Join a room
|
||||||
join: new Command("join", "#alias:domain", function(room_id, args) {
|
join: new Command("join", "#alias:domain", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+)$/);
|
const matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
var room_alias = matches[1];
|
let roomAlias = matches[1];
|
||||||
if (room_alias[0] !== '#') {
|
if (roomAlias[0] !== '#') {
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
}
|
}
|
||||||
if (!room_alias.match(/:/)) {
|
if (!roomAlias.match(/:/)) {
|
||||||
room_alias += ':' + MatrixClientPeg.get().getDomain();
|
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
||||||
}
|
}
|
||||||
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_alias: room_alias,
|
room_alias: roomAlias,
|
||||||
auto_join: true,
|
auto_join: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -147,29 +154,29 @@ var commands = {
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
}),
|
}),
|
||||||
|
|
||||||
part: new Command("part", "[#alias:domain]", function(room_id, args) {
|
part: new Command("part", "[#alias:domain]", function(roomId, args) {
|
||||||
var targetRoomId;
|
let targetRoomId;
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+)$/);
|
const matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
var room_alias = matches[1];
|
let roomAlias = matches[1];
|
||||||
if (room_alias[0] !== '#') {
|
if (roomAlias[0] !== '#') {
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
}
|
}
|
||||||
if (!room_alias.match(/:/)) {
|
if (!roomAlias.match(/:/)) {
|
||||||
room_alias += ':' + MatrixClientPeg.get().getDomain();
|
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find a room with this alias
|
// Try to find a room with this alias
|
||||||
var rooms = MatrixClientPeg.get().getRooms();
|
const rooms = MatrixClientPeg.get().getRooms();
|
||||||
for (var i = 0; i < rooms.length; i++) {
|
for (let i = 0; i < rooms.length; i++) {
|
||||||
var aliasEvents = rooms[i].currentState.getStateEvents(
|
const aliasEvents = rooms[i].currentState.getStateEvents(
|
||||||
"m.room.aliases"
|
"m.room.aliases",
|
||||||
);
|
);
|
||||||
for (var j = 0; j < aliasEvents.length; j++) {
|
for (let j = 0; j < aliasEvents.length; j++) {
|
||||||
var aliases = aliasEvents[j].getContent().aliases || [];
|
const aliases = aliasEvents[j].getContent().aliases || [];
|
||||||
for (var k = 0; k < aliases.length; k++) {
|
for (let k = 0; k < aliases.length; k++) {
|
||||||
if (aliases[k] === room_alias) {
|
if (aliases[k] === roomAlias) {
|
||||||
targetRoomId = rooms[i].roomId;
|
targetRoomId = rooms[i].roomId;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -178,27 +185,28 @@ var commands = {
|
||||||
}
|
}
|
||||||
if (targetRoomId) { break; }
|
if (targetRoomId) { break; }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!targetRoomId) {
|
if (!targetRoomId) {
|
||||||
return reject("Unrecognised room alias: " + room_alias);
|
return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!targetRoomId) targetRoomId = room_id;
|
}
|
||||||
|
if (!targetRoomId) targetRoomId = roomId;
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().leave(targetRoomId).then(
|
MatrixClientPeg.get().leave(targetRoomId).then(
|
||||||
function() {
|
function() {
|
||||||
dis.dispatch({action: 'view_next_room'});
|
dis.dispatch({action: 'view_next_room'});
|
||||||
})
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Kick a user from the room with an optional reason
|
// Kick a user from the room with an optional reason
|
||||||
kick: new Command("kick", "<userId> [<reason>]", function(room_id, args) {
|
kick: new Command("kick", "<userId> [<reason>]", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+?)( +(.*))?$/);
|
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().kick(room_id, matches[1], matches[3])
|
MatrixClientPeg.get().kick(roomId, matches[1], matches[3]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -206,12 +214,12 @@ var commands = {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Ban a user from the room with an optional reason
|
// Ban a user from the room with an optional reason
|
||||||
ban: new Command("ban", "<userId> [<reason>]", function(room_id, args) {
|
ban: new Command("ban", "<userId> [<reason>]", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+?)( +(.*))?$/);
|
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().ban(room_id, matches[1], matches[3])
|
MatrixClientPeg.get().ban(roomId, matches[1], matches[3]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,13 +227,13 @@ var commands = {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Unban a user from the room
|
// Unban a user from the room
|
||||||
unban: new Command("unban", "<userId>", function(room_id, args) {
|
unban: new Command("unban", "<userId>", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+)$/);
|
const matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
// Reset the user membership to "leave" to unban him
|
// Reset the user membership to "leave" to unban him
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().unban(room_id, matches[1])
|
MatrixClientPeg.get().unban(roomId, matches[1]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,27 +241,27 @@ var commands = {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Define the power level of a user
|
// Define the power level of a user
|
||||||
op: new Command("op", "<userId> [<power level>]", function(room_id, args) {
|
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+?)( +(\d+))?$/);
|
const matches = args.match(/^(\S+?)( +(\d+))?$/);
|
||||||
var powerLevel = 50; // default power level for op
|
let powerLevel = 50; // default power level for op
|
||||||
if (matches) {
|
if (matches) {
|
||||||
var user_id = matches[1];
|
const userId = matches[1];
|
||||||
if (matches.length === 4 && undefined !== matches[3]) {
|
if (matches.length === 4 && undefined !== matches[3]) {
|
||||||
powerLevel = parseInt(matches[3]);
|
powerLevel = parseInt(matches[3]);
|
||||||
}
|
}
|
||||||
if (powerLevel !== NaN) {
|
if (!isNaN(powerLevel)) {
|
||||||
var room = MatrixClientPeg.get().getRoom(room_id);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return reject("Bad room ID: " + room_id);
|
return reject("Bad room ID: " + roomId);
|
||||||
}
|
}
|
||||||
var powerLevelEvent = room.currentState.getStateEvents(
|
const powerLevelEvent = room.currentState.getStateEvents(
|
||||||
"m.room.power_levels", ""
|
"m.room.power_levels", "",
|
||||||
);
|
);
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().setPowerLevel(
|
MatrixClientPeg.get().setPowerLevel(
|
||||||
room_id, user_id, powerLevel, powerLevelEvent
|
roomId, userId, powerLevel, powerLevelEvent,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -262,32 +270,96 @@ var commands = {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Reset the power level of a user
|
// Reset the power level of a user
|
||||||
deop: new Command("deop", "<userId>", function(room_id, args) {
|
deop: new Command("deop", "<userId>", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+)$/);
|
const matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
var room = MatrixClientPeg.get().getRoom(room_id);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return reject("Bad room ID: " + room_id);
|
return reject("Bad room ID: " + roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var powerLevelEvent = room.currentState.getStateEvents(
|
const powerLevelEvent = room.currentState.getStateEvents(
|
||||||
"m.room.power_levels", ""
|
"m.room.power_levels", "",
|
||||||
);
|
);
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().setPowerLevel(
|
MatrixClientPeg.get().setPowerLevel(
|
||||||
room_id, args, undefined, powerLevelEvent
|
roomId, args, undefined, powerLevelEvent,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
})
|
}),
|
||||||
|
|
||||||
|
// Verify a user, device, and pubkey tuple
|
||||||
|
verify: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, args) {
|
||||||
|
if (args) {
|
||||||
|
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
const userId = matches[1];
|
||||||
|
const deviceId = matches[2];
|
||||||
|
const fingerprint = matches[3];
|
||||||
|
|
||||||
|
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
|
// helpful aliases
|
||||||
var aliases = {
|
const aliases = {
|
||||||
j: "join"
|
j: "join",
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -304,13 +376,13 @@ module.exports = {
|
||||||
// IRC-style commands
|
// IRC-style commands
|
||||||
input = input.replace(/\s+$/, "");
|
input = input.replace(/\s+$/, "");
|
||||||
if (input[0] === "/" && input[1] !== "/") {
|
if (input[0] === "/" && input[1] !== "/") {
|
||||||
var bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
|
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
|
||||||
var cmd, args;
|
let cmd;
|
||||||
|
let args;
|
||||||
if (bits) {
|
if (bits) {
|
||||||
cmd = bits[1].substring(1).toLowerCase();
|
cmd = bits[1].substring(1).toLowerCase();
|
||||||
args = bits[3];
|
args = bits[3];
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
cmd = input;
|
cmd = input;
|
||||||
}
|
}
|
||||||
if (cmd === "me") return null;
|
if (cmd === "me") return null;
|
||||||
|
@ -319,9 +391,8 @@ module.exports = {
|
||||||
}
|
}
|
||||||
if (commands[cmd]) {
|
if (commands[cmd]) {
|
||||||
return commands[cmd].run(roomId, args);
|
return commands[cmd].run(roomId, args);
|
||||||
}
|
} else {
|
||||||
else {
|
return reject(_t("Unrecognised command:") + ' ' + input);
|
||||||
return reject("Unrecognised command: " + input);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null; // not a command
|
return null; // not a command
|
||||||
|
@ -329,12 +400,12 @@ module.exports = {
|
||||||
|
|
||||||
getCommandList: function() {
|
getCommandList: function() {
|
||||||
// Return all the commands plus /me and /markdown which aren't handled like normal commands
|
// Return all the commands plus /me and /markdown which aren't handled like normal commands
|
||||||
var cmds = Object.keys(commands).sort().map(function(cmdKey) {
|
const cmds = Object.keys(commands).sort().map(function(cmdKey) {
|
||||||
return commands[cmdKey];
|
return commands[cmdKey];
|
||||||
});
|
});
|
||||||
cmds.push(new Command("me", "<action>", function() {}));
|
cmds.push(new Command("me", "<action>", function() {}));
|
||||||
cmds.push(new Command("markdown", "<on|off>", function() {}));
|
cmds.push(new Command("markdown", "<on|off>", function() {}));
|
||||||
|
|
||||||
return cmds;
|
return cmds;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import MatrixClientPeg from "./MatrixClientPeg";
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
import CallHandler from "./CallHandler";
|
||||||
var CallHandler = require("./CallHandler");
|
import { _t } from './languageHandler';
|
||||||
|
import * as Roles from './Roles';
|
||||||
|
|
||||||
function textForMemberEvent(ev) {
|
function textForMemberEvent(ev) {
|
||||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
// 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 targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||||
var ConferenceHandler = CallHandler.getConferenceHandler();
|
var ConferenceHandler = CallHandler.getConferenceHandler();
|
||||||
var reason = ev.getContent().reason ? (
|
var reason = ev.getContent().reason ? (
|
||||||
" Reason: " + ev.getContent().reason
|
_t('Reason') + ': ' + ev.getContent().reason
|
||||||
) : "";
|
) : "";
|
||||||
switch (ev.getContent().membership) {
|
switch (ev.getContent().membership) {
|
||||||
case 'invite':
|
case 'invite':
|
||||||
var threePidContent = ev.getContent().third_party_invite;
|
var threePidContent = ev.getContent().third_party_invite;
|
||||||
if (threePidContent) {
|
if (threePidContent) {
|
||||||
if (threePidContent.display_name) {
|
if (threePidContent.display_name) {
|
||||||
return targetName + " accepted the invitation for " +
|
return _t('%(targetName)s accepted the invitation for %(displayName)s.', {targetName: targetName, displayName: threePidContent.display_name});
|
||||||
threePidContent.display_name + ".";
|
|
||||||
} else {
|
} else {
|
||||||
return targetName + " accepted an invitation.";
|
return _t('%(targetName)s accepted an invitation.', {targetName: targetName});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
||||||
return senderName + " requested a VoIP conference";
|
return _t('%(senderName)s requested a VoIP conference.', {senderName: senderName});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return senderName + " invited " + targetName + ".";
|
return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'ban':
|
case 'ban':
|
||||||
return senderName + " banned " + targetName + "." + reason;
|
return _t(
|
||||||
|
'%(senderName)s banned %(targetName)s.',
|
||||||
|
{senderName: senderName, targetName: targetName}
|
||||||
|
) + ' ' + reason;
|
||||||
case 'join':
|
case 'join':
|
||||||
if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') {
|
if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') {
|
||||||
if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) {
|
if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) {
|
||||||
return ev.getSender() + " changed their display name from " +
|
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname, displayName: ev.getContent().displayname});
|
||||||
ev.getPrevContent().displayname + " to " +
|
|
||||||
ev.getContent().displayname;
|
|
||||||
} else if (!ev.getPrevContent().displayname && ev.getContent().displayname) {
|
} else if (!ev.getPrevContent().displayname && ev.getContent().displayname) {
|
||||||
return ev.getSender() + " set their display name to " + ev.getContent().displayname;
|
return _t('%(senderName)s set their display name to %(displayName)s.', {senderName: ev.getSender(), displayName: ev.getContent().displayname});
|
||||||
} else if (ev.getPrevContent().displayname && !ev.getContent().displayname) {
|
} else if (ev.getPrevContent().displayname && !ev.getContent().displayname) {
|
||||||
return ev.getSender() + " removed their display name (" + ev.getPrevContent().displayname + ")";
|
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname});
|
||||||
} else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) {
|
} else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) {
|
||||||
return senderName + " removed their profile picture";
|
return _t('%(senderName)s removed their profile picture.', {senderName: senderName});
|
||||||
} else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) {
|
} else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) {
|
||||||
return senderName + " changed their profile picture";
|
return _t('%(senderName)s changed their profile picture.', {senderName: senderName});
|
||||||
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
|
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
|
||||||
return senderName + " set a profile picture";
|
return _t('%(senderName)s set a profile picture.', {senderName: senderName});
|
||||||
} else {
|
} else {
|
||||||
// hacky hack for https://github.com/vector-im/vector-web/issues/2020
|
// suppress null rejoins
|
||||||
return senderName + " rejoined the room.";
|
return '';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
||||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
||||||
return "VoIP conference started";
|
return _t('VoIP conference started.');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return targetName + " joined the room.";
|
return _t('%(targetName)s joined the room.', {targetName: targetName});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'leave':
|
case 'leave':
|
||||||
if (ev.getSender() === ev.getStateKey()) {
|
if (ev.getSender() === ev.getStateKey()) {
|
||||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
||||||
return "VoIP conference finished";
|
return _t('VoIP conference finished.');
|
||||||
}
|
}
|
||||||
else if (ev.getPrevContent().membership === "invite") {
|
else if (ev.getPrevContent().membership === "invite") {
|
||||||
return targetName + " rejected the invitation.";
|
return _t('%(targetName)s rejected the invitation.', {targetName: targetName});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return targetName + " left the room.";
|
return _t('%(targetName)s left the room.', {targetName: targetName});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (ev.getPrevContent().membership === "ban") {
|
else if (ev.getPrevContent().membership === "ban") {
|
||||||
return senderName + " unbanned " + targetName + ".";
|
return _t('%(senderName)s unbanned %(targetName)s.', {senderName: senderName, targetName: targetName});
|
||||||
}
|
}
|
||||||
else if (ev.getPrevContent().membership === "join") {
|
else if (ev.getPrevContent().membership === "join") {
|
||||||
return senderName + " kicked " + targetName + "." + reason;
|
return _t(
|
||||||
|
'%(senderName)s kicked %(targetName)s.',
|
||||||
|
{senderName: senderName, targetName: targetName}
|
||||||
|
) + ' ' + reason;
|
||||||
}
|
}
|
||||||
else if (ev.getPrevContent().membership === "invite") {
|
else if (ev.getPrevContent().membership === "invite") {
|
||||||
return senderName + " withdrew " + targetName + "'s invitation." + reason;
|
return _t(
|
||||||
|
'%(senderName)s withdrew %(targetName)s\'s invitation.',
|
||||||
|
{senderName: senderName, targetName: targetName}
|
||||||
|
) + ' ' + reason;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return targetName + " left the room.";
|
return _t('%(targetName)s left the room.', {targetName: targetName});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForTopicEvent(ev) {
|
function textForTopicEvent(ev) {
|
||||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
|
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {senderDisplayName: senderDisplayName, topic: ev.getContent().topic});
|
||||||
return senderDisplayName + ' changed the topic to "' + ev.getContent().topic + '"';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForRoomNameEvent(ev) {
|
function textForRoomNameEvent(ev) {
|
||||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
|
|
||||||
return senderDisplayName + ' changed the room name to "' + ev.getContent().name + '"';
|
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
|
||||||
|
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName: senderDisplayName});
|
||||||
|
}
|
||||||
|
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {senderDisplayName: senderDisplayName, roomName: ev.getContent().name});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForMessageEvent(ev) {
|
function textForMessageEvent(ev) {
|
||||||
|
@ -120,66 +129,123 @@ function textForMessageEvent(ev) {
|
||||||
if (ev.getContent().msgtype === "m.emote") {
|
if (ev.getContent().msgtype === "m.emote") {
|
||||||
message = "* " + senderDisplayName + " " + message;
|
message = "* " + senderDisplayName + " " + message;
|
||||||
} else if (ev.getContent().msgtype === "m.image") {
|
} else if (ev.getContent().msgtype === "m.image") {
|
||||||
message = senderDisplayName + " sent an image.";
|
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName: senderDisplayName});
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallAnswerEvent(event) {
|
function textForCallAnswerEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : "Someone";
|
var senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)";
|
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
||||||
return senderName + " answered the call." + supported;
|
return _t('%(senderName)s answered the call.', {senderName: senderName}) + ' ' + supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallHangupEvent(event) {
|
function textForCallHangupEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : "Someone";
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)";
|
const eventContent = event.getContent();
|
||||||
return senderName + " ended the call." + supported;
|
let reason = "";
|
||||||
|
if(!MatrixClientPeg.get().supportsVoip()) {
|
||||||
|
reason = _t('(not supported by this browser)');
|
||||||
|
} else if(eventContent.reason) {
|
||||||
|
if (eventContent.reason === "ice_failed") {
|
||||||
|
reason = _t('(could not connect media)');
|
||||||
|
} else if (eventContent.reason === "invite_timeout") {
|
||||||
|
reason = _t('(no answer)');
|
||||||
|
} else {
|
||||||
|
reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallInviteEvent(event) {
|
function textForCallInviteEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : "Someone";
|
var senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
// FIXME: Find a better way to determine this from the event?
|
// FIXME: Find a better way to determine this from the event?
|
||||||
var type = "voice";
|
var type = "voice";
|
||||||
if (event.getContent().offer && event.getContent().offer.sdp &&
|
if (event.getContent().offer && event.getContent().offer.sdp &&
|
||||||
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
|
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
|
||||||
type = "video";
|
type = "video";
|
||||||
}
|
}
|
||||||
var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)";
|
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
||||||
return senderName + " placed a " + type + " call." + supported;
|
return _t('%(senderName)s placed a %(callType)s call.', {senderName: senderName, callType: type}) + ' ' + supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForThreePidInviteEvent(event) {
|
function textForThreePidInviteEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
var senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
return senderName + " sent an invitation to " + event.getContent().display_name +
|
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {senderName: senderName, targetDisplayName: event.getContent().display_name});
|
||||||
" to join the room.";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForHistoryVisibilityEvent(event) {
|
function textForHistoryVisibilityEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
var senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
var vis = event.getContent().history_visibility;
|
var vis = event.getContent().history_visibility;
|
||||||
var text = senderName + " made future room history visible to ";
|
// XXX: This i18n just isn't going to work for languages with different sentence structure.
|
||||||
|
var text = _t('%(senderName)s made future room history visible to', {senderName: senderName}) + ' ';
|
||||||
if (vis === "invited") {
|
if (vis === "invited") {
|
||||||
text += "all room members, from the point they are invited.";
|
text += _t('all room members, from the point they are invited') + '.';
|
||||||
}
|
}
|
||||||
else if (vis === "joined") {
|
else if (vis === "joined") {
|
||||||
text += "all room members, from the point they joined.";
|
text += _t('all room members, from the point they joined') + '.';
|
||||||
}
|
}
|
||||||
else if (vis === "shared") {
|
else if (vis === "shared") {
|
||||||
text += "all room members.";
|
text += _t('all room members') + '.';
|
||||||
}
|
}
|
||||||
else if (vis === "world_readable") {
|
else if (vis === "world_readable") {
|
||||||
text += "anyone.";
|
text += _t('anyone') + '.';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
text += " unknown (" + vis + ")";
|
text += ' ' + _t('unknown') + ' (' + vis + ').';
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForEncryptionEvent(event) {
|
function textForEncryptionEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
var senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
return senderName + " turned on end-to-end encryption (algorithm " + event.getContent().algorithm + ")";
|
return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {senderName: senderName, algorithm: event.getContent().algorithm});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently will only display a change if a user's power level is changed
|
||||||
|
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 = {
|
var handlers = {
|
||||||
|
@ -193,6 +259,7 @@ var handlers = {
|
||||||
'm.room.third_party_invite': textForThreePidInviteEvent,
|
'm.room.third_party_invite': textForThreePidInviteEvent,
|
||||||
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
||||||
'm.room.encryption': textForEncryptionEvent,
|
'm.room.encryption': textForEncryptionEvent,
|
||||||
|
'm.room.power_levels': textForPowerEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -22,7 +22,7 @@ let isDialogOpen = false;
|
||||||
|
|
||||||
const onAction = function(payload) {
|
const onAction = function(payload) {
|
||||||
if (payload.action === 'unknown_device_error' && !isDialogOpen) {
|
if (payload.action === 'unknown_device_error' && !isDialogOpen) {
|
||||||
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
|
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
|
||||||
isDialogOpen = true;
|
isDialogOpen = true;
|
||||||
Modal.createDialog(UnknownDeviceDialog, {
|
Modal.createDialog(UnknownDeviceDialog, {
|
||||||
devices: payload.err.devices,
|
devices: payload.err.devices,
|
||||||
|
@ -33,9 +33,9 @@ const onAction = function(payload) {
|
||||||
// https://github.com/vector-im/riot-web/issues/3148
|
// https://github.com/vector-im/riot-web/issues/3148
|
||||||
console.log('UnknownDeviceDialog closed with '+r);
|
console.log('UnknownDeviceDialog closed with '+r);
|
||||||
},
|
},
|
||||||
}, "mx_Dialog_unknownDevice");
|
}, 'mx_Dialog_unknownDevice');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let ref = null;
|
let ref = null;
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
|
import UserSettingsStore from './UserSettingsStore';
|
||||||
|
import shouldHideEvent from './shouldHideEvent';
|
||||||
var sdk = require('./index');
|
var sdk = require('./index');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -25,7 +27,9 @@ module.exports = {
|
||||||
eventTriggersUnreadCount: function(ev) {
|
eventTriggersUnreadCount: function(ev) {
|
||||||
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
||||||
return false;
|
return false;
|
||||||
} else if (ev.getType() == "m.room.member") {
|
} else if (ev.getType() == 'm.room.member') {
|
||||||
|
return false;
|
||||||
|
} else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') {
|
||||||
return false;
|
return false;
|
||||||
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
|
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
|
||||||
return false;
|
return false;
|
||||||
|
@ -35,13 +39,33 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
doesRoomHaveUnreadMessages: function(room) {
|
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
|
// 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
|
// 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
|
// 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,
|
// we have and the read receipt. We could fetch more history to try & find out,
|
||||||
// but currently we just guess.
|
// but currently we just guess.
|
||||||
|
|
||||||
|
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||||
// Loop through messages, starting with the most recent...
|
// Loop through messages, starting with the most recent...
|
||||||
for (var i = room.timeline.length - 1; i >= 0; --i) {
|
for (var i = room.timeline.length - 1; i >= 0; --i) {
|
||||||
var ev = room.timeline[i];
|
var ev = room.timeline[i];
|
||||||
|
@ -51,7 +75,7 @@ module.exports = {
|
||||||
// that counts and we can stop looking because the user's read
|
// that counts and we can stop looking because the user's read
|
||||||
// this and everything before.
|
// this and everything before.
|
||||||
return false;
|
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
|
// We've found a message that counts before we hit
|
||||||
// the read marker, so this room is definitely unread.
|
// the read marker, so this room is definitely unread.
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var dis = require("./dispatcher");
|
import dis from './dispatcher';
|
||||||
|
|
||||||
var MIN_DISPATCH_INTERVAL_MS = 500;
|
const MIN_DISPATCH_INTERVAL_MS = 500;
|
||||||
var CURRENTLY_ACTIVE_THRESHOLD_MS = 2000;
|
const CURRENTLY_ACTIVE_THRESHOLD_MS = 2000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class watches for user activity (moving the mouse or pressing a key)
|
* This class watches for user activity (moving the mouse or pressing a key)
|
||||||
|
@ -32,7 +32,7 @@ class UserActivity {
|
||||||
start() {
|
start() {
|
||||||
document.onmousedown = this._onUserActivity.bind(this);
|
document.onmousedown = this._onUserActivity.bind(this);
|
||||||
document.onmousemove = 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
|
// can't use document.scroll here because that's only the document
|
||||||
// itself being scrolled. Need to use addEventListener's useCapture.
|
// itself being scrolled. Need to use addEventListener's useCapture.
|
||||||
// also this needs to be the wheel event, not scroll, as scroll is
|
// also this needs to be the wheel event, not scroll, as scroll is
|
||||||
|
@ -50,7 +50,7 @@ class UserActivity {
|
||||||
stop() {
|
stop() {
|
||||||
document.onmousedown = undefined;
|
document.onmousedown = undefined;
|
||||||
document.onmousemove = undefined;
|
document.onmousemove = undefined;
|
||||||
document.onkeypress = undefined;
|
document.onkeydown = undefined;
|
||||||
window.removeEventListener('wheel', this._onUserActivity.bind(this),
|
window.removeEventListener('wheel', this._onUserActivity.bind(this),
|
||||||
{ passive: true, capture: true });
|
{ passive: true, capture: true });
|
||||||
}
|
}
|
||||||
|
@ -58,16 +58,15 @@ class UserActivity {
|
||||||
/**
|
/**
|
||||||
* Return true if there has been user activity very recently
|
* Return true if there has been user activity very recently
|
||||||
* (ie. within a few seconds)
|
* (ie. within a few seconds)
|
||||||
|
* @returns {boolean} true if user is currently/very recently active
|
||||||
*/
|
*/
|
||||||
userCurrentlyActive() {
|
userCurrentlyActive() {
|
||||||
return this.lastActivityAtTs > new Date().getTime() - CURRENTLY_ACTIVE_THRESHOLD_MS;
|
return this.lastActivityAtTs > new Date().getTime() - CURRENTLY_ACTIVE_THRESHOLD_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onUserActivity(event) {
|
_onUserActivity(event) {
|
||||||
if (event.screenX && event.type == "mousemove") {
|
if (event.screenX && event.type === "mousemove") {
|
||||||
if (event.screenX === this.lastScreenX &&
|
if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) {
|
||||||
event.screenY === this.lastScreenY)
|
|
||||||
{
|
|
||||||
// mouse hasn't actually moved
|
// mouse hasn't actually moved
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -79,28 +78,24 @@ class UserActivity {
|
||||||
if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL_MS) {
|
if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL_MS) {
|
||||||
this.lastDispatchAtTs = this.lastActivityAtTs;
|
this.lastDispatchAtTs = this.lastActivityAtTs;
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'user_activity'
|
action: 'user_activity',
|
||||||
});
|
});
|
||||||
if (!this.activityEndTimer) {
|
if (!this.activityEndTimer) {
|
||||||
this.activityEndTimer = setTimeout(
|
this.activityEndTimer = setTimeout(this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS);
|
||||||
this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onActivityEndTimer() {
|
_onActivityEndTimer() {
|
||||||
var now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
var targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS;
|
const targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS;
|
||||||
if (now >= targetTime) {
|
if (now >= targetTime) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'user_activity_end'
|
action: 'user_activity_end',
|
||||||
});
|
});
|
||||||
this.activityEndTimer = undefined;
|
this.activityEndTimer = undefined;
|
||||||
} else {
|
} else {
|
||||||
this.activityEndTimer = setTimeout(
|
this.activityEndTimer = setTimeout(this._onActivityEndTimer.bind(this), targetTime - now);
|
||||||
this._onActivityEndTimer.bind(this), targetTime - now
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,26 +14,31 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
import Promise from 'bluebird';
|
||||||
var q = require("q");
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
import Notifier from './Notifier';
|
||||||
var Notifier = require("./Notifier");
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
|
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = {
|
export default {
|
||||||
LABS_FEATURES: [
|
LABS_FEATURES: [
|
||||||
{
|
{
|
||||||
name: 'New Composer & Autocomplete',
|
name: "-",
|
||||||
id: 'rich_text_editor',
|
id: 'matrix_apps',
|
||||||
default: false,
|
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() {
|
loadProfileInfo: function() {
|
||||||
var cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
return cli.getProfileInfo(cli.credentials.userId);
|
return cli.getProfileInfo(cli.credentials.userId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -43,8 +48,8 @@ module.exports = {
|
||||||
|
|
||||||
loadThreePids: function() {
|
loadThreePids: function() {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
return q({
|
return Promise.resolve({
|
||||||
threepids: []
|
threepids: [],
|
||||||
}); // guests can't poke 3pid endpoint
|
}); // guests can't poke 3pid endpoint
|
||||||
}
|
}
|
||||||
return MatrixClientPeg.get().getThreePids();
|
return MatrixClientPeg.get().getThreePids();
|
||||||
|
@ -73,19 +78,19 @@ module.exports = {
|
||||||
Notifier.setAudioEnabled(enable);
|
Notifier.setAudioEnabled(enable);
|
||||||
},
|
},
|
||||||
|
|
||||||
changePassword: function(old_password, new_password) {
|
changePassword: function(oldPassword, newPassword) {
|
||||||
var cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
var authDict = {
|
const authDict = {
|
||||||
type: 'm.login.password',
|
type: 'm.login.password',
|
||||||
user: cli.credentials.userId,
|
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
|
* Returns the email pusher (pusher of type 'email') for a given
|
||||||
* email address. Email pushers all have the same app ID, so since
|
* email address. Email pushers all have the same app ID, so since
|
||||||
* pushers are unique over (app ID, pushkey), there will be at most
|
* pushers are unique over (app ID, pushkey), there will be at most
|
||||||
|
@ -95,8 +100,8 @@ module.exports = {
|
||||||
if (pushers === undefined) {
|
if (pushers === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
for (var i = 0; i < pushers.length; ++i) {
|
for (let i = 0; i < pushers.length; ++i) {
|
||||||
if (pushers[i].kind == 'email' && pushers[i].pushkey == address) {
|
if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
|
||||||
return pushers[i];
|
return pushers[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,7 +115,7 @@ module.exports = {
|
||||||
addEmailPusher: function(address, data) {
|
addEmailPusher: function(address, data) {
|
||||||
return MatrixClientPeg.get().setPusher({
|
return MatrixClientPeg.get().setPusher({
|
||||||
kind: 'email',
|
kind: 'email',
|
||||||
app_id: "m.email",
|
app_id: 'm.email',
|
||||||
pushkey: address,
|
pushkey: address,
|
||||||
app_display_name: 'Email Notifications',
|
app_display_name: 'Email Notifications',
|
||||||
device_display_name: address,
|
device_display_name: address,
|
||||||
|
@ -121,46 +126,46 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getUrlPreviewsDisabled: function() {
|
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);
|
return (event && event.getContent().disable);
|
||||||
},
|
},
|
||||||
|
|
||||||
setUrlPreviewsDisabled: function(disabled) {
|
setUrlPreviewsDisabled: function(disabled) {
|
||||||
// FIXME: handle errors
|
// FIXME: handle errors
|
||||||
return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", {
|
return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', {
|
||||||
disable: disabled
|
disable: disabled,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getSyncedSettings: function() {
|
getSyncedSettings: function() {
|
||||||
var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings");
|
const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings');
|
||||||
return event ? event.getContent() : {};
|
return event ? event.getContent() : {};
|
||||||
},
|
},
|
||||||
|
|
||||||
getSyncedSetting: function(type, defaultValue = null) {
|
getSyncedSetting: function(type, defaultValue = null) {
|
||||||
var settings = this.getSyncedSettings();
|
const settings = this.getSyncedSettings();
|
||||||
return settings.hasOwnProperty(type) ? settings[type] : null;
|
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
|
||||||
},
|
},
|
||||||
|
|
||||||
setSyncedSetting: function(type, value) {
|
setSyncedSetting: function(type, value) {
|
||||||
var settings = this.getSyncedSettings();
|
const settings = this.getSyncedSettings();
|
||||||
settings[type] = value;
|
settings[type] = value;
|
||||||
// FIXME: handle errors
|
// FIXME: handle errors
|
||||||
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
|
return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings);
|
||||||
},
|
},
|
||||||
|
|
||||||
getLocalSettings: function() {
|
getLocalSettings: function() {
|
||||||
var localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
|
const localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
|
||||||
return JSON.parse(localSettingsString);
|
return JSON.parse(localSettingsString);
|
||||||
},
|
},
|
||||||
|
|
||||||
getLocalSetting: function(type, defaultValue = null) {
|
getLocalSetting: function(type, defaultValue = null) {
|
||||||
var settings = this.getLocalSettings();
|
const settings = this.getLocalSettings();
|
||||||
return settings.hasOwnProperty(type) ? settings[type] : null;
|
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
|
||||||
},
|
},
|
||||||
|
|
||||||
setLocalSetting: function(type, value) {
|
setLocalSetting: function(type, value) {
|
||||||
var settings = this.getLocalSettings();
|
const settings = this.getLocalSettings();
|
||||||
settings[type] = value;
|
settings[type] = value;
|
||||||
// FIXME: handle errors
|
// FIXME: handle errors
|
||||||
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
|
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
|
||||||
|
@ -171,8 +176,8 @@ module.exports = {
|
||||||
if (MatrixClientPeg.get().isGuest()) return false;
|
if (MatrixClientPeg.get().isGuest()) return false;
|
||||||
|
|
||||||
if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) {
|
if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) {
|
||||||
for (var i = 0; i < this.LABS_FEATURES.length; i++) {
|
for (let i = 0; i < this.LABS_FEATURES.length; i++) {
|
||||||
var f = this.LABS_FEATURES[i];
|
const f = this.LABS_FEATURES[i];
|
||||||
if (f.id === feature) {
|
if (f.id === feature) {
|
||||||
return f.default;
|
return f.default;
|
||||||
}
|
}
|
||||||
|
@ -183,5 +188,5 @@ module.exports = {
|
||||||
|
|
||||||
setFeatureEnabled: function(feature: string, enabled: boolean) {
|
setFeatureEnabled: function(feature: string, enabled: boolean) {
|
||||||
localStorage.setItem(`mx_labs_feature_${feature}`, enabled);
|
localStorage.setItem(`mx_labs_feature_${feature}`, enabled);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -64,7 +64,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
//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;
|
oldNode.style.visibility = c.props.style.visibility;
|
||||||
}
|
}
|
||||||
self.children[c.key] = old;
|
self.children[c.key] = old;
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
usersTypingApartFromMe: function(room) {
|
usersTypingApartFromMe: function(room) {
|
||||||
|
@ -56,18 +57,18 @@ module.exports = {
|
||||||
if (whoIsTyping.length == 0) {
|
if (whoIsTyping.length == 0) {
|
||||||
return '';
|
return '';
|
||||||
} else if (whoIsTyping.length == 1) {
|
} else if (whoIsTyping.length == 1) {
|
||||||
return whoIsTyping[0].name + ' is typing';
|
return _t('%(displayName)s is typing', {displayName: whoIsTyping[0].name});
|
||||||
}
|
}
|
||||||
const names = whoIsTyping.map(function(m) {
|
const names = whoIsTyping.map(function(m) {
|
||||||
return m.name;
|
return m.name;
|
||||||
});
|
});
|
||||||
if (othersCount) {
|
if (othersCount==1) {
|
||||||
const other = ' other' + (othersCount > 1 ? 's' : '');
|
return _t('%(names)s and one other are typing', {names: names.slice(0, limit - 1).join(', ')});
|
||||||
return names.slice(0, limit - 1).join(', ') + ' and ' +
|
} else if (othersCount>1) {
|
||||||
othersCount + other + ' are typing';
|
return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount});
|
||||||
} else {
|
} else {
|
||||||
const lastPerson = names.pop();
|
const lastPerson = names.pop();
|
||||||
return names.join(', ') + ' and ' + lastPerson + ' are typing';
|
return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
58
src/WidgetUtils.js
Normal file
58
src/WidgetUtils.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
var React = require("react");
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
|
||||||
|
@ -27,23 +28,31 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return { device: this.refreshDevice() };
|
return { device: null };
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
var client = MatrixClientPeg.get();
|
var client = MatrixClientPeg.get();
|
||||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
|
||||||
|
|
||||||
// no need to redownload keys if we already have the device
|
// first try to load the device from our store.
|
||||||
if (this.state.device) {
|
//
|
||||||
return;
|
this.refreshDevice().then((dev) => {
|
||||||
|
if (dev) {
|
||||||
|
return dev;
|
||||||
}
|
}
|
||||||
client.downloadKeys([this.props.event.getSender()], true).done(()=>{
|
|
||||||
|
// 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) {
|
if (this._unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ device: this.refreshDevice() });
|
|
||||||
|
this.setState({ device: dev });
|
||||||
|
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||||
}, (err)=>{
|
}, (err)=>{
|
||||||
console.log("Error downloading devices", err);
|
console.log("Error downloading devices", err);
|
||||||
});
|
});
|
||||||
|
@ -58,12 +67,16 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
refreshDevice: function() {
|
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) {
|
onDeviceVerificationChanged: function(userId, device) {
|
||||||
if (userId == this.props.event.getSender()) {
|
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() {
|
_renderDeviceInfo: function() {
|
||||||
var device = this.state.device;
|
var device = this.state.device;
|
||||||
if (!device) {
|
if (!device) {
|
||||||
return (<i>unknown device</i>);
|
return (<i>{ _t('unknown device') }</i>);
|
||||||
}
|
}
|
||||||
|
|
||||||
var verificationStatus = (<b>NOT verified</b>);
|
var verificationStatus = (<b>{ _t('NOT verified') }</b>);
|
||||||
if (device.isBlocked()) {
|
if (device.isBlocked()) {
|
||||||
verificationStatus = (<b>Blacklisted</b>);
|
verificationStatus = (<b>{ _t('Blacklisted') }</b>);
|
||||||
} else if (device.isVerified()) {
|
} else if (device.isVerified()) {
|
||||||
verificationStatus = "verified";
|
verificationStatus = _t('verified');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Name</td>
|
<td>{ _t('Name') }</td>
|
||||||
<td>{ device.getDisplayName() }</td>
|
<td>{ device.getDisplayName() }</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Device ID</td>
|
<td>{ _t('Device ID') }</td>
|
||||||
<td><code>{ device.deviceId }</code></td>
|
<td><code>{ device.deviceId }</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Verification</td>
|
<td>{ _t('Verification') }</td>
|
||||||
<td>{ verificationStatus }</td>
|
<td>{ verificationStatus }</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Ed25519 fingerprint</td>
|
<td>{ _t('Ed25519 fingerprint') }</td>
|
||||||
<td><code>{device.getFingerprint()}</code></td>
|
<td><code>{device.getFingerprint()}</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -119,32 +132,32 @@ module.exports = React.createClass({
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>User ID</td>
|
<td>{ _t('User ID') }</td>
|
||||||
<td>{ event.getSender() }</td>
|
<td>{ event.getSender() }</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Curve25519 identity key</td>
|
<td>{ _t('Curve25519 identity key') }</td>
|
||||||
<td><code>{ event.getSenderKey() || <i>none</i> }</code></td>
|
<td><code>{ event.getSenderKey() || <i>{ _t('none') }</i> }</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Claimed Ed25519 fingerprint key</td>
|
<td>{ _t('Claimed Ed25519 fingerprint key') }</td>
|
||||||
<td><code>{ event.getKeysClaimed().ed25519 || <i>none</i> }</code></td>
|
<td><code>{ event.getKeysClaimed().ed25519 || <i>{ _t('none') }</i> }</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Algorithm</td>
|
<td>{ _t('Algorithm') }</td>
|
||||||
<td>{ event.getWireContent().algorithm || <i>unencrypted</i> }</td>
|
<td>{ event.getWireContent().algorithm || <i>{ _t('unencrypted') }</i> }</td>
|
||||||
</tr>
|
</tr>
|
||||||
{
|
{
|
||||||
event.getContent().msgtype === 'm.bad.encrypted' ? (
|
event.getContent().msgtype === 'm.bad.encrypted' ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td>Decryption error</td>
|
<td>{ _t('Decryption error') }</td>
|
||||||
<td>{ event.getContent().body }</td>
|
<td>{ event.getContent().body }</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Session ID</td>
|
<td>{ _t('Session ID') }</td>
|
||||||
<td><code>{ event.getWireContent().session_id || <i>none</i> }</code></td>
|
<td><code>{ event.getWireContent().session_id || <i>{ _t('none') }</i> }</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -166,18 +179,18 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_EncryptedEventDialog" onKeyDown={ this.onKeyDown }>
|
<div className="mx_EncryptedEventDialog" onKeyDown={ this.onKeyDown }>
|
||||||
<div className="mx_Dialog_title">
|
<div className="mx_Dialog_title">
|
||||||
End-to-end encryption information
|
{ _t('End-to-end encryption information') }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<h4>Event information</h4>
|
<h4>{ _t('Event information') }</h4>
|
||||||
{this._renderEventInfo()}
|
{this._renderEventInfo()}
|
||||||
|
|
||||||
<h4>Sender device information</h4>
|
<h4>{ _t('Sender device information') }</h4>
|
||||||
{this._renderDeviceInfo()}
|
{this._renderDeviceInfo()}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className="mx_Dialog_primary" onClick={ this.props.onFinished } autoFocus={ true }>
|
<button className="mx_Dialog_primary" onClick={ this.props.onFinished } autoFocus={ true }>
|
||||||
OK
|
{ _t('OK') }
|
||||||
</button>
|
</button>
|
||||||
{buttons}
|
{buttons}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
import * as Matrix from 'matrix-js-sdk';
|
import * as Matrix from 'matrix-js-sdk';
|
||||||
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
||||||
|
@ -52,11 +53,11 @@ export default React.createClass({
|
||||||
|
|
||||||
const passphrase = this.refs.passphrase1.value;
|
const passphrase = this.refs.passphrase1.value;
|
||||||
if (passphrase !== this.refs.passphrase2.value) {
|
if (passphrase !== this.refs.passphrase2.value) {
|
||||||
this.setState({errStr: 'Passphrases must match'});
|
this.setState({errStr: _t('Passphrases must match')});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!passphrase) {
|
if (!passphrase) {
|
||||||
this.setState({errStr: 'Passphrase must not be empty'});
|
this.setState({errStr: _t('Passphrase must not be empty')});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,11 +81,13 @@ export default React.createClass({
|
||||||
FileSaver.saveAs(blob, 'riot-keys.txt');
|
FileSaver.saveAs(blob, 'riot-keys.txt');
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
|
console.error("Error exporting e2e keys:", e);
|
||||||
if (this._unmounted) {
|
if (this._unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const msg = e.friendlyText || _t('Unknown error');
|
||||||
this.setState({
|
this.setState({
|
||||||
errStr: e.message,
|
errStr: msg,
|
||||||
phase: PHASE_EDIT,
|
phase: PHASE_EDIT,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -109,24 +112,28 @@ export default React.createClass({
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_exportE2eKeysDialog'
|
<BaseDialog className='mx_exportE2eKeysDialog'
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title="Export room keys"
|
title={_t("Export room keys")}
|
||||||
>
|
>
|
||||||
<form onSubmit={this._onPassphraseFormSubmit}>
|
<form onSubmit={this._onPassphraseFormSubmit}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<p>
|
<p>
|
||||||
This process allows you to export the keys for messages
|
{ _t(
|
||||||
you have received in encrypted rooms to a local file. You
|
'This process allows you to export the keys for messages ' +
|
||||||
will then be able to import the file into another Matrix
|
'you have received in encrypted rooms to a local file. You ' +
|
||||||
client in the future, so that client will also be able to
|
'will then be able to import the file into another Matrix ' +
|
||||||
decrypt these messages.
|
'client in the future, so that client will also be able to ' +
|
||||||
|
'decrypt these messages.',
|
||||||
|
) }
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The exported file will allow anyone who can read it to decrypt
|
{ _t(
|
||||||
any encrypted messages that you can see, so you should be
|
'The exported file will allow anyone who can read it to decrypt ' +
|
||||||
careful to keep it secure. To help with this, you should enter
|
'any encrypted messages that you can see, so you should be ' +
|
||||||
a passphrase below, which will be used to encrypt the exported
|
'careful to keep it secure. To help with this, you should enter ' +
|
||||||
data. It will only be possible to import the data by using the
|
'a passphrase below, which will be used to encrypt the exported ' +
|
||||||
same passphrase.
|
'data. It will only be possible to import the data by using the ' +
|
||||||
|
'same passphrase.',
|
||||||
|
) }
|
||||||
</p>
|
</p>
|
||||||
<div className='error'>
|
<div className='error'>
|
||||||
{this.state.errStr}
|
{this.state.errStr}
|
||||||
|
@ -135,7 +142,7 @@ export default React.createClass({
|
||||||
<div className='mx_E2eKeysDialog_inputRow'>
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
<label htmlFor='passphrase1'>
|
<label htmlFor='passphrase1'>
|
||||||
Enter passphrase
|
{_t("Enter passphrase")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
@ -148,7 +155,7 @@ export default React.createClass({
|
||||||
<div className='mx_E2eKeysDialog_inputRow'>
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
<label htmlFor='passphrase2'>
|
<label htmlFor='passphrase2'>
|
||||||
Confirm passphrase
|
{_t("Confirm passphrase")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
@ -161,11 +168,11 @@ export default React.createClass({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_Dialog_buttons'>
|
<div className='mx_Dialog_buttons'>
|
||||||
<input className='mx_Dialog_primary' type='submit' value='Export'
|
<input className='mx_Dialog_primary' type='submit' value={_t('Export')}
|
||||||
disabled={disableForm}
|
disabled={disableForm}
|
||||||
/>
|
/>
|
||||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
Cancel
|
{_t("Cancel")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import React from 'react';
|
||||||
import * as Matrix from 'matrix-js-sdk';
|
import * as Matrix from 'matrix-js-sdk';
|
||||||
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
function readFileAsArrayBuffer(file) {
|
function readFileAsArrayBuffer(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -88,11 +89,13 @@ export default React.createClass({
|
||||||
// TODO: it would probably be nice to give some feedback about what we've imported here.
|
// TODO: it would probably be nice to give some feedback about what we've imported here.
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
|
console.error("Error importing e2e keys:", e);
|
||||||
if (this._unmounted) {
|
if (this._unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const msg = e.friendlyText || _t('Unknown error');
|
||||||
this.setState({
|
this.setState({
|
||||||
errStr: e.message,
|
errStr: msg,
|
||||||
phase: PHASE_EDIT,
|
phase: PHASE_EDIT,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -112,20 +115,23 @@ export default React.createClass({
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_importE2eKeysDialog'
|
<BaseDialog className='mx_importE2eKeysDialog'
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title="Import room keys"
|
title={_t("Import room keys")}
|
||||||
>
|
>
|
||||||
<form onSubmit={this._onFormSubmit}>
|
<form onSubmit={this._onFormSubmit}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<p>
|
<p>
|
||||||
This process allows you to import encryption keys
|
{ _t(
|
||||||
that you had previously exported from another Matrix
|
'This process allows you to import encryption keys ' +
|
||||||
client. You will then be able to decrypt any
|
'that you had previously exported from another Matrix ' +
|
||||||
messages that the other client could decrypt.
|
'client. You will then be able to decrypt any ' +
|
||||||
|
'messages that the other client could decrypt.',
|
||||||
|
) }
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The export file will be protected with a passphrase.
|
{ _t(
|
||||||
You should enter the passphrase here, to decrypt the
|
'The export file will be protected with a passphrase. ' +
|
||||||
file.
|
'You should enter the passphrase here, to decrypt the file.',
|
||||||
|
) }
|
||||||
</p>
|
</p>
|
||||||
<div className='error'>
|
<div className='error'>
|
||||||
{this.state.errStr}
|
{this.state.errStr}
|
||||||
|
@ -134,7 +140,7 @@ export default React.createClass({
|
||||||
<div className='mx_E2eKeysDialog_inputRow'>
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
<label htmlFor='importFile'>
|
<label htmlFor='importFile'>
|
||||||
File to import
|
{_t("File to import")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
@ -147,7 +153,7 @@ export default React.createClass({
|
||||||
<div className='mx_E2eKeysDialog_inputRow'>
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
<label htmlFor='passphrase'>
|
<label htmlFor='passphrase'>
|
||||||
Enter passphrase
|
{_t("Enter passphrase")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
@ -160,11 +166,11 @@ export default React.createClass({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_Dialog_buttons'>
|
<div className='mx_Dialog_buttons'>
|
||||||
<input className='mx_Dialog_primary' type='submit' value='Import'
|
<input className='mx_Dialog_primary' type='submit' value={_t('Import')}
|
||||||
disabled={!this.state.enableSubmit || disableForm}
|
disabled={!this.state.enableSubmit || disableForm}
|
||||||
/>
|
/>
|
||||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
Cancel
|
{_t("Cancel")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -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 React from 'react';
|
||||||
import type {Completion, SelectionRange} from './Autocompleter';
|
import type {Completion, SelectionRange} from './Autocompleter';
|
||||||
|
|
||||||
export default class AutocompleteProvider {
|
export default class AutocompleteProvider {
|
||||||
constructor(commandRegex?: RegExp, fuseOpts?: any) {
|
constructor(commandRegex?: RegExp) {
|
||||||
if (commandRegex) {
|
if (commandRegex) {
|
||||||
if (!commandRegex.global) {
|
if (!commandRegex.global) {
|
||||||
throw new Error('commandRegex must have global flag set');
|
throw new Error('commandRegex must have global flag set');
|
||||||
|
|
|
@ -1,3 +1,19 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 Aviral Dasgupta
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import type {Component} from 'react';
|
import type {Component} from 'react';
|
||||||
|
@ -6,7 +22,7 @@ import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||||
import RoomProvider from './RoomProvider';
|
import RoomProvider from './RoomProvider';
|
||||||
import UserProvider from './UserProvider';
|
import UserProvider from './UserProvider';
|
||||||
import EmojiProvider from './EmojiProvider';
|
import EmojiProvider from './EmojiProvider';
|
||||||
import Q from 'q';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
export type SelectionRange = {
|
export type SelectionRange = {
|
||||||
start: number,
|
start: number,
|
||||||
|
@ -18,6 +34,9 @@ export type Completion = {
|
||||||
component: ?Component,
|
component: ?Component,
|
||||||
range: SelectionRange,
|
range: SelectionRange,
|
||||||
command: ?string,
|
command: ?string,
|
||||||
|
// If provided, apply a LINK entity to the completion with the
|
||||||
|
// data = { url: href }.
|
||||||
|
href: ?string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
|
@ -36,21 +55,24 @@ export async function getCompletions(query: string, selection: SelectionRange, f
|
||||||
otherwise, we run into a condition where new completions are displayed
|
otherwise, we run into a condition where new completions are displayed
|
||||||
while the user is interacting with the list, which makes it difficult
|
while the user is interacting with the list, which makes it difficult
|
||||||
to predict whether an action will actually do what is intended
|
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
|
const completionsList = await Promise.all(
|
||||||
state (== "fulfilled" || "rejected") and value. */
|
// Array of inspections of promises that might timeout. Instead of allowing a
|
||||||
const completionsList = await Q.allSettled(
|
// single timeout to reject the Promise.all, reflect each one and once they've all
|
||||||
PROVIDERS.map(provider => {
|
// settled, filter for the fulfilled ones
|
||||||
return Q(provider.getCompletions(query, selection, force))
|
PROVIDERS.map((provider) => {
|
||||||
.timeout(PROVIDER_COMPLETION_TIMEOUT);
|
return provider
|
||||||
})
|
.getCompletions(query, selection, force)
|
||||||
|
.timeout(PROVIDER_COMPLETION_TIMEOUT)
|
||||||
|
.reflect();
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return completionsList
|
return completionsList.filter(
|
||||||
.filter(completion => completion.state === "fulfilled")
|
(inspection) => inspection.isFulfilled(),
|
||||||
.map((completionsState, i) => {
|
).map((completionsState, i) => {
|
||||||
return {
|
return {
|
||||||
completions: completionsState.value,
|
completions: completionsState.value(),
|
||||||
provider: PROVIDERS[i],
|
provider: PROVIDERS[i],
|
||||||
|
|
||||||
/* the currently matched "command" the completer tried to complete
|
/* the currently matched "command" the completer tried to complete
|
||||||
|
|
|
@ -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 React from 'react';
|
||||||
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Fuse from 'fuse.js';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import {TextualCompletion} from './Components';
|
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 = [
|
const COMMANDS = [
|
||||||
{
|
{
|
||||||
command: '/me',
|
command: '/me',
|
||||||
|
@ -14,6 +34,16 @@ const COMMANDS = [
|
||||||
args: '<user-id> [reason]',
|
args: '<user-id> [reason]',
|
||||||
description: 'Bans user with given id',
|
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',
|
command: '/deop',
|
||||||
args: '<user-id>',
|
args: '<user-id>',
|
||||||
|
@ -29,6 +59,16 @@ const COMMANDS = [
|
||||||
args: '<room-alias>',
|
args: '<room-alias>',
|
||||||
description: 'Joins room with given 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',
|
command: '/kick',
|
||||||
args: '<user-id> [reason]',
|
args: '<user-id> [reason]',
|
||||||
|
@ -43,32 +83,43 @@ const COMMANDS = [
|
||||||
command: '/ddg',
|
command: '/ddg',
|
||||||
args: '<query>',
|
args: '<query>',
|
||||||
description: 'Searches DuckDuckGo for results',
|
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;
|
let instance = null;
|
||||||
|
|
||||||
export default class CommandProvider extends AutocompleteProvider {
|
export default class CommandProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(COMMAND_RE);
|
super(COMMAND_RE);
|
||||||
this.fuse = new Fuse(COMMANDS, {
|
this.matcher = new FuzzyMatcher(COMMANDS, {
|
||||||
keys: ['command', 'args', 'description'],
|
keys: ['command', 'args', 'description'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: {start: number, end: number}) {
|
async getCompletions(query: string, selection: {start: number, end: number}) {
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
completions = this.fuse.search(command[0]).map(result => {
|
completions = this.matcher.match(command[0]).map((result) => {
|
||||||
return {
|
return {
|
||||||
completion: result.command + ' ',
|
completion: result.command + ' ',
|
||||||
component: (<TextualCompletion
|
component: (<TextualCompletion
|
||||||
title={result.command}
|
title={result.command}
|
||||||
subtitle={result.args}
|
subtitle={result.args}
|
||||||
description={result.description}
|
description={ _t(result.description) }
|
||||||
/>),
|
/>),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
|
@ -78,12 +129,11 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return '*️⃣ Commands';
|
return '*️⃣ ' + _t('Commands');
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(): CommandProvider {
|
static getInstance(): CommandProvider {
|
||||||
if (instance == null)
|
if (instance === null) instance = new CommandProvider();
|
||||||
{instance = new CommandProvider();}
|
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,20 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 Aviral Dasgupta
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
/* These were earlier stateless functional components but had to be converted
|
/* These were earlier stateless functional components but had to be converted
|
||||||
|
|
|
@ -1,4 +1,22 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 Aviral Dasgupta
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import 'whatwg-fetch';
|
import 'whatwg-fetch';
|
||||||
|
|
||||||
|
@ -75,7 +93,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return '🔍 Results from DuckDuckGo';
|
return '🔍 ' + _t('Results from DuckDuckGo');
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(): DuckDuckGoProvider {
|
static getInstance(): DuckDuckGoProvider {
|
||||||
|
|
|
@ -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 React from 'react';
|
||||||
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
|
import {emojioneList, shortnameToImage, shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
|
||||||
import Fuse from 'fuse.js';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import type {SelectionRange, Completion} from './Autocompleter';
|
import type {SelectionRange, Completion} from './Autocompleter';
|
||||||
|
import _uniq from 'lodash/uniq';
|
||||||
|
import _sortBy from 'lodash/sortBy';
|
||||||
|
|
||||||
const EMOJI_REGEX = /:\w*:?/g;
|
import EmojiData from '../stripped-emoji.json';
|
||||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
|
||||||
|
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;
|
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 {
|
export default class EmojiProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(EMOJI_REGEX);
|
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) {
|
async getCompletions(query: string, selection: SelectionRange) {
|
||||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
completions = this.fuse.search(command[0]).map(result => {
|
let matchedString = command[0];
|
||||||
const shortname = EMOJI_SHORTNAMES[result];
|
|
||||||
|
// 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);
|
const unicode = shortnameToUnicode(shortname);
|
||||||
return {
|
return {
|
||||||
completion: unicode,
|
completion: unicode,
|
||||||
|
@ -33,13 +137,13 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
}).slice(0, 8);
|
}).slice(0, LIMIT);
|
||||||
}
|
}
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return '😃 Emoji';
|
return '😃 ' + _t('Emoji');
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance() {
|
static getInstance() {
|
||||||
|
|
107
src/autocomplete/FuzzyMatcher.js
Normal file
107
src/autocomplete/FuzzyMatcher.js
Normal 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;
|
||||||
|
// }
|
||||||
|
//}
|
112
src/autocomplete/QueryMatcher.js
Normal file
112
src/autocomplete/QueryMatcher.js
Normal 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];
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 React from 'react';
|
||||||
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import Fuse from 'fuse.js';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import {getDisplayAliasForRoom} from '../Rooms';
|
import {getDisplayAliasForRoom} from '../Rooms';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
|
import _sortBy from 'lodash/sortBy';
|
||||||
|
|
||||||
const ROOM_REGEX = /(?=#)(\S*)/g;
|
const ROOM_REGEX = /(?=#)(\S*)/g;
|
||||||
|
|
||||||
let instance = null;
|
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 {
|
export default class RoomProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(ROOM_REGEX, {
|
super(ROOM_REGEX);
|
||||||
keys: ['displayName', 'userId'],
|
this.matcher = new FuzzyMatcher([], {
|
||||||
});
|
keys: ['displayedAlias', 'name'],
|
||||||
this.fuse = new Fuse([], {
|
|
||||||
keys: ['name', 'roomId', 'aliases'],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
||||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
let client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
// the only reason we need to do this is because Fuse only matches on properties
|
// 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 {
|
return {
|
||||||
room: room,
|
room: room,
|
||||||
name: room.name,
|
name: room.name,
|
||||||
aliases: room.getAliases(),
|
displayedAlias: getDisplayAliasForRoom(room),
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
completions = this.fuse.search(command[0]).map(room => {
|
const matchedString = command[0];
|
||||||
let displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
|
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 {
|
return {
|
||||||
completion: displayAlias,
|
completion: displayAlias,
|
||||||
|
suffix: ' ',
|
||||||
|
href: 'https://matrix.to/#/' + displayAlias,
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
||||||
),
|
),
|
||||||
range,
|
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;
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return '💬 Rooms';
|
return '💬 ' + _t('Rooms');
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance() {
|
static getInstance() {
|
||||||
|
@ -62,12 +99,8 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
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}
|
{completions}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldForceComplete(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 React from 'react';
|
||||||
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Q from 'q';
|
|
||||||
import Fuse from 'fuse.js';
|
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import sdk from '../index';
|
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;
|
const USER_REGEX = /@\S*/g;
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
export default class UserProvider extends AutocompleteProvider {
|
export default class UserProvider extends AutocompleteProvider {
|
||||||
|
users: Array<RoomMember> = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(USER_REGEX, {
|
super(USER_REGEX, {
|
||||||
keys: ['name', 'userId'],
|
keys: ['name'],
|
||||||
});
|
});
|
||||||
this.users = [];
|
this.matcher = new FuzzyMatcher([], {
|
||||||
this.fuse = new Fuse([], {
|
keys: ['name'],
|
||||||
keys: ['name', 'userId'],
|
shouldMatchPrefix: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,17 +51,12 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection, force);
|
let {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
this.fuse.set(this.users);
|
completions = this.matcher.match(command[0]).map((user) => {
|
||||||
completions = this.fuse.search(command[0]).map(user => {
|
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
||||||
let displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
|
||||||
let completion = displayName;
|
|
||||||
if (range.start === 0) {
|
|
||||||
completion += ': ';
|
|
||||||
} else {
|
|
||||||
completion += ' ';
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
completion,
|
completion: displayName,
|
||||||
|
suffix: range.start === 0 ? ': ' : ' ',
|
||||||
|
href: 'https://matrix.to/#/' + user.userId,
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion
|
<PillCompletion
|
||||||
initialComponent={<MemberAvatar member={user} width={24} height={24}/>}
|
initialComponent={<MemberAvatar member={user} width={24} height={24}/>}
|
||||||
|
@ -45,17 +65,44 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
}).slice(0, 4);
|
});
|
||||||
}
|
}
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return '👥 Users';
|
return '👥 ' + _t('Users');
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserList(users) {
|
setUserListFromRoom(room: Room) {
|
||||||
this.users = users;
|
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 {
|
static getInstance(): UserProvider {
|
||||||
|
@ -66,7 +113,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
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}
|
{completions}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
|
@ -16,15 +16,15 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
import { _t } from '../../languageHandler';
|
||||||
var PresetValues = {
|
import sdk from '../../index';
|
||||||
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
|
const PresetValues = {
|
||||||
PrivateChat: "private_chat",
|
PrivateChat: "private_chat",
|
||||||
PublicChat: "public_chat",
|
PublicChat: "public_chat",
|
||||||
Custom: "custom",
|
Custom: "custom",
|
||||||
};
|
};
|
||||||
var q = require('q');
|
|
||||||
var sdk = require('../../index');
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'CreateRoom',
|
displayName: 'CreateRoom',
|
||||||
|
@ -231,7 +231,7 @@ module.exports = React.createClass({
|
||||||
if (curr_phase == this.phases.ERROR) {
|
if (curr_phase == this.phases.ERROR) {
|
||||||
error_box = (
|
error_box = (
|
||||||
<div className="mx_Error">
|
<div className="mx_Error">
|
||||||
An error occured: {this.state.error_string}
|
{_t('An error occurred: %(error_string)s', {error_string: this.state.error_string})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -246,29 +246,29 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CreateRoom">
|
<div className="mx_CreateRoom">
|
||||||
<SimpleRoomHeader title="CreateRoom" collapsedRhs={ this.props.collapsedRhs }/>
|
<SimpleRoomHeader title={_t("Create Room")} collapsedRhs={ this.props.collapsedRhs }/>
|
||||||
<div className="mx_CreateRoom_body">
|
<div className="mx_CreateRoom_body">
|
||||||
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder="Name"/> <br />
|
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder={_t('Name')}/> <br />
|
||||||
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder="Topic"/> <br />
|
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder={_t('Topic')}/> <br />
|
||||||
<RoomAlias ref="alias" alias={this.state.alias} homeserver={ domain } onChange={this.onAliasChanged}/> <br />
|
<RoomAlias ref="alias" alias={this.state.alias} homeserver={ domain } onChange={this.onAliasChanged}/> <br />
|
||||||
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br />
|
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br />
|
||||||
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
|
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/>
|
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/>
|
||||||
Make this room private
|
{_t('Make this room private')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged}/>
|
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged}/>
|
||||||
Share message history with new users
|
{_t('Share message history with new users')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_CreateRoom_encrypt">
|
<div className="mx_CreateRoom_encrypt">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/>
|
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/>
|
||||||
Encrypt room
|
{_t('Encrypt room')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
var ReactDOM = require("react-dom");
|
|
||||||
|
|
||||||
var Matrix = require("matrix-js-sdk");
|
import Matrix from 'matrix-js-sdk';
|
||||||
var sdk = require('../../index');
|
import sdk from '../../index';
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
var dis = require("../../dispatcher");
|
import { _t, _tJsx } from '../../languageHandler';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Component which shows the filtered file using a TimelinePanel
|
* Component which shows the filtered file using a TimelinePanel
|
||||||
|
@ -59,6 +58,8 @@ var FilePanel = React.createClass({
|
||||||
var client = MatrixClientPeg.get();
|
var client = MatrixClientPeg.get();
|
||||||
var room = client.getRoom(roomId);
|
var room = client.getRoom(roomId);
|
||||||
|
|
||||||
|
this.noRoom = !room;
|
||||||
|
|
||||||
if (room) {
|
if (room) {
|
||||||
var filter = new Matrix.Filter(client.credentials.userId);
|
var filter = new Matrix.Filter(client.credentials.userId);
|
||||||
filter.setDefinition(
|
filter.setDefinition(
|
||||||
|
@ -82,13 +83,24 @@ var FilePanel = React.createClass({
|
||||||
console.error("Failed to get or create file panel filter", error);
|
console.error("Failed to get or create file panel filter", error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
|
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
|
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||||
|
<div className="mx_RoomView_empty">
|
||||||
|
{_tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{sub}</a>)}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
} else if (this.noRoom) {
|
||||||
|
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||||
|
<div className="mx_RoomView_empty">{_t("You must join the room to see its files")}</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||||
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
@ -105,7 +117,7 @@ var FilePanel = React.createClass({
|
||||||
showUrlPreview = { false }
|
showUrlPreview = { false }
|
||||||
tileShape="file_grid"
|
tileShape="file_grid"
|
||||||
opacity={ this.props.opacity }
|
opacity={ this.props.opacity }
|
||||||
empty="There are no visible files in this room"
|
empty={_t('There are no visible files in this room')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
531
src/components/structures/GroupView.js
Normal file
531
src/components/structures/GroupView.js
Normal 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 />;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
|
@ -19,8 +19,6 @@ const InteractiveAuth = Matrix.InteractiveAuth;
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import sdk from '../../index';
|
|
||||||
|
|
||||||
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
|
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,11 +18,15 @@ limitations under the License.
|
||||||
import * as Matrix from 'matrix-js-sdk';
|
import * as Matrix from 'matrix-js-sdk';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import UserSettingsStore from '../../UserSettingsStore';
|
||||||
import KeyCode from '../../KeyCode';
|
import KeyCode from '../../KeyCode';
|
||||||
import Notifier from '../../Notifier';
|
import Notifier from '../../Notifier';
|
||||||
import PageTypes from '../../PageTypes';
|
import PageTypes from '../../PageTypes';
|
||||||
|
import CallMediaHandler from '../../CallMediaHandler';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
|
import sessionStore from '../../stores/SessionStore';
|
||||||
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||||
|
@ -38,10 +43,13 @@ export default React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||||
page_type: React.PropTypes.string.isRequired,
|
page_type: React.PropTypes.string.isRequired,
|
||||||
onRoomIdResolved: React.PropTypes.func,
|
|
||||||
onRoomCreated: React.PropTypes.func,
|
onRoomCreated: React.PropTypes.func,
|
||||||
onUserSettingsClose: React.PropTypes.func,
|
onUserSettingsClose: React.PropTypes.func,
|
||||||
|
|
||||||
|
// Called with the credentials of a registered user (if they were a ROU that
|
||||||
|
// transitioned to PWLU)
|
||||||
|
onRegistered: React.PropTypes.func,
|
||||||
|
|
||||||
teamToken: React.PropTypes.string,
|
teamToken: React.PropTypes.string,
|
||||||
|
|
||||||
// and lots and lots of other stuff.
|
// and lots and lots of other stuff.
|
||||||
|
@ -62,6 +70,13 @@ export default React.createClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
// use compact timeline view
|
||||||
|
useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
// stash the MatrixClient in case we log out before we are unmounted
|
// stash the MatrixClient in case we log out before we are unmounted
|
||||||
this._matrixClient = this.props.matrixClient;
|
this._matrixClient = this.props.matrixClient;
|
||||||
|
@ -70,11 +85,35 @@ export default React.createClass({
|
||||||
// RoomView.getScrollState()
|
// RoomView.getScrollState()
|
||||||
this._scrollStateMap = {};
|
this._scrollStateMap = {};
|
||||||
|
|
||||||
|
CallMediaHandler.loadDevices();
|
||||||
|
|
||||||
document.addEventListener('keydown', this._onKeyDown);
|
document.addEventListener('keydown', this._onKeyDown);
|
||||||
|
|
||||||
|
this._sessionStore = sessionStore;
|
||||||
|
this._sessionStoreToken = this._sessionStore.addListener(
|
||||||
|
this._setStateFromSessionStore,
|
||||||
|
);
|
||||||
|
this._setStateFromSessionStore();
|
||||||
|
|
||||||
|
this._matrixClient.on("accountData", this.onAccountData);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
document.removeEventListener('keydown', this._onKeyDown);
|
document.removeEventListener('keydown', this._onKeyDown);
|
||||||
|
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||||
|
if (this._sessionStoreToken) {
|
||||||
|
this._sessionStoreToken.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Child components assume that the client peg will not be null, so give them some
|
||||||
|
// sort of assurance here by only allowing a re-render if the client is truthy.
|
||||||
|
//
|
||||||
|
// This is required because `LoggedInView` maintains its own state and if this state
|
||||||
|
// updates after the client peg has been made null (during logout), then it will
|
||||||
|
// attempt to re-render and the children will throw errors.
|
||||||
|
shouldComponentUpdate: function() {
|
||||||
|
return Boolean(MatrixClientPeg.get());
|
||||||
},
|
},
|
||||||
|
|
||||||
getScrollStateForRoom: function(roomId) {
|
getScrollStateForRoom: function(roomId) {
|
||||||
|
@ -88,6 +127,20 @@ export default React.createClass({
|
||||||
return this.refs.roomView.canResetTimeline();
|
return this.refs.roomView.canResetTimeline();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_setStateFromSessionStore() {
|
||||||
|
this.setState({
|
||||||
|
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onAccountData: function(event) {
|
||||||
|
if (event.getType() === "im.vector.web.settings") {
|
||||||
|
this.setState({
|
||||||
|
useCompactLayout: event.getContent().useCompactLayout,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_onKeyDown: function(ev) {
|
_onKeyDown: function(ev) {
|
||||||
/*
|
/*
|
||||||
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
||||||
|
@ -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) {
|
switch (ev.keyCode) {
|
||||||
case KeyCode.UP:
|
case KeyCode.UP:
|
||||||
case KeyCode.DOWN:
|
case KeyCode.DOWN:
|
||||||
if (ev.altKey) {
|
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||||
var action = ev.keyCode == KeyCode.UP ?
|
let action = ev.keyCode == KeyCode.UP ?
|
||||||
'view_prev_room' : 'view_next_room';
|
'view_prev_room' : 'view_next_room';
|
||||||
dis.dispatch({action: action});
|
dis.dispatch({action: action});
|
||||||
handled = true;
|
handled = true;
|
||||||
|
@ -118,17 +178,27 @@ export default React.createClass({
|
||||||
|
|
||||||
case KeyCode.PAGE_UP:
|
case KeyCode.PAGE_UP:
|
||||||
case KeyCode.PAGE_DOWN:
|
case KeyCode.PAGE_DOWN:
|
||||||
|
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this._onScrollKeyPressed(ev);
|
this._onScrollKeyPressed(ev);
|
||||||
handled = true;
|
handled = true;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCode.HOME:
|
case KeyCode.HOME:
|
||||||
case KeyCode.END:
|
case KeyCode.END:
|
||||||
if (ev.ctrlKey) {
|
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this._onScrollKeyPressed(ev);
|
this._onScrollKeyPressed(ev);
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case KeyCode.KEY_K:
|
||||||
|
if (ctrlCmdOnly) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'focus_room_filter',
|
||||||
|
});
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handled) {
|
if (handled) {
|
||||||
|
@ -142,54 +212,60 @@ export default React.createClass({
|
||||||
if (this.refs.roomView) {
|
if (this.refs.roomView) {
|
||||||
this.refs.roomView.handleScrollKey(ev);
|
this.refs.roomView.handleScrollKey(ev);
|
||||||
}
|
}
|
||||||
|
else if (this.refs.roomDirectory) {
|
||||||
|
this.refs.roomDirectory.handleScrollKey(ev);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var LeftPanel = sdk.getComponent('structures.LeftPanel');
|
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||||
var RightPanel = sdk.getComponent('structures.RightPanel');
|
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||||
var RoomView = sdk.getComponent('structures.RoomView');
|
const RoomView = sdk.getComponent('structures.RoomView');
|
||||||
var UserSettings = sdk.getComponent('structures.UserSettings');
|
const UserSettings = sdk.getComponent('structures.UserSettings');
|
||||||
var CreateRoom = sdk.getComponent('structures.CreateRoom');
|
const CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||||
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||||
var HomePage = sdk.getComponent('structures.HomePage');
|
const HomePage = sdk.getComponent('structures.HomePage');
|
||||||
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
const GroupView = sdk.getComponent('structures.GroupView');
|
||||||
var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
|
const MyGroups = sdk.getComponent('structures.MyGroups');
|
||||||
var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
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;
|
let page_element;
|
||||||
var right_panel = '';
|
let right_panel = '';
|
||||||
|
|
||||||
switch (this.props.page_type) {
|
switch (this.props.page_type) {
|
||||||
case PageTypes.RoomView:
|
case PageTypes.RoomView:
|
||||||
page_element = <RoomView
|
page_element = <RoomView
|
||||||
ref='roomView'
|
ref='roomView'
|
||||||
roomAddress={this.props.currentRoomAlias || this.props.currentRoomId}
|
|
||||||
autoJoin={this.props.autoJoin}
|
autoJoin={this.props.autoJoin}
|
||||||
onRoomIdResolved={this.props.onRoomIdResolved}
|
onRegistered={this.props.onRegistered}
|
||||||
eventId={this.props.initialEventId}
|
|
||||||
thirdPartyInvite={this.props.thirdPartyInvite}
|
thirdPartyInvite={this.props.thirdPartyInvite}
|
||||||
oobData={this.props.roomOobData}
|
oobData={this.props.roomOobData}
|
||||||
highlightedEventId={this.props.highlightedEventId}
|
|
||||||
eventPixelOffset={this.props.initialEventPixelOffset}
|
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||||
key={this.props.currentRoomAlias || this.props.currentRoomId}
|
key={this.props.currentRoomId || 'roomview'}
|
||||||
opacity={this.props.middleOpacity}
|
opacity={this.props.middleOpacity}
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
collapsedRhs={this.props.collapse_rhs}
|
||||||
ConferenceHandler={this.props.ConferenceHandler}
|
ConferenceHandler={this.props.ConferenceHandler}
|
||||||
scrollStateMap={this._scrollStateMap}
|
scrollStateMap={this._scrollStateMap}
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.sideOpacity} />;
|
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.UserSettings:
|
case PageTypes.UserSettings:
|
||||||
page_element = <UserSettings
|
page_element = <UserSettings
|
||||||
onClose={this.props.onUserSettingsClose}
|
onClose={this.props.onUserSettingsClose}
|
||||||
brand={this.props.config.brand}
|
brand={this.props.config.brand}
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
|
||||||
enableLabs={this.props.config.enableLabs}
|
enableLabs={this.props.config.enableLabs}
|
||||||
referralBaseUrl={this.props.config.referralBaseUrl}
|
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||||
teamToken={this.props.teamToken}
|
teamToken={this.props.teamToken}
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PageTypes.MyGroups:
|
||||||
|
page_element = <MyGroups />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.CreateRoom:
|
case PageTypes.CreateRoom:
|
||||||
|
@ -197,42 +273,55 @@ export default React.createClass({
|
||||||
onRoomCreated={this.props.onRoomCreated}
|
onRoomCreated={this.props.onRoomCreated}
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
collapsedRhs={this.props.collapse_rhs}
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.RoomDirectory:
|
case PageTypes.RoomDirectory:
|
||||||
page_element = <RoomDirectory
|
page_element = <RoomDirectory
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
ref="roomDirectory"
|
||||||
config={this.props.config.roomDirectory}
|
config={this.props.config.roomDirectory}
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.HomePage:
|
case PageTypes.HomePage:
|
||||||
|
{
|
||||||
|
// If team server config is present, pass the teamServerURL. props.teamToken
|
||||||
|
// must also be set for the team page to be displayed, otherwise the
|
||||||
|
// welcomePageUrl is used (which might be undefined).
|
||||||
|
const teamServerUrl = this.props.config.teamServerConfig ?
|
||||||
|
this.props.config.teamServerConfig.teamServerURL : null;
|
||||||
|
|
||||||
page_element = <HomePage
|
page_element = <HomePage
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
teamServerUrl={teamServerUrl}
|
||||||
teamServerUrl={this.props.config.teamServerConfig.teamServerURL}
|
|
||||||
teamToken={this.props.teamToken}
|
teamToken={this.props.teamToken}
|
||||||
/>
|
homePageUrl={this.props.config.welcomePageUrl}
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>
|
/>;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.UserView:
|
case PageTypes.UserView:
|
||||||
page_element = null; // deliberately null for now
|
page_element = null; // deliberately null for now
|
||||||
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.sideOpacity} />;
|
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
|
||||||
|
break;
|
||||||
|
case PageTypes.GroupView:
|
||||||
|
page_element = <GroupView
|
||||||
|
groupId={this.props.currentGroupId}
|
||||||
|
/>;
|
||||||
|
//right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var topBar;
|
let topBar;
|
||||||
|
const isGuest = this.props.matrixClient.isGuest();
|
||||||
if (this.props.hasNewVersion) {
|
if (this.props.hasNewVersion) {
|
||||||
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
|
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
|
||||||
releaseNotes={this.props.newVersionReleaseNotes}
|
releaseNotes={this.props.newVersionReleaseNotes}
|
||||||
/>;
|
/>;
|
||||||
}
|
} else if (this.props.checkingForUpdate) {
|
||||||
else if (this.props.matrixClient.isGuest()) {
|
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
|
||||||
topBar = <GuestWarningBar />;
|
} else if (this.state.userHasGeneratedPassword) {
|
||||||
}
|
topBar = <PasswordNagBar />;
|
||||||
else if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
|
} else if (!isGuest && Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
|
||||||
topBar = <MatrixToolbar />;
|
topBar = <MatrixToolbar />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,6 +329,9 @@ export default React.createClass({
|
||||||
if (topBar) {
|
if (topBar) {
|
||||||
bodyClasses += ' mx_MatrixChat_toolbarShowing';
|
bodyClasses += ' mx_MatrixChat_toolbarShowing';
|
||||||
}
|
}
|
||||||
|
if (this.state.useCompactLayout) {
|
||||||
|
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mx_MatrixChat_wrapper'>
|
<div className='mx_MatrixChat_wrapper'>
|
||||||
|
@ -248,8 +340,7 @@ export default React.createClass({
|
||||||
<LeftPanel
|
<LeftPanel
|
||||||
selectedRoom={this.props.currentRoomId}
|
selectedRoom={this.props.currentRoomId}
|
||||||
collapsed={this.props.collapse_lhs || false}
|
collapsed={this.props.collapse_lhs || false}
|
||||||
opacity={this.props.sideOpacity}
|
opacity={this.props.leftOpacity}
|
||||||
teamToken={this.props.teamToken}
|
|
||||||
/>
|
/>
|
||||||
<main className='mx_MatrixChat_middlePanel'>
|
<main className='mx_MatrixChat_middlePanel'>
|
||||||
{page_element}
|
{page_element}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
var ReactDOM = require("react-dom");
|
import ReactDOM from 'react-dom';
|
||||||
var dis = require("../../dispatcher");
|
import UserSettingsStore from '../../UserSettingsStore';
|
||||||
var sdk = require('../../index');
|
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;
|
const MILLIS_IN_DAY = 86400000;
|
||||||
|
|
||||||
|
@ -84,6 +86,12 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// shape parameter to be passed to EventTiles
|
// shape parameter to be passed to EventTiles
|
||||||
tileShape: React.PropTypes.string,
|
tileShape: React.PropTypes.string,
|
||||||
|
|
||||||
|
// show twelve hour timestamps
|
||||||
|
isTwelveHour: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// show timestamps always
|
||||||
|
alwaysShowTimestamps: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
@ -104,6 +112,8 @@ module.exports = React.createClass({
|
||||||
// Velocity requires
|
// Velocity requires
|
||||||
this._readMarkerGhostNode = null;
|
this._readMarkerGhostNode = null;
|
||||||
|
|
||||||
|
this._syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||||
|
|
||||||
this._isMounted = true;
|
this._isMounted = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -229,9 +239,21 @@ module.exports = React.createClass({
|
||||||
return !this._isMounted;
|
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() {
|
_getEventTiles: function() {
|
||||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
var DateSeparator = sdk.getComponent('messages.DateSeparator');
|
|
||||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||||
|
|
||||||
this.eventNodes = {};
|
this.eventNodes = {};
|
||||||
|
@ -240,20 +262,21 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// first figure out which is the last event in the list which we're
|
// first figure out which is the last event in the list which we're
|
||||||
// actually going to show; this allows us to behave slightly
|
// 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
|
// we also need to figure out which is the last event we show which isn't
|
||||||
// a local echo, to manage the read-marker.
|
// a local echo, to manage the read-marker.
|
||||||
var lastShownEventIndex = -1;
|
let lastShownEvent;
|
||||||
|
|
||||||
var lastShownNonLocalEchoIndex = -1;
|
var lastShownNonLocalEchoIndex = -1;
|
||||||
for (i = this.props.events.length-1; i >= 0; i--) {
|
for (i = this.props.events.length-1; i >= 0; i--) {
|
||||||
var mxEv = this.props.events[i];
|
var mxEv = this.props.events[i];
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
if (!this._shouldShowEvent(mxEv)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastShownEventIndex < 0) {
|
if (lastShownEvent === undefined) {
|
||||||
lastShownEventIndex = i;
|
lastShownEvent = mxEv;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mxEv.status) {
|
if (mxEv.status) {
|
||||||
|
@ -279,26 +302,18 @@ module.exports = React.createClass({
|
||||||
this.currentGhostEventId = null;
|
this.currentGhostEventId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isMembershipChange = (e) =>
|
const isMembershipChange = (e) => e.getType() === 'm.room.member';
|
||||||
e.getType() === 'm.room.member'
|
|
||||||
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
|
|
||||||
|
|
||||||
for (i = 0; i < this.props.events.length; i++) {
|
for (i = 0; i < this.props.events.length; i++) {
|
||||||
var mxEv = this.props.events[i];
|
let mxEv = this.props.events[i];
|
||||||
var wantTile = true;
|
let eventId = mxEv.getId();
|
||||||
var eventId = mxEv.getId();
|
let readMarkerInMels = false;
|
||||||
|
let last = (mxEv === lastShownEvent);
|
||||||
|
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
const wantTile = this._shouldShowEvent(mxEv);
|
||||||
wantTile = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var last = (i == lastShownEventIndex);
|
|
||||||
|
|
||||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||||
if (isMembershipChange(mxEv) &&
|
if (isMembershipChange(mxEv) && wantTile) {
|
||||||
EventTile.haveTileForEvent(mxEv) &&
|
|
||||||
!mxEv.isRedacted()
|
|
||||||
) {
|
|
||||||
let ts1 = mxEv.getTs();
|
let ts1 = mxEv.getTs();
|
||||||
// Ensure that the key of the MemberEventListSummary does not change with new
|
// Ensure that the key of the MemberEventListSummary does not change with new
|
||||||
// member events. This will prevent it from being re-created unnecessarily, and
|
// 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");
|
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
|
||||||
|
|
||||||
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
||||||
let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1}/></li>;
|
let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
|
||||||
ret.push(dateSeparator);
|
ret.push(dateSeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
let summarisedEvents = [mxEv];
|
let summarisedEvents = [mxEv];
|
||||||
for (;i + 1 < this.props.events.length; i++) {
|
for (;i + 1 < this.props.events.length; i++) {
|
||||||
let collapsedMxEv = this.props.events[i + 1];
|
const collapsedMxEv = this.props.events[i + 1];
|
||||||
|
|
||||||
// Ignore redacted member events
|
|
||||||
if (!EventTile.haveTileForEvent(collapsedMxEv)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMembershipChange(collapsedMxEv) ||
|
if (!isMembershipChange(collapsedMxEv) ||
|
||||||
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
||||||
break;
|
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);
|
summarisedEvents.push(collapsedMxEv);
|
||||||
}
|
}
|
||||||
// At this point, i = the index of the last event in the summary sequence
|
|
||||||
|
|
||||||
let eventTiles = summarisedEvents.map(
|
// At this point, i = the index of the last event in the summary sequence
|
||||||
(e) => {
|
let eventTiles = summarisedEvents.map((e) => {
|
||||||
// In order to prevent DateSeparators from appearing in the expanded form
|
// In order to prevent DateSeparators from appearing in the expanded form
|
||||||
// of MemberEventListSummary, render each member event as if the previous
|
// of MemberEventListSummary, render each member event as if the previous
|
||||||
// one was itself. This way, the timestamp of the previous event === the
|
// one was itself. This way, the timestamp of the previous event === the
|
||||||
// timestamp of the current event, and no DateSeperator is inserted.
|
// timestamp of the current event, and no DateSeperator is inserted.
|
||||||
let ret = this._getTilesForEvent(e, e);
|
const ret = this._getTilesForEvent(e, e, e === lastShownEvent);
|
||||||
prevEvent = e;
|
prevEvent = e;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}).reduce((a, b) => a.concat(b));
|
||||||
).reduce((a, b) => a.concat(b));
|
|
||||||
|
|
||||||
if (eventTiles.length === 0) {
|
if (eventTiles.length === 0) {
|
||||||
eventTiles = null;
|
eventTiles = null;
|
||||||
|
@ -352,12 +371,16 @@ module.exports = React.createClass({
|
||||||
<MemberEventListSummary
|
<MemberEventListSummary
|
||||||
key={key}
|
key={key}
|
||||||
events={summarisedEvents}
|
events={summarisedEvents}
|
||||||
data-scroll-token={eventId}
|
|
||||||
onToggle={this._onWidgetLoad} // Update scroll state
|
onToggle={this._onWidgetLoad} // Update scroll state
|
||||||
>
|
>
|
||||||
{eventTiles}
|
{eventTiles}
|
||||||
</MemberEventListSummary>
|
</MemberEventListSummary>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (readMarkerInMels) {
|
||||||
|
ret.push(this._getReadMarkerTile(visible));
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -388,6 +411,8 @@ module.exports = React.createClass({
|
||||||
isVisibleReadMarker = visible;
|
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 (eventId == this.currentGhostEventId) {
|
||||||
// if we're showing an animation, continue to show it.
|
// if we're showing an animation, continue to show it.
|
||||||
ret.push(this._getReadMarkerGhostTile());
|
ret.push(this._getReadMarkerGhostTile());
|
||||||
|
@ -405,8 +430,8 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_getTilesForEvent: function(prevEvent, mxEv, last) {
|
_getTilesForEvent: function(prevEvent, mxEv, last) {
|
||||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
var DateSeparator = sdk.getComponent('messages.DateSeparator');
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
var ret = [];
|
var ret = [];
|
||||||
|
|
||||||
// is this a continuation of the previous message?
|
// is this a continuation of the previous message?
|
||||||
|
@ -444,7 +469,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// do we need a date separator since the last event?
|
// do we need a date separator since the last event?
|
||||||
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
||||||
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1}/></li>;
|
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
|
||||||
ret.push(dateSeparator);
|
ret.push(dateSeparator);
|
||||||
continuation = false;
|
continuation = false;
|
||||||
}
|
}
|
||||||
|
@ -460,11 +485,10 @@ module.exports = React.createClass({
|
||||||
if (this.props.manageReadReceipts) {
|
if (this.props.manageReadReceipts) {
|
||||||
readReceipts = this._getReadReceiptsForEvent(mxEv);
|
readReceipts = this._getReadReceiptsForEvent(mxEv);
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.push(
|
ret.push(
|
||||||
<li key={eventId}
|
<li key={eventId}
|
||||||
ref={this._collectEventNode.bind(this, eventId)}
|
ref={this._collectEventNode.bind(this, eventId)}
|
||||||
data-scroll-token={scrollToken}>
|
data-scroll-tokens={scrollToken}>
|
||||||
<EventTile mxEvent={mxEv} continuation={continuation}
|
<EventTile mxEvent={mxEv} continuation={continuation}
|
||||||
isRedacted={mxEv.isRedacted()}
|
isRedacted={mxEv.isRedacted()}
|
||||||
onWidgetLoad={this._onWidgetLoad}
|
onWidgetLoad={this._onWidgetLoad}
|
||||||
|
@ -474,6 +498,7 @@ module.exports = React.createClass({
|
||||||
checkUnmounting={this._isUnmounting}
|
checkUnmounting={this._isUnmounting}
|
||||||
eventSendStatus={mxEv.status}
|
eventSendStatus={mxEv.status}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
|
isTwelveHour={this.props.isTwelveHour}
|
||||||
last={last} isSelectedEvent={highlight}/>
|
last={last} isSelectedEvent={highlight}/>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -607,8 +632,13 @@ module.exports = React.createClass({
|
||||||
var style = this.props.hidden ? { display: 'none' } : {};
|
var style = this.props.hidden ? { display: 'none' } : {};
|
||||||
style.opacity = this.props.opacity;
|
style.opacity = this.props.opacity;
|
||||||
|
|
||||||
|
var className = this.props.className + " mx_fadable";
|
||||||
|
if (this.props.alwaysShowTimestamps) {
|
||||||
|
className += " mx_MessagePanel_alwaysShowTimestamps";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollPanel ref="scrollPanel" className={ this.props.className + " mx_fadable" }
|
<ScrollPanel ref="scrollPanel" className={ className }
|
||||||
onScroll={ this.props.onScroll }
|
onScroll={ this.props.onScroll }
|
||||||
onResize={ this.onResize }
|
onResize={ this.onResize }
|
||||||
onFillRequest={ this.props.onFillRequest }
|
onFillRequest={ this.props.onFillRequest }
|
||||||
|
|
141
src/components/structures/MyGroups.js
Normal file
141
src/components/structures/MyGroups.js
Normal 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>;
|
||||||
|
},
|
||||||
|
}));
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var ReactDOM = require("react-dom");
|
var ReactDOM = require("react-dom");
|
||||||
|
import { _t } from '../../languageHandler';
|
||||||
var Matrix = require("matrix-js-sdk");
|
var Matrix = require("matrix-js-sdk");
|
||||||
var sdk = require('../../index');
|
var sdk = require('../../index');
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
|
@ -37,7 +37,6 @@ var NotificationPanel = React.createClass({
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
var timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
var timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||||
|
|
||||||
if (timelineSet) {
|
if (timelineSet) {
|
||||||
return (
|
return (
|
||||||
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
|
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
|
||||||
|
@ -48,7 +47,7 @@ var NotificationPanel = React.createClass({
|
||||||
showUrlPreview = { false }
|
showUrlPreview = { false }
|
||||||
opacity={ this.props.opacity }
|
opacity={ this.props.opacity }
|
||||||
tileShape="notif"
|
tileShape="notif"
|
||||||
empty="You have no visible notifications"
|
empty={ _t('You have no visible notifications') }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
var sdk = require('../../index');
|
import { _t, _tJsx } from '../../languageHandler';
|
||||||
var dis = require("../../dispatcher");
|
import sdk from '../../index';
|
||||||
var WhoIsTyping = require("../../WhoIsTyping");
|
import WhoIsTyping from '../../WhoIsTyping';
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
const MemberAvatar = require("../views/avatars/MemberAvatar");
|
import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||||
|
|
||||||
const HIDE_DEBOUNCE_MS = 10000;
|
const HIDE_DEBOUNCE_MS = 10000;
|
||||||
const STATUS_BAR_HIDDEN = 0;
|
const STATUS_BAR_HIDDEN = 0;
|
||||||
|
@ -33,9 +33,6 @@ module.exports = React.createClass({
|
||||||
// the room this statusbar is representing.
|
// the room this statusbar is representing.
|
||||||
room: React.PropTypes.object.isRequired,
|
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
|
// the number of messages which have arrived since we've been scrolled up
|
||||||
numUnreadMessages: React.PropTypes.number,
|
numUnreadMessages: React.PropTypes.number,
|
||||||
|
|
||||||
|
@ -143,12 +140,9 @@ module.exports = React.createClass({
|
||||||
(this.state.usersTyping.length > 0) ||
|
(this.state.usersTyping.length > 0) ||
|
||||||
this.props.numUnreadMessages ||
|
this.props.numUnreadMessages ||
|
||||||
!this.props.atEndOfLiveTimeline ||
|
!this.props.atEndOfLiveTimeline ||
|
||||||
this.props.hasActiveCall ||
|
this.props.hasActiveCall
|
||||||
this.props.tabComplete.isTabCompleting()
|
|
||||||
) {
|
) {
|
||||||
return STATUS_BAR_EXPANDED;
|
return STATUS_BAR_EXPANDED;
|
||||||
} else if (this.props.tabCompleteEntries) {
|
|
||||||
return STATUS_BAR_HIDDEN;
|
|
||||||
} else if (this.props.unsentMessageError) {
|
} else if (this.props.unsentMessageError) {
|
||||||
return STATUS_BAR_EXPANDED_LARGE;
|
return STATUS_BAR_EXPANDED_LARGE;
|
||||||
}
|
}
|
||||||
|
@ -175,8 +169,8 @@ module.exports = React.createClass({
|
||||||
<div className="mx_RoomStatusBar_scrollDownIndicator"
|
<div className="mx_RoomStatusBar_scrollDownIndicator"
|
||||||
onClick={ this.props.onScrollToBottomClick }>
|
onClick={ this.props.onScrollToBottomClick }>
|
||||||
<img src="img/scrolldown.svg" width="24" height="24"
|
<img src="img/scrolldown.svg" width="24" height="24"
|
||||||
alt="Scroll to bottom of page"
|
alt={ _t("Scroll to bottom of page") }
|
||||||
title="Scroll to bottom of page"/>
|
title={ _t("Scroll to bottom of page") }/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -237,8 +231,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// return suitable content for the main (text) part of the status bar.
|
// return suitable content for the main (text) part of the status bar.
|
||||||
_getContent: function() {
|
_getContent: function() {
|
||||||
var TabCompleteBar = sdk.getComponent('rooms.TabCompleteBar');
|
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
|
|
||||||
// no conn bar trumps unread count since you can't get unread messages
|
// 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">
|
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||||
Connectivity to the server has been lost.
|
{_t('Connectivity to the server has been lost.')}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||||
Sent messages will be stored until your connection has returned.
|
{_t('Sent messages will be stored until your connection has returned.')}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -281,15 +259,13 @@ module.exports = React.createClass({
|
||||||
{ this.props.unsentMessageError }
|
{ this.props.unsentMessageError }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||||
<a className="mx_RoomStatusBar_resend_link"
|
{_tJsx("<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
|
||||||
onClick={ this.props.onResendAllClick }>
|
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
|
||||||
Resend all
|
[
|
||||||
</a> or <a
|
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={ this.props.onResendAllClick }>{sub}</a>,
|
||||||
className="mx_RoomStatusBar_resend_link"
|
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={ this.props.onCancelAllClick }>{sub}</a>,
|
||||||
onClick={ this.props.onCancelAllClick }>
|
]
|
||||||
cancel all
|
)}
|
||||||
</a> now. You can also select individual messages to
|
|
||||||
resend or cancel.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -298,8 +274,8 @@ module.exports = React.createClass({
|
||||||
// unread count trumps who is typing since the unread count is only
|
// unread count trumps who is typing since the unread count is only
|
||||||
// set when you've scrolled up
|
// set when you've scrolled up
|
||||||
if (this.props.numUnreadMessages) {
|
if (this.props.numUnreadMessages) {
|
||||||
var unreadMsgs = this.props.numUnreadMessages + " new message" +
|
// MUST use var name "count" for pluralization to kick in
|
||||||
(this.props.numUnreadMessages > 1 ? "s" : "");
|
var unreadMsgs = _t("%(count)s new messages", {count: this.props.numUnreadMessages});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_unreadMessagesBar"
|
<div className="mx_RoomStatusBar_unreadMessagesBar"
|
||||||
|
@ -324,7 +300,7 @@ module.exports = React.createClass({
|
||||||
if (this.props.hasActiveCall) {
|
if (this.props.hasActiveCall) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_callBar">
|
<div className="mx_RoomStatusBar_callBar">
|
||||||
<b>Active call</b>
|
<b>{_t('Active call')}</b>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
var React = require("react");
|
var React = require("react");
|
||||||
var ReactDOM = require("react-dom");
|
var ReactDOM = require("react-dom");
|
||||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
var q = require("q");
|
import Promise from 'bluebird';
|
||||||
var KeyCode = require('../../KeyCode');
|
var KeyCode = require('../../KeyCode');
|
||||||
|
|
||||||
var DEBUG_SCROLL = false;
|
var DEBUG_SCROLL = false;
|
||||||
|
@ -46,9 +46,13 @@ if (DEBUG_SCROLL) {
|
||||||
* It also provides a hook which allows parents to provide more list elements
|
* 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.
|
* 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
|
* Each child element should have a 'data-scroll-tokens'. This string of
|
||||||
* serialise the scroll state, and returned as the 'trackedScrollToken'
|
* comma-separated tokens may contain a single token or many, where many indicates
|
||||||
* attribute by getScrollState().
|
* 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:
|
* Some notes about the implementation:
|
||||||
*
|
*
|
||||||
|
@ -141,7 +145,7 @@ module.exports = React.createClass({
|
||||||
return {
|
return {
|
||||||
stickyBottom: true,
|
stickyBottom: true,
|
||||||
startAtBottom: true,
|
startAtBottom: true,
|
||||||
onFillRequest: function(backwards) { return q(false); },
|
onFillRequest: function(backwards) { return Promise.resolve(false); },
|
||||||
onUnfillRequest: function(backwards, scrollToken) {},
|
onUnfillRequest: function(backwards, scrollToken) {},
|
||||||
onScroll: function() {},
|
onScroll: function() {},
|
||||||
};
|
};
|
||||||
|
@ -348,13 +352,14 @@ module.exports = React.createClass({
|
||||||
const tile = tiles[backwards ? i : tiles.length - 1 - i];
|
const tile = tiles[backwards ? i : tiles.length - 1 - i];
|
||||||
// Subtract height of tile as if it were unpaginated
|
// Subtract height of tile as if it were unpaginated
|
||||||
excessHeight -= tile.clientHeight;
|
excessHeight -= tile.clientHeight;
|
||||||
// The tile may not have a scroll token, so guard it
|
//If removing the tile would lead to future pagination, break before setting scroll token
|
||||||
if (tile.dataset.scrollToken) {
|
|
||||||
markerScrollToken = tile.dataset.scrollToken;
|
|
||||||
}
|
|
||||||
if (tile.clientHeight > excessHeight) {
|
if (tile.clientHeight > excessHeight) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// The tile may not have a scroll token, so guard it
|
||||||
|
if (tile.dataset.scrollTokens) {
|
||||||
|
markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (markerScrollToken) {
|
if (markerScrollToken) {
|
||||||
|
@ -381,19 +386,12 @@ module.exports = React.createClass({
|
||||||
debuglog("ScrollPanel: starting "+dir+" fill");
|
debuglog("ScrollPanel: starting "+dir+" fill");
|
||||||
|
|
||||||
// onFillRequest can end up calling us recursively (via onScroll
|
// onFillRequest can end up calling us recursively (via onScroll
|
||||||
// events) so make sure we set this before firing off the call. That
|
// events) so make sure we set this before firing off the call.
|
||||||
// does present the risk that we might not ever actually fire off the
|
|
||||||
// fill request, so wrap it in a try/catch.
|
|
||||||
this._pendingFillRequests[dir] = true;
|
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;
|
this._pendingFillRequests[dir] = false;
|
||||||
}).then((hasMoreResults) => {
|
}).then((hasMoreResults) => {
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
|
@ -419,7 +417,8 @@ module.exports = React.createClass({
|
||||||
* scroll. false if we are tracking a particular child.
|
* scroll. false if we are tracking a particular child.
|
||||||
*
|
*
|
||||||
* string trackedScrollToken: undefined if stuckAtBottom is true; if it is
|
* 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,
|
* number pixelOffset: undefined if stuckAtBottom is true; if it is false,
|
||||||
* the number of pixels the bottom of the tracked child is above the
|
* the number of pixels the bottom of the tracked child is above the
|
||||||
|
@ -483,21 +482,25 @@ module.exports = React.createClass({
|
||||||
handleScrollKey: function(ev) {
|
handleScrollKey: function(ev) {
|
||||||
switch (ev.keyCode) {
|
switch (ev.keyCode) {
|
||||||
case KeyCode.PAGE_UP:
|
case KeyCode.PAGE_UP:
|
||||||
|
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this.scrollRelative(-1);
|
this.scrollRelative(-1);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCode.PAGE_DOWN:
|
case KeyCode.PAGE_DOWN:
|
||||||
|
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this.scrollRelative(1);
|
this.scrollRelative(1);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCode.HOME:
|
case KeyCode.HOME:
|
||||||
if (ev.ctrlKey) {
|
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this.scrollToTop();
|
this.scrollToTop();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCode.END:
|
case KeyCode.END:
|
||||||
if (ev.ctrlKey) {
|
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -547,8 +550,10 @@ module.exports = React.createClass({
|
||||||
var messages = this.refs.itemlist.children;
|
var messages = this.refs.itemlist.children;
|
||||||
for (var i = messages.length-1; i >= 0; --i) {
|
for (var i = messages.length-1; i >= 0; --i) {
|
||||||
var m = messages[i];
|
var m = messages[i];
|
||||||
if (!m.dataset.scrollToken) continue;
|
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
|
||||||
if (m.dataset.scrollToken == scrollToken) {
|
// There might only be one scroll token
|
||||||
|
if (m.dataset.scrollTokens &&
|
||||||
|
m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) {
|
||||||
node = m;
|
node = m;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -564,7 +569,7 @@ module.exports = React.createClass({
|
||||||
var boundingRect = node.getBoundingClientRect();
|
var boundingRect = node.getBoundingClientRect();
|
||||||
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||||
|
|
||||||
debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" +
|
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
|
||||||
pixelOffset + " (delta: "+scrollDelta+")");
|
pixelOffset + " (delta: "+scrollDelta+")");
|
||||||
|
|
||||||
if(scrollDelta != 0) {
|
if(scrollDelta != 0) {
|
||||||
|
@ -587,12 +592,12 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
for (var i = messages.length-1; i >= 0; --i) {
|
for (var i = messages.length-1; i >= 0; --i) {
|
||||||
var node = messages[i];
|
var node = messages[i];
|
||||||
if (!node.dataset.scrollToken) continue;
|
if (!node.dataset.scrollTokens) continue;
|
||||||
|
|
||||||
var boundingRect = node.getBoundingClientRect();
|
var boundingRect = node.getBoundingClientRect();
|
||||||
newScrollState = {
|
newScrollState = {
|
||||||
stuckAtBottom: false,
|
stuckAtBottom: false,
|
||||||
trackedScrollToken: node.dataset.scrollToken,
|
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
|
||||||
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||||
};
|
};
|
||||||
// If the bottom of the panel intersects the ClientRect of node, use this node
|
// If the bottom of the panel intersects the ClientRect of node, use this node
|
||||||
|
@ -604,7 +609,7 @@ module.exports = React.createClass({
|
||||||
break;
|
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) {
|
if (newScrollState) {
|
||||||
this.scrollState = newScrollState;
|
this.scrollState = newScrollState;
|
||||||
debuglog("ScrollPanel: saved scroll state", this.scrollState);
|
debuglog("ScrollPanel: saved scroll state", this.scrollState);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,18 +17,20 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var ReactDOM = require("react-dom");
|
var ReactDOM = require("react-dom");
|
||||||
var q = require("q");
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
var Matrix = require("matrix-js-sdk");
|
var Matrix = require("matrix-js-sdk");
|
||||||
var EventTimeline = Matrix.EventTimeline;
|
var EventTimeline = Matrix.EventTimeline;
|
||||||
|
|
||||||
var sdk = require('../../index');
|
var sdk = require('../../index');
|
||||||
|
import { _t } from '../../languageHandler';
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
var dis = require("../../dispatcher");
|
var dis = require("../../dispatcher");
|
||||||
var ObjectUtils = require('../../ObjectUtils');
|
var ObjectUtils = require('../../ObjectUtils');
|
||||||
var Modal = require("../../Modal");
|
var Modal = require("../../Modal");
|
||||||
var UserActivity = require("../../UserActivity");
|
var UserActivity = require("../../UserActivity");
|
||||||
var KeyCode = require('../../KeyCode');
|
var KeyCode = require('../../KeyCode');
|
||||||
|
import UserSettingsStore from '../../UserSettingsStore';
|
||||||
|
|
||||||
var PAGINATE_SIZE = 20;
|
var PAGINATE_SIZE = 20;
|
||||||
var INITIAL_SIZE = 20;
|
var INITIAL_SIZE = 20;
|
||||||
|
@ -102,9 +105,6 @@ var TimelinePanel = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
statics: {
|
statics: {
|
||||||
// a map from room id to read marker event ID
|
|
||||||
roomReadMarkerMap: {},
|
|
||||||
|
|
||||||
// a map from room id to read marker event timestamp
|
// a map from room id to read marker event timestamp
|
||||||
roomReadMarkerTsMap: {},
|
roomReadMarkerTsMap: {},
|
||||||
},
|
},
|
||||||
|
@ -121,11 +121,17 @@ var TimelinePanel = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
// XXX: we could track RM per TimelineSet rather than per Room.
|
// XXX: we could track RM per TimelineSet rather than per Room.
|
||||||
// but for now we just do it per room for simplicity.
|
// but for now we just do it per room for simplicity.
|
||||||
|
let initialReadMarker = null;
|
||||||
if (this.props.manageReadMarkers) {
|
if (this.props.manageReadMarkers) {
|
||||||
var initialReadMarker =
|
const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read');
|
||||||
TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId]
|
if (readmarker) {
|
||||||
|| this._getCurrentReadReceipt();
|
initialReadMarker = readmarker.getContent().event_id;
|
||||||
|
} else {
|
||||||
|
initialReadMarker = this._getCurrentReadReceipt();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
events: [],
|
events: [],
|
||||||
|
@ -166,13 +172,23 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
backPaginating: false,
|
backPaginating: false,
|
||||||
forwardPaginating: 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() {
|
componentWillMount: function() {
|
||||||
debuglog("TimelinePanel: mounting");
|
debuglog("TimelinePanel: mounting");
|
||||||
|
|
||||||
this.last_rr_sent_event_id = undefined;
|
this.lastRRSentEventId = undefined;
|
||||||
|
this.lastRMSentEventId = undefined;
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||||
|
@ -180,6 +196,8 @@ var TimelinePanel = React.createClass({
|
||||||
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
||||||
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
||||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||||
|
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
|
||||||
|
MatrixClientPeg.get().on("sync", this.onSync);
|
||||||
|
|
||||||
this._initTimeline(this.props);
|
this._initTimeline(this.props);
|
||||||
},
|
},
|
||||||
|
@ -247,6 +265,8 @@ var TimelinePanel = React.createClass({
|
||||||
client.removeListener("Room.redaction", this.onRoomRedaction);
|
client.removeListener("Room.redaction", this.onRoomRedaction);
|
||||||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||||
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
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]) {
|
if (!this.state[canPaginateKey]) {
|
||||||
debuglog("TimelinePanel: have given up", dir, "paginating this timeline");
|
debuglog("TimelinePanel: have given up", dir, "paginating this timeline");
|
||||||
return q(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!this._timelineWindow.canPaginate(dir)) {
|
if(!this._timelineWindow.canPaginate(dir)) {
|
||||||
debuglog("TimelinePanel: can't", dir, "paginate any further");
|
debuglog("TimelinePanel: can't", dir, "paginate any further");
|
||||||
this.setState({[canPaginateKey]: false});
|
this.setState({[canPaginateKey]: false});
|
||||||
return q(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
|
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
|
||||||
|
@ -327,9 +347,9 @@ var TimelinePanel = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessageListScroll: function() {
|
onMessageListScroll: function(e) {
|
||||||
if (this.props.onScroll) {
|
if (this.props.onScroll) {
|
||||||
this.props.onScroll();
|
this.props.onScroll(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.manageReadMarkers) {
|
if (this.props.manageReadMarkers) {
|
||||||
|
@ -414,6 +434,7 @@ var TimelinePanel = React.createClass({
|
||||||
} else if(lastEv && this.getReadMarkerPosition() === 0) {
|
} else if(lastEv && this.getReadMarkerPosition() === 0) {
|
||||||
// we know we're stuckAtBottom, so we can advance the RM
|
// we know we're stuckAtBottom, so we can advance the RM
|
||||||
// immediately, to save a later render cycle
|
// immediately, to save a later render cycle
|
||||||
|
|
||||||
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
|
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
|
||||||
updatedState.readMarkerVisible = false;
|
updatedState.readMarkerVisible = false;
|
||||||
updatedState.readMarkerEventId = lastEv.getId();
|
updatedState.readMarkerEventId = lastEv.getId();
|
||||||
|
@ -466,6 +487,25 @@ var TimelinePanel = React.createClass({
|
||||||
this._reloadEvents();
|
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() {
|
sendReadReceipt: function() {
|
||||||
if (!this.refs.messagePanel) return;
|
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
|
// This happens on user_activity_end which is delayed, and it's
|
||||||
// very possible have logged out within that timeframe, so check
|
// very possible have logged out within that timeframe, so check
|
||||||
// we still have a client.
|
// we still have a client.
|
||||||
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);
|
let shouldSendRR = true;
|
||||||
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
|
|
||||||
|
|
||||||
|
const currentRREventId = this._getCurrentReadReceipt(true);
|
||||||
|
const currentRREventIndex = this._indexForEventId(currentRREventId);
|
||||||
// We want to avoid sending out read receipts when we are looking at
|
// We want to avoid sending out read receipts when we are looking at
|
||||||
// events in the past which are before the latest RR.
|
// events in the past which are before the latest RR.
|
||||||
//
|
//
|
||||||
|
@ -491,26 +534,60 @@ var TimelinePanel = React.createClass({
|
||||||
// RRs) - but that is a bit of a niche case. It will sort itself out when
|
// RRs) - but that is a bit of a niche case. It will sort itself out when
|
||||||
// the user eventually hits the live timeline.
|
// the user eventually hits the live timeline.
|
||||||
//
|
//
|
||||||
if (currentReadUpToEventId && currentReadUpToEventIndex === null &&
|
if (currentRREventId && currentRREventIndex === null &&
|
||||||
this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||||
return;
|
shouldSendRR = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastReadEventIndex = this._getLastDisplayedEventIndex({
|
const lastReadEventIndex = this._getLastDisplayedEventIndex({
|
||||||
ignoreOwn: true
|
ignoreOwn: true,
|
||||||
});
|
});
|
||||||
if (lastReadEventIndex === null) return;
|
if (lastReadEventIndex === null) {
|
||||||
|
shouldSendRR = false;
|
||||||
|
}
|
||||||
|
let lastReadEvent = this.state.events[lastReadEventIndex];
|
||||||
|
shouldSendRR = shouldSendRR &&
|
||||||
|
// Only send a RR if the last read event is ahead in the timeline relative to
|
||||||
|
// the current RR event.
|
||||||
|
lastReadEventIndex > currentRREventIndex &&
|
||||||
|
// Only send a RR if the last RR set != the one we would send
|
||||||
|
this.lastRRSentEventId != lastReadEvent.getId();
|
||||||
|
|
||||||
var lastReadEvent = this.state.events[lastReadEventIndex];
|
// Only send a RM if the last RM sent != the one we would send
|
||||||
|
const shouldSendRM =
|
||||||
|
this.lastRMSentEventId != this.state.readMarkerEventId;
|
||||||
|
|
||||||
// we also remember the last read receipt we sent to avoid spamming the
|
// we also remember the last read receipt we sent to avoid spamming the
|
||||||
// same one at the server repeatedly
|
// same one at the server repeatedly
|
||||||
if (lastReadEventIndex > currentReadUpToEventIndex
|
if (shouldSendRR || shouldSendRM) {
|
||||||
&& this.last_rr_sent_event_id != lastReadEvent.getId()) {
|
if (shouldSendRR) {
|
||||||
this.last_rr_sent_event_id = lastReadEvent.getId();
|
this.lastRRSentEventId = lastReadEvent.getId();
|
||||||
MatrixClientPeg.get().sendReadReceipt(lastReadEvent).catch(() => {
|
} 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
|
// 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
|
// 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.
|
// 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.
|
// 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.length > 0) {
|
||||||
if (rmTs < this.state.events[0].getTs()) {
|
if (rmTs < this.state.events[0].getTs()) {
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -718,6 +795,19 @@ var TimelinePanel = React.createClass({
|
||||||
return null;
|
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.
|
* 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
|
// jump to the live timeline on ctrl-end, rather than the end of the
|
||||||
// timeline window.
|
// timeline window.
|
||||||
if (ev.ctrlKey && ev.keyCode == KeyCode.END) {
|
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
|
||||||
|
ev.keyCode == KeyCode.END)
|
||||||
|
{
|
||||||
this.jumpToLiveTimeline();
|
this.jumpToLiveTimeline();
|
||||||
} else {
|
} else {
|
||||||
this.refs.messagePanel.handleScrollKey(ev);
|
this.refs.messagePanel.handleScrollKey(ev);
|
||||||
|
@ -807,6 +899,9 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
var onError = (error) => {
|
var onError = (error) => {
|
||||||
this.setState({timelineLoading: false});
|
this.setState({timelineLoading: false});
|
||||||
|
console.error(
|
||||||
|
`Error loading timeline panel at ${eventId}: ${error}`,
|
||||||
|
);
|
||||||
var msg = error.message ? error.message : JSON.stringify(error);
|
var msg = error.message ? error.message : JSON.stringify(error);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
|
||||||
|
@ -825,14 +920,11 @@ var TimelinePanel = React.createClass({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
var message = "Tried to load a specific point in this room's timeline, but ";
|
var message = (error.errcode == 'M_FORBIDDEN')
|
||||||
if (error.errcode == 'M_FORBIDDEN') {
|
? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.")
|
||||||
message += "you do not have permission to view the message in question.";
|
: _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
|
||||||
} else {
|
|
||||||
message += "was unable to find it.";
|
|
||||||
}
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failed to load timeline position",
|
title: _t("Failed to load timeline position"),
|
||||||
description: message,
|
description: message,
|
||||||
onFinished: onFinished,
|
onFinished: onFinished,
|
||||||
});
|
});
|
||||||
|
@ -956,16 +1048,12 @@ var TimelinePanel = React.createClass({
|
||||||
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
|
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
|
||||||
var roomId = this.props.timelineSet.room.roomId;
|
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
|
// don't update the state (and cause a re-render) if there is
|
||||||
// no change to the RM.
|
// no change to the RM.
|
||||||
|
if (eventId === this.state.readMarkerEventId) {
|
||||||
return;
|
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
|
// in order to later figure out if the read marker is
|
||||||
// above or below the visible timeline, we stash the timestamp.
|
// above or below the visible timeline, we stash the timestamp.
|
||||||
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
|
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
|
||||||
|
@ -974,6 +1062,7 @@ var TimelinePanel = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do the local echo of the RM
|
||||||
// run the render cycle before calling the callback, so that
|
// run the render cycle before calling the callback, so that
|
||||||
// getReadMarkerPosition() returns the right thing.
|
// getReadMarkerPosition() returns the right thing.
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -1022,11 +1111,16 @@ var TimelinePanel = React.createClass({
|
||||||
// of paginating our way through the entire history of the room.
|
// of paginating our way through the entire history of the room.
|
||||||
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
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 (
|
return (
|
||||||
<MessagePanel ref="messagePanel"
|
<MessagePanel ref="messagePanel"
|
||||||
hidden={ this.props.hidden }
|
hidden={ this.props.hidden }
|
||||||
backPaginating={ this.state.backPaginating }
|
backPaginating={ this.state.backPaginating }
|
||||||
forwardPaginating={ this.state.forwardPaginating }
|
forwardPaginating={ forwardPaginating }
|
||||||
events={ this.state.events }
|
events={ this.state.events }
|
||||||
highlightedEventId={ this.props.highlightedEventId }
|
highlightedEventId={ this.props.highlightedEventId }
|
||||||
readMarkerEventId={ this.state.readMarkerEventId }
|
readMarkerEventId={ this.state.readMarkerEventId }
|
||||||
|
@ -1040,6 +1134,8 @@ var TimelinePanel = React.createClass({
|
||||||
onFillRequest={ this.onMessageListFillRequest }
|
onFillRequest={ this.onMessageListFillRequest }
|
||||||
onUnfillRequest={ this.onMessageListUnfillRequest }
|
onUnfillRequest={ this.onMessageListUnfillRequest }
|
||||||
opacity={ this.props.opacity }
|
opacity={ this.props.opacity }
|
||||||
|
isTwelveHour={ this.state.isTwelveHour }
|
||||||
|
alwaysShowTimestamps={ this.state.alwaysShowTimestamps }
|
||||||
className={ this.props.className }
|
className={ this.props.className }
|
||||||
tileShape={ this.props.tileShape }
|
tileShape={ this.props.tileShape }
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -18,6 +18,7 @@ var React = require('react');
|
||||||
var ContentMessages = require('../../ContentMessages');
|
var ContentMessages = require('../../ContentMessages');
|
||||||
var dis = require('../../dispatcher');
|
var dis = require('../../dispatcher');
|
||||||
var filesize = require('filesize');
|
var filesize = require('filesize');
|
||||||
|
import { _t } from '../../languageHandler';
|
||||||
|
|
||||||
module.exports = React.createClass({displayName: 'UploadBar',
|
module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -81,10 +82,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
uploadedSize = uploadedSize.replace(/ .*/, '');
|
uploadedSize = uploadedSize.replace(/ .*/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
var others;
|
// MUST use var name 'count' for pluralization to kick in
|
||||||
if (uploads.length > 1) {
|
var uploadText = _t("Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)});
|
||||||
others = ' and ' + (uploads.length - 1) + ' other' + (uploads.length > 2 ? 's' : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_UploadBar">
|
<div className="mx_UploadBar">
|
||||||
|
@ -98,7 +97,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
<div className="mx_UploadBar_uploadBytes">
|
<div className="mx_UploadBar_uploadBytes">
|
||||||
{ uploadedSize } / { totalSize }
|
{ uploadedSize } / { totalSize }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UploadBar_uploadFilename">Uploading {upload.fileName}{others}</div>
|
<div className="mx_UploadBar_uploadFilename">{uploadText}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
var Modal = require("../../../Modal");
|
var Modal = require("../../../Modal");
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
@ -54,7 +55,7 @@ module.exports = React.createClass({
|
||||||
progress: "sent_email"
|
progress: "sent_email"
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.showErrorDialog("Failed to send email: " + err.message);
|
this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
|
||||||
this.setState({
|
this.setState({
|
||||||
progress: null
|
progress: null
|
||||||
});
|
});
|
||||||
|
@ -78,30 +79,33 @@ module.exports = React.createClass({
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
if (!this.state.email) {
|
if (!this.state.email) {
|
||||||
this.showErrorDialog("The email address linked to your account must be entered.");
|
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
|
||||||
}
|
}
|
||||||
else if (!this.state.password || !this.state.password2) {
|
else if (!this.state.password || !this.state.password2) {
|
||||||
this.showErrorDialog("A new password must be entered.");
|
this.showErrorDialog(_t('A new password must be entered.'));
|
||||||
}
|
}
|
||||||
else if (this.state.password !== this.state.password2) {
|
else if (this.state.password !== this.state.password2) {
|
||||||
this.showErrorDialog("New passwords must match each other.");
|
this.showErrorDialog(_t('New passwords must match each other.'));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createDialog(QuestionDialog, {
|
||||||
title: "Warning",
|
title: _t('Warning!'),
|
||||||
description:
|
description:
|
||||||
<div>
|
<div>
|
||||||
Resetting password will currently reset any end-to-end encryption keys on all devices,
|
{ _t(
|
||||||
making encrypted chat history unreadable, unless you first export your room keys
|
'Resetting password will currently reset any ' +
|
||||||
and re-import them afterwards.
|
'end-to-end encryption keys on all devices, ' +
|
||||||
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">will be improved</a>.
|
'making encrypted chat history unreadable, ' +
|
||||||
|
'unless you first export your room keys and re-import ' +
|
||||||
|
'them afterwards. In future this will be improved.'
|
||||||
|
) }
|
||||||
</div>,
|
</div>,
|
||||||
button: "Continue",
|
button: _t('Continue'),
|
||||||
extraButtons: [
|
extraButtons: [
|
||||||
<button className="mx_Dialog_primary"
|
<button className="mx_Dialog_primary"
|
||||||
onClick={this._onExportE2eKeysClicked}>
|
onClick={this._onExportE2eKeysClicked}>
|
||||||
Export E2E room keys
|
{ _t('Export E2E room keys') }
|
||||||
</button>
|
</button>
|
||||||
],
|
],
|
||||||
onFinished: (confirmed) => {
|
onFinished: (confirmed) => {
|
||||||
|
@ -150,7 +154,7 @@ module.exports = React.createClass({
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: title,
|
title: title,
|
||||||
description: body
|
description: body,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -168,22 +172,20 @@ module.exports = React.createClass({
|
||||||
else if (this.state.progress === "sent_email") {
|
else if (this.state.progress === "sent_email") {
|
||||||
resetPasswordJsx = (
|
resetPasswordJsx = (
|
||||||
<div>
|
<div>
|
||||||
An email has been sent to {this.state.email}. Once you've followed
|
{ _t('An email has been sent to') } {this.state.email}. { _t('Once you've followed the link it contains, click below') }.
|
||||||
the link it contains, click below.
|
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||||
value="I have verified my email address" />
|
value={ _t('I have verified my email address') } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else if (this.state.progress === "complete") {
|
else if (this.state.progress === "complete") {
|
||||||
resetPasswordJsx = (
|
resetPasswordJsx = (
|
||||||
<div>
|
<div>
|
||||||
<p>Your password has been reset.</p>
|
<p>{ _t('Your password has been reset') }.</p>
|
||||||
<p>You have been logged out of all devices and will no longer receive push notifications.
|
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
|
||||||
To re-enable notifications, sign in again on each device.</p>
|
|
||||||
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||||
value="Return to login screen" />
|
value={ _t('Return to login screen') } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -191,7 +193,7 @@ module.exports = React.createClass({
|
||||||
resetPasswordJsx = (
|
resetPasswordJsx = (
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_Login_prompt">
|
<div className="mx_Login_prompt">
|
||||||
To reset your password, enter the email address linked to your account:
|
{ _t('To reset your password, enter the email address linked to your account') }:
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={this.onSubmitForm}>
|
<form onSubmit={this.onSubmitForm}>
|
||||||
|
@ -199,21 +201,21 @@ module.exports = React.createClass({
|
||||||
name="reset_email" // define a name so browser's password autofill gets less confused
|
name="reset_email" // define a name so browser's password autofill gets less confused
|
||||||
value={this.state.email}
|
value={this.state.email}
|
||||||
onChange={this.onInputChanged.bind(this, "email")}
|
onChange={this.onInputChanged.bind(this, "email")}
|
||||||
placeholder="Email address" autoFocus />
|
placeholder={ _t('Email address') } autoFocus />
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_field" ref="pass" type="password"
|
<input className="mx_Login_field" ref="pass" type="password"
|
||||||
name="reset_password"
|
name="reset_password"
|
||||||
value={this.state.password}
|
value={this.state.password}
|
||||||
onChange={this.onInputChanged.bind(this, "password")}
|
onChange={this.onInputChanged.bind(this, "password")}
|
||||||
placeholder="New password" />
|
placeholder={ _t('New password') } />
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_field" ref="pass" type="password"
|
<input className="mx_Login_field" ref="pass" type="password"
|
||||||
name="reset_password_confirm"
|
name="reset_password_confirm"
|
||||||
value={this.state.password2}
|
value={this.state.password2}
|
||||||
onChange={this.onInputChanged.bind(this, "password2")}
|
onChange={this.onInputChanged.bind(this, "password2")}
|
||||||
placeholder="Confirm your new password" />
|
placeholder={ _t('Confirm your new password') } />
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_submit" type="submit" value="Send Reset Email" />
|
<input className="mx_Login_submit" type="submit" value={ _t('Send Reset Email') } />
|
||||||
</form>
|
</form>
|
||||||
<ServerConfig ref="serverConfig"
|
<ServerConfig ref="serverConfig"
|
||||||
withToggleButton={true}
|
withToggleButton={true}
|
||||||
|
@ -227,10 +229,10 @@ module.exports = React.createClass({
|
||||||
<div className="mx_Login_error">
|
<div className="mx_Login_error">
|
||||||
</div>
|
</div>
|
||||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||||
Return to login
|
{_t('Return to login screen')}
|
||||||
</a>
|
</a>
|
||||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||||
Create a new account
|
{ _t('Create an account') }
|
||||||
</a>
|
</a>
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,13 +17,13 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
var ReactDOM = require('react-dom');
|
import { _t, _tJsx } from '../../../languageHandler';
|
||||||
var sdk = require('../../../index');
|
import sdk from '../../../index';
|
||||||
var Login = require("../../../Login");
|
import Login from '../../../Login';
|
||||||
var PasswordLogin = require("../../views/login/PasswordLogin");
|
|
||||||
var CasLogin = require("../../views/login/CasLogin");
|
// For validating phone numbers without country codes
|
||||||
var ServerConfig = require("../../views/login/ServerConfig");
|
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wire component which glues together login UI components and Login logic
|
* A wire component which glues together login UI components and Login logic
|
||||||
|
@ -67,13 +67,19 @@ module.exports = React.createClass({
|
||||||
username: "",
|
username: "",
|
||||||
phoneCountry: null,
|
phoneCountry: null,
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
|
currentFlow: "m.login.password",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
this._initLoginLogic();
|
this._initLoginLogic();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this._unmounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
|
@ -86,10 +92,36 @@ module.exports = React.createClass({
|
||||||
).then((data) => {
|
).then((data) => {
|
||||||
this.props.onLoggedIn(data);
|
this.props.onLoggedIn(data);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
this._setStateFromError(error, true);
|
if(this._unmounted) {
|
||||||
}).finally(() => {
|
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({
|
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();
|
}).done();
|
||||||
},
|
},
|
||||||
|
@ -109,7 +141,16 @@ module.exports = React.createClass({
|
||||||
this._loginLogic.loginAsGuest().then(function(data) {
|
this._loginLogic.loginAsGuest().then(function(data) {
|
||||||
self.props.onLoggedIn(data);
|
self.props.onLoggedIn(data);
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
self._setStateFromError(error, true);
|
let errorText;
|
||||||
|
if (error.httpStatus === 403) {
|
||||||
|
errorText = _t("Guest access is disabled on this Home Server.");
|
||||||
|
} else {
|
||||||
|
errorText = self._errorTextFromError(error);
|
||||||
|
}
|
||||||
|
self.setState({
|
||||||
|
errorText: errorText,
|
||||||
|
loginIncorrect: false,
|
||||||
|
});
|
||||||
}).finally(function() {
|
}).finally(function() {
|
||||||
self.setState({
|
self.setState({
|
||||||
busy: false
|
busy: false
|
||||||
|
@ -126,26 +167,31 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onPhoneNumberChanged: function(phoneNumber) {
|
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({
|
this.setState({
|
||||||
enteredHomeserverUrl: newHsUrl,
|
phoneNumber: phoneNumber,
|
||||||
errorText: null, // reset err messages
|
errorText: null,
|
||||||
}, function() {
|
|
||||||
self._initLoginLogic(newHsUrl);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onIsUrlChanged: function(newIsUrl) {
|
onServerConfigChange: function(config) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.setState({
|
let newState = {
|
||||||
enteredIdentityServerUrl: newIsUrl,
|
|
||||||
errorText: null, // reset err messages
|
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;
|
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({
|
this.setState({
|
||||||
enteredHomeserverUrl: hsUrl,
|
enteredHomeserverUrl: hsUrl,
|
||||||
enteredIdentityServerUrl: isUrl,
|
enteredIdentityServerUrl: isUrl,
|
||||||
busy: true,
|
busy: true,
|
||||||
loginIncorrect: false,
|
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() {
|
_getCurrentFlowStep: function() {
|
||||||
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
|
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
_setStateFromError: function(err, isLoginAttempt) {
|
|
||||||
this.setState({
|
|
||||||
errorText: this._errorTextFromError(err),
|
|
||||||
// https://matrix.org/jira/browse/SYN-744
|
|
||||||
loginIncorrect: isLoginAttempt && (err.httpStatus == 401 || err.httpStatus == 403)
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_errorTextFromError(err) {
|
_errorTextFromError(err) {
|
||||||
if (err.friendlyText) {
|
|
||||||
return err.friendlyText;
|
|
||||||
}
|
|
||||||
|
|
||||||
let errCode = err.errcode;
|
let errCode = err.errcode;
|
||||||
if (!errCode && err.httpStatus) {
|
if (!errCode && err.httpStatus) {
|
||||||
errCode = "HTTP " + err.httpStatus;
|
errCode = "HTTP " + err.httpStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
let errorText = "Error: Problem communicating with the given homeserver " +
|
let errorText = _t("Error: Problem communicating with the given homeserver.") +
|
||||||
(errCode ? " (" + errCode + ")" : "");
|
(errCode ? " (" + errCode + ")" : "");
|
||||||
|
|
||||||
if (err.cors === 'rejected') {
|
if (err.cors === 'rejected') {
|
||||||
if (window.location.protocol === 'https:' &&
|
if (window.location.protocol === 'https:' &&
|
||||||
(this.state.enteredHomeserverUrl.startsWith("http:") ||
|
(this.state.enteredHomeserverUrl.startsWith("http:") ||
|
||||||
!this.state.enteredHomeserverUrl.startsWith("http")))
|
!this.state.enteredHomeserverUrl.startsWith("http"))
|
||||||
{
|
) {
|
||||||
errorText = <span>
|
errorText = <span>
|
||||||
Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar.
|
{ _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||||
Either use HTTPS or <a href='https://www.google.com/search?&q=enable%20unsafe%20scripts'>enable unsafe scripts</a>
|
"Either use HTTPS or <a>enable unsafe scripts</a>.",
|
||||||
|
/<a>(.*?)<\/a>/,
|
||||||
|
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; }
|
||||||
|
)}
|
||||||
</span>;
|
</span>;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
errorText = <span>
|
errorText = <span>
|
||||||
Can't connect to homeserver - please check your connectivity and ensure
|
{ _tJsx("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
|
||||||
your <a href={ this.state.enteredHomeserverUrl }>homeserver's SSL certificate</a> is trusted.
|
/<a>(.*?)<\/a>/,
|
||||||
|
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; }
|
||||||
|
)}
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -231,6 +275,7 @@ module.exports = React.createClass({
|
||||||
componentForStep: function(step) {
|
componentForStep: function(step) {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 'm.login.password':
|
case 'm.login.password':
|
||||||
|
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
|
||||||
return (
|
return (
|
||||||
<PasswordLogin
|
<PasswordLogin
|
||||||
onSubmit={this.onPasswordLogin}
|
onSubmit={this.onPasswordLogin}
|
||||||
|
@ -245,6 +290,7 @@ module.exports = React.createClass({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'm.login.cas':
|
case 'm.login.cas':
|
||||||
|
const CasLogin = sdk.getComponent('login.CasLogin');
|
||||||
return (
|
return (
|
||||||
<CasLogin onSubmit={this.onCasLogin} />
|
<CasLogin onSubmit={this.onCasLogin} />
|
||||||
);
|
);
|
||||||
|
@ -254,24 +300,24 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
Sorry, this homeserver is using a login which is not
|
{ _t('Sorry, this homeserver is using a login which is not recognised ')}({step})
|
||||||
recognised ({step})
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
var LoginHeader = sdk.getComponent("login.LoginHeader");
|
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||||
var LoginFooter = sdk.getComponent("login.LoginFooter");
|
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||||
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||||
|
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||||
|
|
||||||
var loginAsGuestJsx;
|
var loginAsGuestJsx;
|
||||||
if (this.props.enableGuest) {
|
if (this.props.enableGuest) {
|
||||||
loginAsGuestJsx =
|
loginAsGuestJsx =
|
||||||
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
|
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
|
||||||
Login as guest
|
{ _t('Login as guest')}
|
||||||
</a>;
|
</a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,7 +325,7 @@ module.exports = React.createClass({
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
returnToAppJsx =
|
returnToAppJsx =
|
||||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||||
Return to app
|
{ _t('Return to app')}
|
||||||
</a>;
|
</a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,24 +334,23 @@ module.exports = React.createClass({
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
<LoginHeader />
|
<LoginHeader />
|
||||||
<div>
|
<div>
|
||||||
<h2>Sign in
|
<h2>{ _t('Sign in')}
|
||||||
{ loader }
|
{ loader }
|
||||||
</h2>
|
</h2>
|
||||||
{ this.componentForStep(this._getCurrentFlowStep()) }
|
{ this.componentForStep(this.state.currentFlow) }
|
||||||
<ServerConfig ref="serverConfig"
|
<ServerConfig ref="serverConfig"
|
||||||
withToggleButton={true}
|
withToggleButton={true}
|
||||||
customHsUrl={this.props.customHsUrl}
|
customHsUrl={this.props.customHsUrl}
|
||||||
customIsUrl={this.props.customIsUrl}
|
customIsUrl={this.props.customIsUrl}
|
||||||
defaultHsUrl={this.props.defaultHsUrl}
|
defaultHsUrl={this.props.defaultHsUrl}
|
||||||
defaultIsUrl={this.props.defaultIsUrl}
|
defaultIsUrl={this.props.defaultIsUrl}
|
||||||
onHsUrlChanged={this.onHsUrlChanged}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
onIsUrlChanged={this.onIsUrlChanged}
|
|
||||||
delayTimeMs={1000}/>
|
delayTimeMs={1000}/>
|
||||||
<div className="mx_Login_error">
|
<div className="mx_Login_error">
|
||||||
{ this.state.errorText }
|
{ this.state.errorText }
|
||||||
</div>
|
</div>
|
||||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||||
Create a new account
|
{ _t('Create an account')}
|
||||||
</a>
|
</a>
|
||||||
{ loginAsGuestJsx }
|
{ loginAsGuestJsx }
|
||||||
{ returnToAppJsx }
|
{ returnToAppJsx }
|
||||||
|
|
|
@ -16,9 +16,10 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
var sdk = require('../../../index');
|
import sdk from '../../../index';
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'PostRegistration',
|
displayName: 'PostRegistration',
|
||||||
|
@ -49,7 +50,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
self.setState({
|
self.setState({
|
||||||
errorString: "Failed to fetch avatar URL",
|
errorString: _t("Failed to fetch avatar URL"),
|
||||||
busy: false
|
busy: false
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -64,12 +65,12 @@ module.exports = React.createClass({
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
<LoginHeader />
|
<LoginHeader />
|
||||||
<div className="mx_Login_profile">
|
<div className="mx_Login_profile">
|
||||||
Set a display name:
|
{ _t('Set a display name:') }
|
||||||
<ChangeDisplayName />
|
<ChangeDisplayName />
|
||||||
Upload an avatar:
|
{ _t('Upload an avatar:') }
|
||||||
<ChangeAvatar
|
<ChangeAvatar
|
||||||
initialAvatarUrl={this.state.avatarUrl} />
|
initialAvatarUrl={this.state.avatarUrl} />
|
||||||
<button onClick={this.props.onComplete}>Continue</button>
|
<button onClick={this.props.onComplete}>{ _t('Continue') }</button>
|
||||||
{this.state.errorString}
|
{this.state.errorString}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,16 +17,15 @@ limitations under the License.
|
||||||
|
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
|
|
||||||
import q from 'q';
|
import Promise from 'bluebird';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
|
||||||
import ServerConfig from '../../views/login/ServerConfig';
|
import ServerConfig from '../../views/login/ServerConfig';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import RegistrationForm from '../../views/login/RegistrationForm';
|
import RegistrationForm from '../../views/login/RegistrationForm';
|
||||||
import CaptchaForm from '../../views/login/CaptchaForm';
|
|
||||||
import RtsClient from '../../../RtsClient';
|
import RtsClient from '../../../RtsClient';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
const MIN_PASSWORD_LENGTH = 6;
|
const MIN_PASSWORD_LENGTH = 6;
|
||||||
|
|
||||||
|
@ -46,8 +45,6 @@ module.exports = React.createClass({
|
||||||
brand: React.PropTypes.string,
|
brand: React.PropTypes.string,
|
||||||
email: React.PropTypes.string,
|
email: React.PropTypes.string,
|
||||||
referrer: React.PropTypes.string,
|
referrer: React.PropTypes.string,
|
||||||
username: React.PropTypes.string,
|
|
||||||
guestAccessToken: React.PropTypes.string,
|
|
||||||
teamServerConfig: React.PropTypes.shape({
|
teamServerConfig: React.PropTypes.shape({
|
||||||
// Email address to request new teams
|
// Email address to request new teams
|
||||||
supportEmail: React.PropTypes.string.isRequired,
|
supportEmail: React.PropTypes.string.isRequired,
|
||||||
|
@ -98,7 +95,7 @@ module.exports = React.createClass({
|
||||||
this.props.teamServerConfig.teamServerURL &&
|
this.props.teamServerConfig.teamServerURL &&
|
||||||
!this._rtsClient
|
!this._rtsClient
|
||||||
) {
|
) {
|
||||||
this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL);
|
this._rtsClient = this.props.rtsClient || new RtsClient(this.props.teamServerConfig.teamServerURL);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
teamServerBusy: true,
|
teamServerBusy: true,
|
||||||
|
@ -123,18 +120,17 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onHsUrlChanged: function(newHsUrl) {
|
onServerConfigChange: function(config) {
|
||||||
this.setState({
|
let newState = {};
|
||||||
hsUrl: newHsUrl,
|
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() {
|
_replaceClient: function() {
|
||||||
|
@ -163,7 +159,7 @@ module.exports = React.createClass({
|
||||||
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
|
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||||
}
|
}
|
||||||
if (!msisdn_available) {
|
if (!msisdn_available) {
|
||||||
msg = "This server does not support authentication with a phone number";
|
msg = _t('This server does not support authentication with a phone number.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -184,7 +180,7 @@ module.exports = React.createClass({
|
||||||
// will just nop. The point of this being we might not have the email address
|
// 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
|
// that the user registered with at this stage (depending on whether this
|
||||||
// is the client they initiated registration).
|
// is the client they initiated registration).
|
||||||
let trackPromise = q(null);
|
let trackPromise = Promise.resolve(null);
|
||||||
if (this._rtsClient && extra.emailSid) {
|
if (this._rtsClient && extra.emailSid) {
|
||||||
// Track referral if this.props.referrer set, get team_token in order to
|
// Track referral if this.props.referrer set, get team_token in order to
|
||||||
// retrieve team config and see welcome page etc.
|
// retrieve team config and see welcome page etc.
|
||||||
|
@ -222,30 +218,29 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
trackPromise.then((teamToken) => {
|
trackPromise.then((teamToken) => {
|
||||||
console.info('Team token promise',teamToken);
|
return this.props.onLoggedIn({
|
||||||
this.props.onLoggedIn({
|
|
||||||
userId: response.user_id,
|
userId: response.user_id,
|
||||||
deviceId: response.device_id,
|
deviceId: response.device_id,
|
||||||
homeserverUrl: this._matrixClient.getHomeserverUrl(),
|
homeserverUrl: this._matrixClient.getHomeserverUrl(),
|
||||||
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
|
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
|
||||||
accessToken: response.access_token
|
accessToken: response.access_token
|
||||||
}, teamToken);
|
}, teamToken);
|
||||||
}).then(() => {
|
}).then((cli) => {
|
||||||
return this._setupPushers();
|
return this._setupPushers(cli);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_setupPushers: function() {
|
_setupPushers: function(matrixClient) {
|
||||||
if (!this.props.brand) {
|
if (!this.props.brand) {
|
||||||
return q();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return MatrixClientPeg.get().getPushers().then((resp)=>{
|
return matrixClient.getPushers().then((resp)=>{
|
||||||
const pushers = resp.pushers;
|
const pushers = resp.pushers;
|
||||||
for (let i = 0; i < pushers.length; ++i) {
|
for (let i = 0; i < pushers.length; ++i) {
|
||||||
if (pushers[i].kind == 'email') {
|
if (pushers[i].kind == 'email') {
|
||||||
const emailPusher = pushers[i];
|
const emailPusher = pushers[i];
|
||||||
emailPusher.data = { brand: this.props.brand };
|
emailPusher.data = { brand: this.props.brand };
|
||||||
MatrixClientPeg.get().setPusher(emailPusher).done(() => {
|
matrixClient.setPusher(emailPusher).done(() => {
|
||||||
console.log("Set email branding to " + this.props.brand);
|
console.log("Set email branding to " + this.props.brand);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
console.error("Couldn't set email branding: " + error);
|
console.error("Couldn't set email branding: " + error);
|
||||||
|
@ -261,29 +256,29 @@ module.exports = React.createClass({
|
||||||
var errMsg;
|
var errMsg;
|
||||||
switch (errCode) {
|
switch (errCode) {
|
||||||
case "RegistrationForm.ERR_PASSWORD_MISSING":
|
case "RegistrationForm.ERR_PASSWORD_MISSING":
|
||||||
errMsg = "Missing password.";
|
errMsg = _t('Missing password.');
|
||||||
break;
|
break;
|
||||||
case "RegistrationForm.ERR_PASSWORD_MISMATCH":
|
case "RegistrationForm.ERR_PASSWORD_MISMATCH":
|
||||||
errMsg = "Passwords don't match.";
|
errMsg = _t('Passwords don\'t match.');
|
||||||
break;
|
break;
|
||||||
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
||||||
errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`;
|
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH: MIN_PASSWORD_LENGTH});
|
||||||
break;
|
break;
|
||||||
case "RegistrationForm.ERR_EMAIL_INVALID":
|
case "RegistrationForm.ERR_EMAIL_INVALID":
|
||||||
errMsg = "This doesn't look like a valid email address";
|
errMsg = _t('This doesn\'t look like a valid email address.');
|
||||||
break;
|
break;
|
||||||
case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
|
case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
|
||||||
errMsg = "This doesn't look like a valid phone number";
|
errMsg = _t('This doesn\'t look like a valid phone number.');
|
||||||
break;
|
break;
|
||||||
case "RegistrationForm.ERR_USERNAME_INVALID":
|
case "RegistrationForm.ERR_USERNAME_INVALID":
|
||||||
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
|
errMsg = _t('User names may only contain letters, numbers, dots, hyphens and underscores.');
|
||||||
break;
|
break;
|
||||||
case "RegistrationForm.ERR_USERNAME_BLANK":
|
case "RegistrationForm.ERR_USERNAME_BLANK":
|
||||||
errMsg = "You need to enter a user name";
|
errMsg = _t('You need to enter a user name.');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.error("Unknown error code: %s", errCode);
|
console.error("Unknown error code: %s", errCode);
|
||||||
errMsg = "An unknown error occurred.";
|
errMsg = _t('An unknown error occurred.');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -298,17 +293,6 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_makeRegisterRequest: function(auth) {
|
_makeRegisterRequest: function(auth) {
|
||||||
let guestAccessToken = this.props.guestAccessToken;
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.state.formVals.username !== this.props.username ||
|
|
||||||
this.state.hsUrl != this.props.defaultHsUrl
|
|
||||||
) {
|
|
||||||
// don't try to upgrade if we changed our username
|
|
||||||
// or are registering on a different HS
|
|
||||||
guestAccessToken = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only send the bind params if we're sending username / pw params
|
// Only send the bind params if we're sending username / pw params
|
||||||
// (Since we need to send no params at all to use the ones saved in the
|
// (Since we need to send no params at all to use the ones saved in the
|
||||||
// session).
|
// session).
|
||||||
|
@ -323,7 +307,7 @@ module.exports = React.createClass({
|
||||||
undefined, // session id: included in the auth dict already
|
undefined, // session id: included in the auth dict already
|
||||||
auth,
|
auth,
|
||||||
bindThreepids,
|
bindThreepids,
|
||||||
guestAccessToken,
|
null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -360,10 +344,6 @@ module.exports = React.createClass({
|
||||||
} else if (this.state.busy || this.state.teamServerBusy) {
|
} else if (this.state.busy || this.state.teamServerBusy) {
|
||||||
registerBody = <Spinner />;
|
registerBody = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
let guestUsername = this.props.username;
|
|
||||||
if (this.state.hsUrl != this.props.defaultHsUrl) {
|
|
||||||
guestUsername = null;
|
|
||||||
}
|
|
||||||
let errorSection;
|
let errorSection;
|
||||||
if (this.state.errorText) {
|
if (this.state.errorText) {
|
||||||
errorSection = <div className="mx_Login_error">{this.state.errorText}</div>;
|
errorSection = <div className="mx_Login_error">{this.state.errorText}</div>;
|
||||||
|
@ -377,7 +357,6 @@ module.exports = React.createClass({
|
||||||
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||||
defaultPassword={this.state.formVals.password}
|
defaultPassword={this.state.formVals.password}
|
||||||
teamsConfig={this.state.teamsConfig}
|
teamsConfig={this.state.teamsConfig}
|
||||||
guestUsername={guestUsername}
|
|
||||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||||
onError={this.onFormValidationFailed}
|
onError={this.onFormValidationFailed}
|
||||||
onRegisterClick={this.onFormSubmit}
|
onRegisterClick={this.onFormSubmit}
|
||||||
|
@ -390,8 +369,7 @@ module.exports = React.createClass({
|
||||||
customIsUrl={this.props.customIsUrl}
|
customIsUrl={this.props.customIsUrl}
|
||||||
defaultHsUrl={this.props.defaultHsUrl}
|
defaultHsUrl={this.props.defaultHsUrl}
|
||||||
defaultIsUrl={this.props.defaultIsUrl}
|
defaultIsUrl={this.props.defaultIsUrl}
|
||||||
onHsUrlChanged={this.onHsUrlChanged}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
onIsUrlChanged={this.onIsUrlChanged}
|
|
||||||
delayTimeMs={1000}
|
delayTimeMs={1000}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -402,7 +380,7 @@ module.exports = React.createClass({
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
returnToAppJsx = (
|
returnToAppJsx = (
|
||||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||||
Return to app
|
{_t('Return to app')}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -415,10 +393,10 @@ module.exports = React.createClass({
|
||||||
this.state.teamSelected.domain + "/icon.png" :
|
this.state.teamSelected.domain + "/icon.png" :
|
||||||
null}
|
null}
|
||||||
/>
|
/>
|
||||||
<h2>Create an account</h2>
|
<h2>{_t('Create an account')}</h2>
|
||||||
{registerBody}
|
{registerBody}
|
||||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||||
I already have an account
|
{_t('I already have an account')}
|
||||||
</a>
|
</a>
|
||||||
{returnToAppJsx}
|
{returnToAppJsx}
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
|
|
|
@ -32,6 +32,7 @@ module.exports = React.createClass({
|
||||||
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
|
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||||
width: React.PropTypes.number,
|
width: React.PropTypes.number,
|
||||||
height: React.PropTypes.number,
|
height: React.PropTypes.number,
|
||||||
|
// XXX resizeMethod not actually used.
|
||||||
resizeMethod: React.PropTypes.string,
|
resizeMethod: React.PropTypes.string,
|
||||||
defaultToInitialLetter: React.PropTypes.bool // true to add default url
|
defaultToInitialLetter: React.PropTypes.bool // true to add default url
|
||||||
},
|
},
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue