Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into rxl881/parallelshell
This commit is contained in:
commit
a49eabda4c
253 changed files with 13210 additions and 9039 deletions
|
@ -1,23 +1,17 @@
|
||||||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
||||||
|
|
||||||
src/async-components/views/dialogs/EncryptedEventDialog.js
|
|
||||||
src/autocomplete/AutocompleteProvider.js
|
src/autocomplete/AutocompleteProvider.js
|
||||||
src/autocomplete/Autocompleter.js
|
src/autocomplete/Autocompleter.js
|
||||||
src/autocomplete/Components.js
|
|
||||||
src/autocomplete/DuckDuckGoProvider.js
|
|
||||||
src/autocomplete/EmojiProvider.js
|
src/autocomplete/EmojiProvider.js
|
||||||
src/autocomplete/RoomProvider.js
|
|
||||||
src/autocomplete/UserProvider.js
|
src/autocomplete/UserProvider.js
|
||||||
src/CallHandler.js
|
src/CallHandler.js
|
||||||
src/component-index.js
|
src/component-index.js
|
||||||
src/components/structures/ContextualMenu.js
|
src/components/structures/ContextualMenu.js
|
||||||
src/components/structures/CreateRoom.js
|
src/components/structures/CreateRoom.js
|
||||||
src/components/structures/FilePanel.js
|
src/components/structures/FilePanel.js
|
||||||
src/components/structures/InteractiveAuth.js
|
|
||||||
src/components/structures/LoggedInView.js
|
src/components/structures/LoggedInView.js
|
||||||
src/components/structures/login/ForgotPassword.js
|
src/components/structures/login/ForgotPassword.js
|
||||||
src/components/structures/login/Login.js
|
src/components/structures/login/Login.js
|
||||||
src/components/structures/login/PostRegistration.js
|
|
||||||
src/components/structures/login/Registration.js
|
src/components/structures/login/Registration.js
|
||||||
src/components/structures/MessagePanel.js
|
src/components/structures/MessagePanel.js
|
||||||
src/components/structures/NotificationPanel.js
|
src/components/structures/NotificationPanel.js
|
||||||
|
@ -28,53 +22,32 @@ src/components/structures/TimelinePanel.js
|
||||||
src/components/structures/UploadBar.js
|
src/components/structures/UploadBar.js
|
||||||
src/components/views/avatars/BaseAvatar.js
|
src/components/views/avatars/BaseAvatar.js
|
||||||
src/components/views/avatars/MemberAvatar.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/create_room/RoomAlias.js
|
||||||
src/components/views/dialogs/ChatCreateOrReuseDialog.js
|
src/components/views/dialogs/ChatCreateOrReuseDialog.js
|
||||||
src/components/views/dialogs/DeactivateAccountDialog.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/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/AddressSelector.js
|
||||||
src/components/views/elements/AddressTile.js
|
|
||||||
src/components/views/elements/CreateRoomButton.js
|
src/components/views/elements/CreateRoomButton.js
|
||||||
src/components/views/elements/DeviceVerifyButtons.js
|
src/components/views/elements/DeviceVerifyButtons.js
|
||||||
src/components/views/elements/DirectorySearchBox.js
|
src/components/views/elements/DirectorySearchBox.js
|
||||||
src/components/views/elements/Dropdown.js
|
|
||||||
src/components/views/elements/EditableText.js
|
src/components/views/elements/EditableText.js
|
||||||
src/components/views/elements/EditableTextContainer.js
|
|
||||||
src/components/views/elements/HomeButton.js
|
src/components/views/elements/HomeButton.js
|
||||||
src/components/views/elements/LanguageDropdown.js
|
|
||||||
src/components/views/elements/MemberEventListSummary.js
|
src/components/views/elements/MemberEventListSummary.js
|
||||||
src/components/views/elements/PowerSelector.js
|
src/components/views/elements/PowerSelector.js
|
||||||
src/components/views/elements/ProgressBar.js
|
|
||||||
src/components/views/elements/RoomDirectoryButton.js
|
src/components/views/elements/RoomDirectoryButton.js
|
||||||
src/components/views/elements/SettingsButton.js
|
src/components/views/elements/SettingsButton.js
|
||||||
src/components/views/elements/StartChatButton.js
|
src/components/views/elements/StartChatButton.js
|
||||||
src/components/views/elements/TintableSvg.js
|
src/components/views/elements/TintableSvg.js
|
||||||
src/components/views/elements/TruncatedList.js
|
|
||||||
src/components/views/elements/UserSelector.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/CountryDropdown.js
|
||||||
src/components/views/login/CustomServerDialog.js
|
|
||||||
src/components/views/login/InteractiveAuthEntryComponents.js
|
src/components/views/login/InteractiveAuthEntryComponents.js
|
||||||
src/components/views/login/LoginHeader.js
|
|
||||||
src/components/views/login/PasswordLogin.js
|
src/components/views/login/PasswordLogin.js
|
||||||
src/components/views/login/RegistrationForm.js
|
src/components/views/login/RegistrationForm.js
|
||||||
src/components/views/login/ServerConfig.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/MFileBody.js
|
||||||
src/components/views/messages/MImageBody.js
|
src/components/views/messages/MImageBody.js
|
||||||
src/components/views/messages/MVideoBody.js
|
|
||||||
src/components/views/messages/RoomAvatarEvent.js
|
src/components/views/messages/RoomAvatarEvent.js
|
||||||
src/components/views/messages/TextualBody.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/AliasSettings.js
|
||||||
src/components/views/room_settings/ColorSettings.js
|
src/components/views/room_settings/ColorSettings.js
|
||||||
src/components/views/room_settings/UrlPreviewSettings.js
|
src/components/views/room_settings/UrlPreviewSettings.js
|
||||||
|
@ -89,18 +62,13 @@ src/components/views/rooms/MemberList.js
|
||||||
src/components/views/rooms/MemberTile.js
|
src/components/views/rooms/MemberTile.js
|
||||||
src/components/views/rooms/MessageComposer.js
|
src/components/views/rooms/MessageComposer.js
|
||||||
src/components/views/rooms/MessageComposerInput.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/ReadReceiptMarker.js
|
||||||
src/components/views/rooms/RoomList.js
|
src/components/views/rooms/RoomList.js
|
||||||
src/components/views/rooms/RoomNameEditor.js
|
|
||||||
src/components/views/rooms/RoomPreviewBar.js
|
src/components/views/rooms/RoomPreviewBar.js
|
||||||
src/components/views/rooms/RoomSettings.js
|
src/components/views/rooms/RoomSettings.js
|
||||||
src/components/views/rooms/RoomTile.js
|
src/components/views/rooms/RoomTile.js
|
||||||
src/components/views/rooms/RoomTopicEditor.js
|
|
||||||
src/components/views/rooms/SearchableEntityList.js
|
src/components/views/rooms/SearchableEntityList.js
|
||||||
src/components/views/rooms/SearchResultTile.js
|
src/components/views/rooms/SearchResultTile.js
|
||||||
src/components/views/rooms/TabCompleteBar.js
|
|
||||||
src/components/views/rooms/TopUnreadMessagesBar.js
|
src/components/views/rooms/TopUnreadMessagesBar.js
|
||||||
src/components/views/rooms/UserTile.js
|
src/components/views/rooms/UserTile.js
|
||||||
src/components/views/settings/AddPhoneNumber.js
|
src/components/views/settings/AddPhoneNumber.js
|
||||||
|
@ -108,8 +76,6 @@ src/components/views/settings/ChangeAvatar.js
|
||||||
src/components/views/settings/ChangeDisplayName.js
|
src/components/views/settings/ChangeDisplayName.js
|
||||||
src/components/views/settings/ChangePassword.js
|
src/components/views/settings/ChangePassword.js
|
||||||
src/components/views/settings/DevicesPanel.js
|
src/components/views/settings/DevicesPanel.js
|
||||||
src/components/views/settings/DevicesPanelEntry.js
|
|
||||||
src/components/views/settings/EnableNotificationsButton.js
|
|
||||||
src/ContentMessages.js
|
src/ContentMessages.js
|
||||||
src/HtmlUtils.js
|
src/HtmlUtils.js
|
||||||
src/ImageUtils.js
|
src/ImageUtils.js
|
||||||
|
@ -127,10 +93,6 @@ src/RichText.js
|
||||||
src/Roles.js
|
src/Roles.js
|
||||||
src/Rooms.js
|
src/Rooms.js
|
||||||
src/ScalarAuthClient.js
|
src/ScalarAuthClient.js
|
||||||
src/ScalarMessaging.js
|
|
||||||
src/TabComplete.js
|
|
||||||
src/TabCompleteEntries.js
|
|
||||||
src/TextForEvent.js
|
|
||||||
src/Tinter.js
|
src/Tinter.js
|
||||||
src/UiEffects.js
|
src/UiEffects.js
|
||||||
src/Unread.js
|
src/Unread.js
|
||||||
|
@ -142,18 +104,14 @@ src/utils/Receipt.js
|
||||||
src/Velociraptor.js
|
src/Velociraptor.js
|
||||||
src/VelocityBounce.js
|
src/VelocityBounce.js
|
||||||
src/WhoIsTyping.js
|
src/WhoIsTyping.js
|
||||||
src/wrappers/WithMatrixClient.js
|
src/wrappers/withMatrixClient.js
|
||||||
test/all-tests.js
|
|
||||||
test/components/structures/login/Registration-test.js
|
test/components/structures/login/Registration-test.js
|
||||||
test/components/structures/MessagePanel-test.js
|
test/components/structures/MessagePanel-test.js
|
||||||
test/components/structures/ScrollPanel-test.js
|
test/components/structures/ScrollPanel-test.js
|
||||||
test/components/structures/TimelinePanel-test.js
|
test/components/structures/TimelinePanel-test.js
|
||||||
test/components/stub-component.js
|
|
||||||
test/components/views/dialogs/InteractiveAuthDialog-test.js
|
test/components/views/dialogs/InteractiveAuthDialog-test.js
|
||||||
test/components/views/elements/MemberEventListSummary-test.js
|
test/components/views/elements/MemberEventListSummary-test.js
|
||||||
test/components/views/login/RegistrationForm-test.js
|
test/components/views/login/RegistrationForm-test.js
|
||||||
test/components/views/rooms/MessageComposerInput-test.js
|
test/components/views/rooms/MessageComposerInput-test.js
|
||||||
test/mock-clock.js
|
test/mock-clock.js
|
||||||
test/skinned-sdk.js
|
|
||||||
test/stores/RoomViewStore-test.js
|
test/stores/RoomViewStore-test.js
|
||||||
test/test-utils.js
|
|
||||||
|
|
13
.eslintrc.js
13
.eslintrc.js
|
@ -40,6 +40,19 @@ module.exports = {
|
||||||
}],
|
}],
|
||||||
"react/jsx-key": ["error"],
|
"react/jsx-key": ["error"],
|
||||||
|
|
||||||
|
// Assert no spacing in JSX curly brackets
|
||||||
|
// <Element prop={ consideredError} prop={notConsideredError} />
|
||||||
|
//
|
||||||
|
// https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/docs/rules/jsx-curly-spacing.md
|
||||||
|
"react/jsx-curly-spacing": ["error", {"when": "never", "children": {"when": "always"}}],
|
||||||
|
|
||||||
|
// Assert spacing before self-closing JSX tags, and no spacing before or
|
||||||
|
// after the closing slash, and no spacing after the opening bracket of
|
||||||
|
// the opening tag or closing tag.
|
||||||
|
//
|
||||||
|
// https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/docs/rules/jsx-tag-spacing.md
|
||||||
|
"react/jsx-tag-spacing": ["error"],
|
||||||
|
|
||||||
/** flowtype **/
|
/** flowtype **/
|
||||||
"flowtype/require-parameter-type": ["warn", {
|
"flowtype/require-parameter-type": ["warn", {
|
||||||
"excludeArrowFunctions": true,
|
"excludeArrowFunctions": true,
|
||||||
|
|
310
CHANGELOG.md
310
CHANGELOG.md
|
@ -1,3 +1,313 @@
|
||||||
|
Changes in [0.10.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7) (2017-10-16)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7-rc.3...v0.10.7)
|
||||||
|
|
||||||
|
* Update to latest js-sdk
|
||||||
|
|
||||||
|
Changes in [0.10.7-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7-rc.3) (2017-10-13)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7-rc.2...v0.10.7-rc.3)
|
||||||
|
|
||||||
|
* Fix the enableLabs flag, again
|
||||||
|
[\#1474](https://github.com/matrix-org/matrix-react-sdk/pull/1474)
|
||||||
|
|
||||||
|
Changes in [0.10.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7-rc.2) (2017-10-13)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7-rc.1...v0.10.7-rc.2)
|
||||||
|
|
||||||
|
* Honour the (now legacy) enableLabs flag
|
||||||
|
[\#1473](https://github.com/matrix-org/matrix-react-sdk/pull/1473)
|
||||||
|
* Don't show labs features by default
|
||||||
|
[\#1472](https://github.com/matrix-org/matrix-react-sdk/pull/1472)
|
||||||
|
* Make features disabled by default
|
||||||
|
[\#1470](https://github.com/matrix-org/matrix-react-sdk/pull/1470)
|
||||||
|
|
||||||
|
Changes in [0.10.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7-rc.1) (2017-10-13)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.6...v0.10.7-rc.1)
|
||||||
|
|
||||||
|
* Add warm fuzzy dialog for inviting users to a group
|
||||||
|
[\#1459](https://github.com/matrix-org/matrix-react-sdk/pull/1459)
|
||||||
|
* enable/disable features in config.json
|
||||||
|
[\#1468](https://github.com/matrix-org/matrix-react-sdk/pull/1468)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1469](https://github.com/matrix-org/matrix-react-sdk/pull/1469)
|
||||||
|
* Don't send RR or RM when peeking at a room
|
||||||
|
[\#1463](https://github.com/matrix-org/matrix-react-sdk/pull/1463)
|
||||||
|
* Fix bug that inserted emoji when typing
|
||||||
|
[\#1467](https://github.com/matrix-org/matrix-react-sdk/pull/1467)
|
||||||
|
* Ignore VS16 char in RTE
|
||||||
|
[\#1458](https://github.com/matrix-org/matrix-react-sdk/pull/1458)
|
||||||
|
* Show failures when sending messages
|
||||||
|
[\#1460](https://github.com/matrix-org/matrix-react-sdk/pull/1460)
|
||||||
|
* Run eslint --fix
|
||||||
|
[\#1461](https://github.com/matrix-org/matrix-react-sdk/pull/1461)
|
||||||
|
* Show who banned the user on hover
|
||||||
|
[\#1441](https://github.com/matrix-org/matrix-react-sdk/pull/1441)
|
||||||
|
* Enhancements to room power level settings
|
||||||
|
[\#1440](https://github.com/matrix-org/matrix-react-sdk/pull/1440)
|
||||||
|
* Added TextInputWithCheckbox dialog
|
||||||
|
[\#868](https://github.com/matrix-org/matrix-react-sdk/pull/868)
|
||||||
|
* Make it clearer which HS you're logging into
|
||||||
|
[\#1456](https://github.com/matrix-org/matrix-react-sdk/pull/1456)
|
||||||
|
* Remove redundant stale onKeyDown
|
||||||
|
[\#1451](https://github.com/matrix-org/matrix-react-sdk/pull/1451)
|
||||||
|
* Only allow event state event handlers on state events
|
||||||
|
[\#1453](https://github.com/matrix-org/matrix-react-sdk/pull/1453)
|
||||||
|
* Modify the group store to include group rooms
|
||||||
|
[\#1452](https://github.com/matrix-org/matrix-react-sdk/pull/1452)
|
||||||
|
* Factor-out GroupStore and create GroupStoreCache
|
||||||
|
[\#1449](https://github.com/matrix-org/matrix-react-sdk/pull/1449)
|
||||||
|
* Put related groups UI behind groups labs flag
|
||||||
|
[\#1448](https://github.com/matrix-org/matrix-react-sdk/pull/1448)
|
||||||
|
* Restrict Flair in the timeline to related groups of the room
|
||||||
|
[\#1447](https://github.com/matrix-org/matrix-react-sdk/pull/1447)
|
||||||
|
* Implement UI for editing related groups of a room
|
||||||
|
[\#1446](https://github.com/matrix-org/matrix-react-sdk/pull/1446)
|
||||||
|
* Fix a couple of bugs with EditableItemList
|
||||||
|
[\#1445](https://github.com/matrix-org/matrix-react-sdk/pull/1445)
|
||||||
|
* Factor out EditableItemList from AliasSettings
|
||||||
|
[\#1444](https://github.com/matrix-org/matrix-react-sdk/pull/1444)
|
||||||
|
* Add dummy translation function to mark translatable strings
|
||||||
|
[\#1421](https://github.com/matrix-org/matrix-react-sdk/pull/1421)
|
||||||
|
* Implement button to remove a room from a group
|
||||||
|
[\#1438](https://github.com/matrix-org/matrix-react-sdk/pull/1438)
|
||||||
|
* Fix showing 3pid invites in member list
|
||||||
|
[\#1443](https://github.com/matrix-org/matrix-react-sdk/pull/1443)
|
||||||
|
* Add button to get to MyGroups (view_my_groups or path #/groups)
|
||||||
|
[\#1435](https://github.com/matrix-org/matrix-react-sdk/pull/1435)
|
||||||
|
* Add eslint rule to disallow spaces inside of curly braces
|
||||||
|
[\#1436](https://github.com/matrix-org/matrix-react-sdk/pull/1436)
|
||||||
|
* Fix ability to invite existing mx users
|
||||||
|
[\#1437](https://github.com/matrix-org/matrix-react-sdk/pull/1437)
|
||||||
|
* Construct address picker message using provided `validAddressTypes`
|
||||||
|
[\#1434](https://github.com/matrix-org/matrix-react-sdk/pull/1434)
|
||||||
|
* Fix GroupView summary rooms displaying without avatars
|
||||||
|
[\#1433](https://github.com/matrix-org/matrix-react-sdk/pull/1433)
|
||||||
|
* Implement adding rooms to a group (or group summary) by room ID
|
||||||
|
[\#1432](https://github.com/matrix-org/matrix-react-sdk/pull/1432)
|
||||||
|
* Give flair avatars a tooltip = the group ID
|
||||||
|
[\#1431](https://github.com/matrix-org/matrix-react-sdk/pull/1431)
|
||||||
|
* Fix ability to feature self in a group summary
|
||||||
|
[\#1430](https://github.com/matrix-org/matrix-react-sdk/pull/1430)
|
||||||
|
* Implement "Add room to group" feature
|
||||||
|
[\#1429](https://github.com/matrix-org/matrix-react-sdk/pull/1429)
|
||||||
|
* Fix group membership publicity
|
||||||
|
[\#1428](https://github.com/matrix-org/matrix-react-sdk/pull/1428)
|
||||||
|
* Add support for Jitsi screensharing in electron app
|
||||||
|
[\#1355](https://github.com/matrix-org/matrix-react-sdk/pull/1355)
|
||||||
|
* Delint and DRY TextForEvent
|
||||||
|
[\#1424](https://github.com/matrix-org/matrix-react-sdk/pull/1424)
|
||||||
|
* Bust the flair caches after 30mins
|
||||||
|
[\#1427](https://github.com/matrix-org/matrix-react-sdk/pull/1427)
|
||||||
|
* Show displayname / avatar in group member info
|
||||||
|
[\#1426](https://github.com/matrix-org/matrix-react-sdk/pull/1426)
|
||||||
|
* Create GroupSummaryStore for storing group summary stuff
|
||||||
|
[\#1418](https://github.com/matrix-org/matrix-react-sdk/pull/1418)
|
||||||
|
* Add status & toggle for publicity
|
||||||
|
[\#1419](https://github.com/matrix-org/matrix-react-sdk/pull/1419)
|
||||||
|
* MemberList: show 100 more on overflow tile click
|
||||||
|
[\#1417](https://github.com/matrix-org/matrix-react-sdk/pull/1417)
|
||||||
|
* Fix NPE in MemberList
|
||||||
|
[\#1425](https://github.com/matrix-org/matrix-react-sdk/pull/1425)
|
||||||
|
* Fix incorrect variable in string
|
||||||
|
[\#1422](https://github.com/matrix-org/matrix-react-sdk/pull/1422)
|
||||||
|
* apply i18n _t to string which has already been translated
|
||||||
|
[\#1420](https://github.com/matrix-org/matrix-react-sdk/pull/1420)
|
||||||
|
* Make the invite section a truncatedlist too
|
||||||
|
[\#1416](https://github.com/matrix-org/matrix-react-sdk/pull/1416)
|
||||||
|
* Implement removal function of features users/rooms
|
||||||
|
[\#1415](https://github.com/matrix-org/matrix-react-sdk/pull/1415)
|
||||||
|
* Allow TruncatedList to get children via a callback
|
||||||
|
[\#1412](https://github.com/matrix-org/matrix-react-sdk/pull/1412)
|
||||||
|
* Experimental: Lazy load user autocomplete entries
|
||||||
|
[\#1413](https://github.com/matrix-org/matrix-react-sdk/pull/1413)
|
||||||
|
* Show displayname & avatar url in group member list
|
||||||
|
[\#1414](https://github.com/matrix-org/matrix-react-sdk/pull/1414)
|
||||||
|
* De-lint TruncatedList
|
||||||
|
[\#1411](https://github.com/matrix-org/matrix-react-sdk/pull/1411)
|
||||||
|
* Remove unneeded strings
|
||||||
|
[\#1409](https://github.com/matrix-org/matrix-react-sdk/pull/1409)
|
||||||
|
* Clean on prerelease
|
||||||
|
[\#1410](https://github.com/matrix-org/matrix-react-sdk/pull/1410)
|
||||||
|
* Redesign membership section in GroupView
|
||||||
|
[\#1408](https://github.com/matrix-org/matrix-react-sdk/pull/1408)
|
||||||
|
* Implement adding rooms to the group summary
|
||||||
|
[\#1406](https://github.com/matrix-org/matrix-react-sdk/pull/1406)
|
||||||
|
* Honour the is_privileged flag in GroupView
|
||||||
|
[\#1407](https://github.com/matrix-org/matrix-react-sdk/pull/1407)
|
||||||
|
* Update when a group arrives
|
||||||
|
[\#1405](https://github.com/matrix-org/matrix-react-sdk/pull/1405)
|
||||||
|
* Implement `view_group` dispatch when clicking flair
|
||||||
|
[\#1404](https://github.com/matrix-org/matrix-react-sdk/pull/1404)
|
||||||
|
* GroupView: Add a User
|
||||||
|
[\#1402](https://github.com/matrix-org/matrix-react-sdk/pull/1402)
|
||||||
|
* Track action button click event
|
||||||
|
[\#1403](https://github.com/matrix-org/matrix-react-sdk/pull/1403)
|
||||||
|
* Separate sender profile into elements with classes
|
||||||
|
[\#1401](https://github.com/matrix-org/matrix-react-sdk/pull/1401)
|
||||||
|
* Fix ugly integration button, use hover to show error
|
||||||
|
[\#1399](https://github.com/matrix-org/matrix-react-sdk/pull/1399)
|
||||||
|
* Fix promise error in flair
|
||||||
|
[\#1400](https://github.com/matrix-org/matrix-react-sdk/pull/1400)
|
||||||
|
* Flair!
|
||||||
|
[\#1351](https://github.com/matrix-org/matrix-react-sdk/pull/1351)
|
||||||
|
* Group Membership UI
|
||||||
|
[\#1328](https://github.com/matrix-org/matrix-react-sdk/pull/1328)
|
||||||
|
|
||||||
|
Changes in [0.10.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.6) (2017-09-21)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.5...v0.10.6)
|
||||||
|
|
||||||
|
* New version of js-sdk with fixed build
|
||||||
|
|
||||||
|
Changes in [0.10.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.5) (2017-09-21)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.4...v0.10.5)
|
||||||
|
|
||||||
|
* Fix build error (https://github.com/vector-im/riot-web/issues/5091)
|
||||||
|
|
||||||
|
Changes in [0.10.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.4) (2017-09-20)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.4-rc.1...v0.10.4)
|
||||||
|
|
||||||
|
* No changes
|
||||||
|
|
||||||
|
Changes in [0.10.4-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.4-rc.1) (2017-09-19)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.3...v0.10.4-rc.1)
|
||||||
|
|
||||||
|
* Fix RoomView stuck in 'accept invite' state
|
||||||
|
[\#1396](https://github.com/matrix-org/matrix-react-sdk/pull/1396)
|
||||||
|
* Only show the integ management button if user is joined
|
||||||
|
[\#1398](https://github.com/matrix-org/matrix-react-sdk/pull/1398)
|
||||||
|
* suppressOnHover for member entity tiles which have no onClick
|
||||||
|
[\#1273](https://github.com/matrix-org/matrix-react-sdk/pull/1273)
|
||||||
|
* add /devtools command
|
||||||
|
[\#1268](https://github.com/matrix-org/matrix-react-sdk/pull/1268)
|
||||||
|
* Fix broken Link
|
||||||
|
[\#1359](https://github.com/matrix-org/matrix-react-sdk/pull/1359)
|
||||||
|
* Show who redacted an event on hover
|
||||||
|
[\#1387](https://github.com/matrix-org/matrix-react-sdk/pull/1387)
|
||||||
|
* start MELS expanded if it contains a highlighted/permalinked event.
|
||||||
|
[\#1388](https://github.com/matrix-org/matrix-react-sdk/pull/1388)
|
||||||
|
* Add ignore user API support
|
||||||
|
[\#1389](https://github.com/matrix-org/matrix-react-sdk/pull/1389)
|
||||||
|
* Add option to disable Emoji suggestions
|
||||||
|
[\#1392](https://github.com/matrix-org/matrix-react-sdk/pull/1392)
|
||||||
|
* sanitize the i18n for fn:textForHistoryVisibilityEvent
|
||||||
|
[\#1397](https://github.com/matrix-org/matrix-react-sdk/pull/1397)
|
||||||
|
* Don't check for only-emoji if there were none
|
||||||
|
[\#1394](https://github.com/matrix-org/matrix-react-sdk/pull/1394)
|
||||||
|
* Fix emojification of symbol characters
|
||||||
|
[\#1393](https://github.com/matrix-org/matrix-react-sdk/pull/1393)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1395](https://github.com/matrix-org/matrix-react-sdk/pull/1395)
|
||||||
|
* Make /join join again
|
||||||
|
[\#1391](https://github.com/matrix-org/matrix-react-sdk/pull/1391)
|
||||||
|
* Display spinner not room preview after room create
|
||||||
|
[\#1390](https://github.com/matrix-org/matrix-react-sdk/pull/1390)
|
||||||
|
* Fix the avatar / room name in room preview
|
||||||
|
[\#1384](https://github.com/matrix-org/matrix-react-sdk/pull/1384)
|
||||||
|
* Remove spurious cancel button
|
||||||
|
[\#1381](https://github.com/matrix-org/matrix-react-sdk/pull/1381)
|
||||||
|
* Fix starting a chat by email address
|
||||||
|
[\#1386](https://github.com/matrix-org/matrix-react-sdk/pull/1386)
|
||||||
|
* respond on copy code block
|
||||||
|
[\#1363](https://github.com/matrix-org/matrix-react-sdk/pull/1363)
|
||||||
|
* fix DateUtils inconsistency with 12/24h
|
||||||
|
[\#1383](https://github.com/matrix-org/matrix-react-sdk/pull/1383)
|
||||||
|
* allow sending sub,sup and whitelist them on receive
|
||||||
|
[\#1382](https://github.com/matrix-org/matrix-react-sdk/pull/1382)
|
||||||
|
* Update roomlist when an event is decrypted
|
||||||
|
[\#1380](https://github.com/matrix-org/matrix-react-sdk/pull/1380)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1379](https://github.com/matrix-org/matrix-react-sdk/pull/1379)
|
||||||
|
* fix radio for theme selection
|
||||||
|
[\#1368](https://github.com/matrix-org/matrix-react-sdk/pull/1368)
|
||||||
|
* fix some more zh_Hans - remove entirely broken lines
|
||||||
|
[\#1378](https://github.com/matrix-org/matrix-react-sdk/pull/1378)
|
||||||
|
* fix placeholder causing app to break when using zh
|
||||||
|
[\#1377](https://github.com/matrix-org/matrix-react-sdk/pull/1377)
|
||||||
|
* Avoid re-rendering RoomList on room switch
|
||||||
|
[\#1375](https://github.com/matrix-org/matrix-react-sdk/pull/1375)
|
||||||
|
* Fix 'Failed to load timeline position' regression
|
||||||
|
[\#1376](https://github.com/matrix-org/matrix-react-sdk/pull/1376)
|
||||||
|
* Fast path for emojifying strings
|
||||||
|
[\#1372](https://github.com/matrix-org/matrix-react-sdk/pull/1372)
|
||||||
|
* Consolidate the code copy button
|
||||||
|
[\#1374](https://github.com/matrix-org/matrix-react-sdk/pull/1374)
|
||||||
|
* Only add the code copy button for HTML messages
|
||||||
|
[\#1373](https://github.com/matrix-org/matrix-react-sdk/pull/1373)
|
||||||
|
* Don't re-render matrixchat unnecessarily
|
||||||
|
[\#1371](https://github.com/matrix-org/matrix-react-sdk/pull/1371)
|
||||||
|
* Don't wait for setState to run onHaveRoom
|
||||||
|
[\#1370](https://github.com/matrix-org/matrix-react-sdk/pull/1370)
|
||||||
|
* Introduce a RoomScrollStateStore
|
||||||
|
[\#1367](https://github.com/matrix-org/matrix-react-sdk/pull/1367)
|
||||||
|
* Don't always paginate when mounting a ScrollPanel
|
||||||
|
[\#1369](https://github.com/matrix-org/matrix-react-sdk/pull/1369)
|
||||||
|
* Remove unused scrollStateMap from LoggedinView
|
||||||
|
[\#1366](https://github.com/matrix-org/matrix-react-sdk/pull/1366)
|
||||||
|
* Revert "Implement sticky date separators"
|
||||||
|
[\#1365](https://github.com/matrix-org/matrix-react-sdk/pull/1365)
|
||||||
|
* Remove unused string "changing room on a RoomView is not supported"
|
||||||
|
[\#1361](https://github.com/matrix-org/matrix-react-sdk/pull/1361)
|
||||||
|
* Remove unused translation code translations
|
||||||
|
[\#1360](https://github.com/matrix-org/matrix-react-sdk/pull/1360)
|
||||||
|
* Implement sticky date separators
|
||||||
|
[\#1353](https://github.com/matrix-org/matrix-react-sdk/pull/1353)
|
||||||
|
|
||||||
|
Changes in [0.10.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3) (2017-09-06)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.3-rc.2...v0.10.3)
|
||||||
|
|
||||||
|
* No changes
|
||||||
|
|
||||||
|
Changes in [0.10.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3-rc.2) (2017-09-05)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.3-rc.1...v0.10.3-rc.2)
|
||||||
|
|
||||||
|
* Fix plurals in translations
|
||||||
|
[\#1358](https://github.com/matrix-org/matrix-react-sdk/pull/1358)
|
||||||
|
* Fix typo
|
||||||
|
[\#1357](https://github.com/matrix-org/matrix-react-sdk/pull/1357)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1356](https://github.com/matrix-org/matrix-react-sdk/pull/1356)
|
||||||
|
|
||||||
|
Changes in [0.10.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3-rc.1) (2017-09-01)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.2...v0.10.3-rc.1)
|
||||||
|
|
||||||
|
* Fix room change sometimes being very slow
|
||||||
|
[\#1354](https://github.com/matrix-org/matrix-react-sdk/pull/1354)
|
||||||
|
* apply shouldHideEvent fn to onRoomTimeline for RoomStatusBar
|
||||||
|
[\#1346](https://github.com/matrix-org/matrix-react-sdk/pull/1346)
|
||||||
|
* text4event widget modified, used to show widget added each time.
|
||||||
|
[\#1345](https://github.com/matrix-org/matrix-react-sdk/pull/1345)
|
||||||
|
* separate concepts of showing and managing RRs to fix regression
|
||||||
|
[\#1352](https://github.com/matrix-org/matrix-react-sdk/pull/1352)
|
||||||
|
* Make staging widgets work with live and vice versa.
|
||||||
|
[\#1350](https://github.com/matrix-org/matrix-react-sdk/pull/1350)
|
||||||
|
* Avoid breaking /sync with uncaught exceptions
|
||||||
|
[\#1349](https://github.com/matrix-org/matrix-react-sdk/pull/1349)
|
||||||
|
* we need to pass whether it is an invite RoomSubList explicitly (i18n)
|
||||||
|
[\#1343](https://github.com/matrix-org/matrix-react-sdk/pull/1343)
|
||||||
|
* Percent encoding isn't a valid thing within _t
|
||||||
|
[\#1348](https://github.com/matrix-org/matrix-react-sdk/pull/1348)
|
||||||
|
* Fix spurious notifications
|
||||||
|
[\#1339](https://github.com/matrix-org/matrix-react-sdk/pull/1339)
|
||||||
|
* Unbreak password reset with a non-default HS
|
||||||
|
[\#1347](https://github.com/matrix-org/matrix-react-sdk/pull/1347)
|
||||||
|
* Remove unnecessary 'load' on notif audio element
|
||||||
|
[\#1341](https://github.com/matrix-org/matrix-react-sdk/pull/1341)
|
||||||
|
* _tJsx returns a React Object, the sub fn must return a React Object
|
||||||
|
[\#1340](https://github.com/matrix-org/matrix-react-sdk/pull/1340)
|
||||||
|
* Fix deprecation warning about promise.defer()
|
||||||
|
[\#1292](https://github.com/matrix-org/matrix-react-sdk/pull/1292)
|
||||||
|
* Fix click to insert completion
|
||||||
|
[\#1331](https://github.com/matrix-org/matrix-react-sdk/pull/1331)
|
||||||
|
|
||||||
Changes in [0.10.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.2) (2017-08-24)
|
Changes in [0.10.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.2) (2017-08-24)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.1...v0.10.2)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.1...v0.10.2)
|
||||||
|
|
|
@ -46,7 +46,7 @@ Please follow the standard Matrix contributor's guide:
|
||||||
https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst
|
https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst
|
||||||
|
|
||||||
Please follow the Matrix JS/React code style as per:
|
Please follow the Matrix JS/React code style as per:
|
||||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/code_style.rst
|
https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md
|
||||||
|
|
||||||
Whilst the layering separation between matrix-react-sdk and Riot is broken
|
Whilst the layering separation between matrix-react-sdk and Riot is broken
|
||||||
(as of July 2016), code should be committed as follows:
|
(as of July 2016), code should be committed as follows:
|
||||||
|
|
|
@ -21,9 +21,7 @@ npm run test -- --no-colors
|
||||||
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.
|
# re-run the linter, excluding any files known to have errors or warnings.
|
||||||
./node_modules/.bin/eslint --max-warnings 0 \
|
npm run lintwithexclusions
|
||||||
--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
|
||||||
|
|
16
package.json
16
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.10.2",
|
"version": "0.10.7",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -28,19 +28,22 @@
|
||||||
"test"
|
"test"
|
||||||
],
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"reskindex": "scripts/reskindex.js"
|
"reskindex": "scripts/reskindex.js",
|
||||||
|
"matrix-gen-i18n": "scripts/gen-i18n.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"reskindex": "node scripts/reskindex.js -h header",
|
"reskindex": "node scripts/reskindex.js -h header",
|
||||||
"reskindex:watch": "node scripts/reskindex.js -h header -w",
|
"reskindex:watch": "node scripts/reskindex.js -h header -w",
|
||||||
|
"i18n": "matrix-gen-i18n",
|
||||||
"build": "npm run reskindex && babel src -d lib --source-maps --copy-files",
|
"build": "npm run reskindex && babel src -d lib --source-maps --copy-files",
|
||||||
"build:watch": "babel src -w -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",
|
"emoji-data-strip": "node scripts/emoji-data-strip.js",
|
||||||
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
|
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"lintall": "eslint src/ test/",
|
"lintall": "eslint src/ test/",
|
||||||
|
"lintwithexclusions": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
|
||||||
"clean": "rimraf lib",
|
"clean": "rimraf lib",
|
||||||
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt",
|
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt",
|
||||||
"test": "karma start --single-run=true --browsers ChromeHeadless",
|
"test": "karma start --single-run=true --browsers ChromeHeadless",
|
||||||
"test-multi": "karma start"
|
"test-multi": "karma start"
|
||||||
},
|
},
|
||||||
|
@ -66,7 +69,7 @@
|
||||||
"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.8.2",
|
"matrix-js-sdk": "0.8.5",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
"react": "^15.4.0",
|
"react": "^15.4.0",
|
||||||
|
@ -99,8 +102,10 @@
|
||||||
"eslint-config-google": "^0.7.1",
|
"eslint-config-google": "^0.7.1",
|
||||||
"eslint-plugin-babel": "^4.0.1",
|
"eslint-plugin-babel": "^4.0.1",
|
||||||
"eslint-plugin-flowtype": "^2.30.0",
|
"eslint-plugin-flowtype": "^2.30.0",
|
||||||
"eslint-plugin-react": "^6.9.0",
|
"eslint-plugin-react": "^7.4.0",
|
||||||
|
"estree-walker": "^0.5.0",
|
||||||
"expect": "^1.16.0",
|
"expect": "^1.16.0",
|
||||||
|
"flow-parser": "^0.57.3",
|
||||||
"json-loader": "^0.5.3",
|
"json-loader": "^0.5.3",
|
||||||
"karma": "^1.7.0",
|
"karma": "^1.7.0",
|
||||||
"karma-chrome-launcher": "^0.2.3",
|
"karma-chrome-launcher": "^0.2.3",
|
||||||
|
@ -120,6 +125,7 @@
|
||||||
"rimraf": "^2.4.3",
|
"rimraf": "^2.4.3",
|
||||||
"sinon": "^1.17.3",
|
"sinon": "^1.17.3",
|
||||||
"source-map-loader": "^0.1.5",
|
"source-map-loader": "^0.1.5",
|
||||||
|
"walk": "^2.3.9",
|
||||||
"webpack": "^1.12.14"
|
"webpack": "^1.12.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
188
scripts/gen-i18n.js
Executable file
188
scripts/gen-i18n.js
Executable file
|
@ -0,0 +1,188 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2017 New Vector 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerates the translations en_EN file by walking the source tree and
|
||||||
|
* parsing each file with flow-parser. Emits a JSON file with the
|
||||||
|
* translatable strings mapped to themselves in the order they appeared
|
||||||
|
* in the files and grouped by the file they appeared in.
|
||||||
|
*
|
||||||
|
* Usage: node scripts/gen-i18n.js
|
||||||
|
*/
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const walk = require('walk');
|
||||||
|
|
||||||
|
const flowParser = require('flow-parser');
|
||||||
|
const estreeWalker = require('estree-walker');
|
||||||
|
|
||||||
|
const TRANSLATIONS_FUNCS = ['_t', '_td', '_tJsx'];
|
||||||
|
|
||||||
|
const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json';
|
||||||
|
|
||||||
|
// NB. The sync version of walk is broken for single files so we walk
|
||||||
|
// all of res rather than just res/home.html.
|
||||||
|
// https://git.daplie.com/Daplie/node-walk/merge_requests/1 fixes it,
|
||||||
|
// or if we get bored waiting for it to be merged, we could switch
|
||||||
|
// to a project that's actively maintained.
|
||||||
|
const SEARCH_PATHS = ['src', 'res'];
|
||||||
|
|
||||||
|
const FLOW_PARSER_OPTS = {
|
||||||
|
esproposal_class_instance_fields: true,
|
||||||
|
esproposal_class_static_fields: true,
|
||||||
|
esproposal_decorators: true,
|
||||||
|
esproposal_export_star_as: true,
|
||||||
|
types: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getObjectValue(obj, key) {
|
||||||
|
for (const prop of obj.properties) {
|
||||||
|
if (prop.key.type == 'Identifier' && prop.key.name == key) {
|
||||||
|
return prop.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTKey(arg) {
|
||||||
|
if (arg.type == 'Literal') {
|
||||||
|
return arg.value;
|
||||||
|
} else if (arg.type == 'BinaryExpression' && arg.operator == '+') {
|
||||||
|
return getTKey(arg.left) + getTKey(arg.right);
|
||||||
|
} else if (arg.type == 'TemplateLiteral') {
|
||||||
|
return arg.quasis.map((q) => {
|
||||||
|
return q.value.raw;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTranslationsJs(file) {
|
||||||
|
const tree = flowParser.parse(fs.readFileSync(file, { encoding: 'utf8' }), FLOW_PARSER_OPTS);
|
||||||
|
|
||||||
|
const trs = new Set();
|
||||||
|
|
||||||
|
estreeWalker.walk(tree, {
|
||||||
|
enter: function(node, parent) {
|
||||||
|
if (
|
||||||
|
node.type == 'CallExpression' &&
|
||||||
|
TRANSLATIONS_FUNCS.includes(node.callee.name)
|
||||||
|
) {
|
||||||
|
const tKey = getTKey(node.arguments[0]);
|
||||||
|
// This happens whenever we call _t with non-literals (ie. whenever we've
|
||||||
|
// had to use a _td to compensate) so is expected.
|
||||||
|
if (tKey === null) return;
|
||||||
|
|
||||||
|
let isPlural = false;
|
||||||
|
if (node.arguments.length > 1 && node.arguments[1].type == 'ObjectExpression') {
|
||||||
|
const countVal = getObjectValue(node.arguments[1], 'count');
|
||||||
|
if (countVal) {
|
||||||
|
isPlural = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlural) {
|
||||||
|
trs.add(tKey + "|other");
|
||||||
|
const plurals = enPlurals[tKey];
|
||||||
|
if (plurals) {
|
||||||
|
for (const pluralType of Object.keys(plurals)) {
|
||||||
|
trs.add(tKey + "|" + pluralType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trs.add(tKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return trs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTranslationsOther(file) {
|
||||||
|
const contents = fs.readFileSync(file, { encoding: 'utf8' });
|
||||||
|
|
||||||
|
const trs = new Set();
|
||||||
|
|
||||||
|
// Taken from riot-web src/components/structures/HomePage.js
|
||||||
|
const translationsRegex = /_t\(['"]([\s\S]*?)['"]\)/mg;
|
||||||
|
let matches;
|
||||||
|
while (matches = translationsRegex.exec(contents)) {
|
||||||
|
trs.add(matches[1]);
|
||||||
|
}
|
||||||
|
return trs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// gather en_EN plural strings from the input translations file:
|
||||||
|
// the en_EN strings are all in the source with the exception of
|
||||||
|
// pluralised strings, which we need to pull in from elsewhere.
|
||||||
|
const inputTranslationsRaw = JSON.parse(fs.readFileSync(INPUT_TRANSLATIONS_FILE, { encoding: 'utf8' }));
|
||||||
|
const enPlurals = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(inputTranslationsRaw)) {
|
||||||
|
const parts = key.split("|");
|
||||||
|
if (parts.length > 1) {
|
||||||
|
const plurals = enPlurals[parts[0]] || {};
|
||||||
|
plurals[parts[1]] = inputTranslationsRaw[key];
|
||||||
|
enPlurals[parts[0]] = plurals;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const translatables = new Set();
|
||||||
|
|
||||||
|
const walkOpts = {
|
||||||
|
listeners: {
|
||||||
|
file: function(root, fileStats, next) {
|
||||||
|
const fullPath = path.join(root, fileStats.name);
|
||||||
|
|
||||||
|
let ltrs;
|
||||||
|
if (fileStats.name.endsWith('.js')) {
|
||||||
|
trs = getTranslationsJs(fullPath);
|
||||||
|
} else if (fileStats.name.endsWith('.html')) {
|
||||||
|
trs = getTranslationsOther(fullPath);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`${fullPath} (${trs.size} strings)`);
|
||||||
|
for (const tr of trs.values()) {
|
||||||
|
translatables.add(tr);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const path of SEARCH_PATHS) {
|
||||||
|
if (fs.existsSync(path)) {
|
||||||
|
walk.walkSync(path, walkOpts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trObj = {};
|
||||||
|
for (const tr of translatables) {
|
||||||
|
trObj[tr] = tr;
|
||||||
|
if (tr.includes("|")) {
|
||||||
|
trObj[tr] = inputTranslationsRaw[tr];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
"src/i18n/strings/en_EN.json",
|
||||||
|
JSON.stringify(trObj, translatables.values(), 4) + "\n"
|
||||||
|
);
|
||||||
|
|
|
@ -6,6 +6,4 @@ npm run test
|
||||||
./.travis-test-riot.sh
|
./.travis-test-riot.sh
|
||||||
|
|
||||||
# run the linter, but exclude any files known to have errors or warnings.
|
# run the linter, but exclude any files known to have errors or warnings.
|
||||||
./node_modules/.bin/eslint --max-warnings 0 \
|
npm run lintwithexclusions
|
||||||
--ignore-path .eslintignore.errorfiles \
|
|
||||||
src test
|
|
||||||
|
|
77
src/ActiveRoomObserver.js
Normal file
77
src/ActiveRoomObserver.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 New Vector 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 RoomViewStore from './stores/RoomViewStore';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes changes from the RoomViewStore and notifies specific things
|
||||||
|
* about when the active room changes. Unlike listening for RoomViewStore
|
||||||
|
* changes, you can subscribe to only changes relevant to a particular
|
||||||
|
* room.
|
||||||
|
*
|
||||||
|
* TODO: If we introduce an observer for something else, factor out
|
||||||
|
* the adding / removing of listeners & emitting into a common class.
|
||||||
|
*/
|
||||||
|
class ActiveRoomObserver {
|
||||||
|
constructor() {
|
||||||
|
this._listeners = {};
|
||||||
|
|
||||||
|
this._activeRoomId = RoomViewStore.getRoomId();
|
||||||
|
// TODO: We could self-destruct when the last listener goes away, or at least
|
||||||
|
// stop listening.
|
||||||
|
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(roomId, listener) {
|
||||||
|
if (!this._listeners[roomId]) this._listeners[roomId] = [];
|
||||||
|
this._listeners[roomId].push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListener(roomId, listener) {
|
||||||
|
if (this._listeners[roomId]) {
|
||||||
|
const i = this._listeners[roomId].indexOf(listener);
|
||||||
|
if (i > -1) {
|
||||||
|
this._listeners[roomId].splice(i, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Unregistering unrecognised listener (roomId=" + roomId + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_emit(roomId) {
|
||||||
|
if (!this._listeners[roomId]) return;
|
||||||
|
|
||||||
|
for (const l of this._listeners[roomId]) {
|
||||||
|
l.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRoomViewStoreUpdate() {
|
||||||
|
// emit for the old room ID
|
||||||
|
if (this._activeRoomId) this._emit(this._activeRoomId);
|
||||||
|
|
||||||
|
// update our cache
|
||||||
|
this._activeRoomId = RoomViewStore.getRoomId();
|
||||||
|
|
||||||
|
// and emit for the new one
|
||||||
|
if (this._activeRoomId) this._emit(this._activeRoomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global.mx_ActiveRoomObserver === undefined) {
|
||||||
|
global.mx_ActiveRoomObserver = new ActiveRoomObserver();
|
||||||
|
}
|
||||||
|
export default global.mx_ActiveRoomObserver;
|
|
@ -107,6 +107,9 @@ export default class BasePlatform {
|
||||||
|
|
||||||
isElectron(): boolean { return false; }
|
isElectron(): boolean { return false; }
|
||||||
|
|
||||||
|
setupScreenSharingForIframe() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restarts the application, without neccessarily reloading
|
* Restarts the application, without neccessarily reloading
|
||||||
* any application code
|
* any application code
|
||||||
|
|
|
@ -63,23 +63,22 @@ import dis from './dispatcher';
|
||||||
global.mxCalls = {
|
global.mxCalls = {
|
||||||
//room_id: MatrixCall
|
//room_id: MatrixCall
|
||||||
};
|
};
|
||||||
var calls = global.mxCalls;
|
const calls = global.mxCalls;
|
||||||
var ConferenceHandler = null;
|
let ConferenceHandler = null;
|
||||||
|
|
||||||
var audioPromises = {};
|
const audioPromises = {};
|
||||||
|
|
||||||
function play(audioId) {
|
function play(audioId) {
|
||||||
// TODO: Attach an invisible element for this instead
|
// TODO: Attach an invisible element for this instead
|
||||||
// which listens?
|
// which listens?
|
||||||
var audio = document.getElementById(audioId);
|
const audio = document.getElementById(audioId);
|
||||||
if (audio) {
|
if (audio) {
|
||||||
if (audioPromises[audioId]) {
|
if (audioPromises[audioId]) {
|
||||||
audioPromises[audioId] = audioPromises[audioId].then(()=>{
|
audioPromises[audioId] = audioPromises[audioId].then(()=>{
|
||||||
audio.load();
|
audio.load();
|
||||||
return audio.play();
|
return audio.play();
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
audioPromises[audioId] = audio.play();
|
audioPromises[audioId] = audio.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,12 +87,11 @@ function play(audioId) {
|
||||||
function pause(audioId) {
|
function pause(audioId) {
|
||||||
// TODO: Attach an invisible element for this instead
|
// TODO: Attach an invisible element for this instead
|
||||||
// which listens?
|
// which listens?
|
||||||
var audio = document.getElementById(audioId);
|
const audio = document.getElementById(audioId);
|
||||||
if (audio) {
|
if (audio) {
|
||||||
if (audioPromises[audioId]) {
|
if (audioPromises[audioId]) {
|
||||||
audioPromises[audioId] = audioPromises[audioId].then(()=>audio.pause());
|
audioPromises[audioId] = audioPromises[audioId].then(()=>audio.pause());
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// pause doesn't actually return a promise, but might as well do this for symmetry with play();
|
// pause doesn't actually return a promise, but might as well do this for symmetry with play();
|
||||||
audioPromises[audioId] = audio.pause();
|
audioPromises[audioId] = audio.pause();
|
||||||
}
|
}
|
||||||
|
@ -125,38 +123,32 @@ function _setCallListeners(call) {
|
||||||
if (newState === "ringing") {
|
if (newState === "ringing") {
|
||||||
_setCallState(call, call.roomId, "ringing");
|
_setCallState(call, call.roomId, "ringing");
|
||||||
pause("ringbackAudio");
|
pause("ringbackAudio");
|
||||||
}
|
} else if (newState === "invite_sent") {
|
||||||
else if (newState === "invite_sent") {
|
|
||||||
_setCallState(call, call.roomId, "ringback");
|
_setCallState(call, call.roomId, "ringback");
|
||||||
play("ringbackAudio");
|
play("ringbackAudio");
|
||||||
}
|
} else if (newState === "ended" && oldState === "connected") {
|
||||||
else if (newState === "ended" && oldState === "connected") {
|
|
||||||
_setCallState(undefined, call.roomId, "ended");
|
_setCallState(undefined, call.roomId, "ended");
|
||||||
pause("ringbackAudio");
|
pause("ringbackAudio");
|
||||||
play("callendAudio");
|
play("callendAudio");
|
||||||
}
|
} else if (newState === "ended" && oldState === "invite_sent" &&
|
||||||
else if (newState === "ended" && oldState === "invite_sent" &&
|
|
||||||
(call.hangupParty === "remote" ||
|
(call.hangupParty === "remote" ||
|
||||||
(call.hangupParty === "local" && call.hangupReason === "invite_timeout")
|
(call.hangupParty === "local" && call.hangupReason === "invite_timeout")
|
||||||
)) {
|
)) {
|
||||||
_setCallState(call, call.roomId, "busy");
|
_setCallState(call, call.roomId, "busy");
|
||||||
pause("ringbackAudio");
|
pause("ringbackAudio");
|
||||||
play("busyAudio");
|
play("busyAudio");
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
|
||||||
title: _t('Call Timeout'),
|
title: _t('Call Timeout'),
|
||||||
description: _t('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") {
|
|
||||||
_setCallState(call, call.roomId, "stop_ringback");
|
_setCallState(call, call.roomId, "stop_ringback");
|
||||||
pause("ringbackAudio");
|
pause("ringbackAudio");
|
||||||
}
|
} else if (oldState === "ringing") {
|
||||||
else if (oldState === "ringing") {
|
|
||||||
_setCallState(call, call.roomId, "stop_ringing");
|
_setCallState(call, call.roomId, "stop_ringing");
|
||||||
pause("ringbackAudio");
|
pause("ringbackAudio");
|
||||||
}
|
} else if (newState === "connected") {
|
||||||
else if (newState === "connected") {
|
|
||||||
_setCallState(call, call.roomId, "connected");
|
_setCallState(call, call.roomId, "connected");
|
||||||
pause("ringbackAudio");
|
pause("ringbackAudio");
|
||||||
}
|
}
|
||||||
|
@ -165,14 +157,13 @@ function _setCallListeners(call) {
|
||||||
|
|
||||||
function _setCallState(call, roomId, status) {
|
function _setCallState(call, roomId, status) {
|
||||||
console.log(
|
console.log(
|
||||||
"Call state in %s changed to %s (%s)", roomId, status, (call ? call.call_state : "-")
|
"Call state in %s changed to %s (%s)", roomId, status, (call ? call.call_state : "-"),
|
||||||
);
|
);
|
||||||
calls[roomId] = call;
|
calls[roomId] = call;
|
||||||
|
|
||||||
if (status === "ringing") {
|
if (status === "ringing") {
|
||||||
play("ringAudio");
|
play("ringAudio");
|
||||||
}
|
} else if (call && call.call_state === "ringing") {
|
||||||
else if (call && call.call_state === "ringing") {
|
|
||||||
pause("ringAudio");
|
pause("ringAudio");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,14 +183,12 @@ function _onAction(payload) {
|
||||||
_setCallState(newCall, newCall.roomId, "ringback");
|
_setCallState(newCall, newCall.roomId, "ringback");
|
||||||
if (payload.type === 'voice') {
|
if (payload.type === 'voice') {
|
||||||
newCall.placeVoiceCall();
|
newCall.placeVoiceCall();
|
||||||
}
|
} else if (payload.type === 'video') {
|
||||||
else if (payload.type === 'video') {
|
|
||||||
newCall.placeVideoCall(
|
newCall.placeVideoCall(
|
||||||
payload.remote_element,
|
payload.remote_element,
|
||||||
payload.local_element
|
payload.local_element,
|
||||||
);
|
);
|
||||||
}
|
} else if (payload.type === 'screensharing') {
|
||||||
else if (payload.type === 'screensharing') {
|
|
||||||
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
||||||
if (screenCapErrorString) {
|
if (screenCapErrorString) {
|
||||||
_setCallState(undefined, newCall.roomId, "ended");
|
_setCallState(undefined, newCall.roomId, "ended");
|
||||||
|
@ -213,10 +202,9 @@ function _onAction(payload) {
|
||||||
}
|
}
|
||||||
newCall.placeScreenSharingCall(
|
newCall.placeScreenSharingCall(
|
||||||
payload.remote_element,
|
payload.remote_element,
|
||||||
payload.local_element
|
payload.local_element,
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
console.error("Unknown conf call type: %s", payload.type);
|
console.error("Unknown conf call type: %s", payload.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -255,21 +243,19 @@ function _onAction(payload) {
|
||||||
description: _t('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);
|
||||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, {
|
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, {
|
||||||
forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false),
|
forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false),
|
||||||
});
|
});
|
||||||
placeCall(call);
|
placeCall(call);
|
||||||
}
|
} else { // > 2
|
||||||
else { // > 2
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "place_conference_call",
|
action: "place_conference_call",
|
||||||
room_id: payload.room_id,
|
room_id: payload.room_id,
|
||||||
type: payload.type,
|
type: payload.type,
|
||||||
remote_element: payload.remote_element,
|
remote_element: payload.remote_element,
|
||||||
local_element: payload.local_element
|
local_element: payload.local_element,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -280,15 +266,13 @@ function _onAction(payload) {
|
||||||
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
|
||||||
description: _t('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.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||||
title: _t('VoIP is unsupported'),
|
title: _t('VoIP is unsupported'),
|
||||||
description: _t('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)) {
|
|
||||||
// Conference calls are implemented by sending the media to central
|
// Conference calls are implemented by sending the media to central
|
||||||
// server which combines the audio from all the participants together
|
// server which combines the audio from all the participants together
|
||||||
// into a single stream. This is incompatible with end-to-end encryption
|
// into a single stream. This is incompatible with end-to-end encryption
|
||||||
|
@ -299,16 +283,15 @@ function _onAction(payload) {
|
||||||
Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, {
|
||||||
description: _t('Conference calls are not supported in encrypted rooms'),
|
description: _t('Conference calls are not supported in encrypted rooms'),
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
else {
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
||||||
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
|
||||||
title: _t('Warning!'),
|
title: _t('Warning!'),
|
||||||
description: _t('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(
|
||||||
MatrixClientPeg.get(), payload.room_id
|
MatrixClientPeg.get(), payload.room_id,
|
||||||
).done(function(call) {
|
).done(function(call) {
|
||||||
placeCall(call);
|
placeCall(call);
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
|
@ -357,7 +340,7 @@ function _onAction(payload) {
|
||||||
_setCallState(calls[payload.room_id], payload.room_id, "connected");
|
_setCallState(calls[payload.room_id], payload.room_id, "connected");
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "view_room",
|
action: "view_room",
|
||||||
room_id: payload.room_id
|
room_id: payload.room_id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -368,9 +351,9 @@ if (!global.mxCallHandler) {
|
||||||
dis.register(_onAction);
|
dis.register(_onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
var callHandler = {
|
const callHandler = {
|
||||||
getCallForRoom: function(roomId) {
|
getCallForRoom: function(roomId) {
|
||||||
var call = module.exports.getCall(roomId);
|
let call = module.exports.getCall(roomId);
|
||||||
if (call) return call;
|
if (call) return call;
|
||||||
|
|
||||||
if (ConferenceHandler) {
|
if (ConferenceHandler) {
|
||||||
|
@ -386,8 +369,8 @@ var callHandler = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getAnyActiveCall: function() {
|
getAnyActiveCall: function() {
|
||||||
var roomsWithCalls = Object.keys(calls);
|
const roomsWithCalls = Object.keys(calls);
|
||||||
for (var i = 0; i < roomsWithCalls.length; i++) {
|
for (let i = 0; i < roomsWithCalls.length; i++) {
|
||||||
if (calls[roomsWithCalls[i]] &&
|
if (calls[roomsWithCalls[i]] &&
|
||||||
calls[roomsWithCalls[i]].call_state !== "ended") {
|
calls[roomsWithCalls[i]].call_state !== "ended") {
|
||||||
return calls[roomsWithCalls[i]];
|
return calls[roomsWithCalls[i]];
|
||||||
|
@ -402,7 +385,7 @@ var callHandler = {
|
||||||
|
|
||||||
getConferenceHandler: function() {
|
getConferenceHandler: function() {
|
||||||
return ConferenceHandler;
|
return ConferenceHandler;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
// Only things in here which actually need to be global are the
|
// Only things in here which actually need to be global are the
|
||||||
// calls list (done separately) and making sure we only register
|
// calls list (done separately) and making sure we only register
|
||||||
|
|
|
@ -17,14 +17,14 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
var extend = require('./extend');
|
const extend = require('./extend');
|
||||||
var dis = require('./dispatcher');
|
const dis = require('./dispatcher');
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
const MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
var sdk = require('./index');
|
const sdk = require('./index');
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
var Modal = require('./Modal');
|
const Modal = require('./Modal');
|
||||||
|
|
||||||
var encrypt = require("browser-encrypt-attachment");
|
const encrypt = require("browser-encrypt-attachment");
|
||||||
|
|
||||||
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
|
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
|
||||||
require("blueimp-canvas-to-blob");
|
require("blueimp-canvas-to-blob");
|
||||||
|
@ -54,8 +54,8 @@ const MAX_HEIGHT = 600;
|
||||||
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
||||||
const deferred = Promise.defer();
|
const deferred = Promise.defer();
|
||||||
|
|
||||||
var targetWidth = inputWidth;
|
let targetWidth = inputWidth;
|
||||||
var targetHeight = inputHeight;
|
let targetHeight = inputHeight;
|
||||||
if (targetHeight > MAX_HEIGHT) {
|
if (targetHeight > MAX_HEIGHT) {
|
||||||
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
|
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
|
||||||
targetHeight = MAX_HEIGHT;
|
targetHeight = MAX_HEIGHT;
|
||||||
|
@ -81,7 +81,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
||||||
w: inputWidth,
|
w: inputWidth,
|
||||||
h: inputHeight,
|
h: inputHeight,
|
||||||
},
|
},
|
||||||
thumbnail: thumbnail
|
thumbnail: thumbnail,
|
||||||
});
|
});
|
||||||
}, mimeType);
|
}, mimeType);
|
||||||
|
|
||||||
|
@ -129,12 +129,12 @@ function loadImageElement(imageFile) {
|
||||||
* @return {Promise} A promise that resolves with the attachment info.
|
* @return {Promise} A promise that resolves with the attachment info.
|
||||||
*/
|
*/
|
||||||
function infoForImageFile(matrixClient, roomId, imageFile) {
|
function infoForImageFile(matrixClient, roomId, imageFile) {
|
||||||
var thumbnailType = "image/png";
|
let thumbnailType = "image/png";
|
||||||
if (imageFile.type == "image/jpeg") {
|
if (imageFile.type == "image/jpeg") {
|
||||||
thumbnailType = "image/jpeg";
|
thumbnailType = "image/jpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageInfo;
|
let imageInfo;
|
||||||
return loadImageElement(imageFile).then(function(img) {
|
return loadImageElement(imageFile).then(function(img) {
|
||||||
return createThumbnail(img, img.width, img.height, thumbnailType);
|
return createThumbnail(img, img.width, img.height, thumbnailType);
|
||||||
}).then(function(result) {
|
}).then(function(result) {
|
||||||
|
@ -191,7 +191,7 @@ function loadVideoElement(videoFile) {
|
||||||
function infoForVideoFile(matrixClient, roomId, videoFile) {
|
function infoForVideoFile(matrixClient, roomId, videoFile) {
|
||||||
const thumbnailType = "image/jpeg";
|
const thumbnailType = "image/jpeg";
|
||||||
|
|
||||||
var videoInfo;
|
let videoInfo;
|
||||||
return loadVideoElement(videoFile).then(function(video) {
|
return loadVideoElement(videoFile).then(function(video) {
|
||||||
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
|
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
|
||||||
}).then(function(result) {
|
}).then(function(result) {
|
||||||
|
@ -286,7 +286,7 @@ class ContentMessages {
|
||||||
body: file.name || 'Attachment',
|
body: file.name || 'Attachment',
|
||||||
info: {
|
info: {
|
||||||
size: file.size,
|
size: file.size,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// if we have a mime type for the file, add it to the message metadata
|
// if we have a mime type for the file, add it to the message metadata
|
||||||
|
@ -297,10 +297,10 @@ class ContentMessages {
|
||||||
const def = Promise.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)=>{
|
||||||
extend(content.info, imageInfo);
|
extend(content.info, imageInfo);
|
||||||
def.resolve();
|
def.resolve();
|
||||||
}, error=>{
|
}, (error)=>{
|
||||||
console.error(error);
|
console.error(error);
|
||||||
content.msgtype = 'm.file';
|
content.msgtype = 'm.file';
|
||||||
def.resolve();
|
def.resolve();
|
||||||
|
@ -310,10 +310,10 @@ class ContentMessages {
|
||||||
def.resolve();
|
def.resolve();
|
||||||
} else if (file.type.indexOf('video/') == 0) {
|
} else if (file.type.indexOf('video/') == 0) {
|
||||||
content.msgtype = 'm.video';
|
content.msgtype = 'm.video';
|
||||||
infoForVideoFile(matrixClient, roomId, file).then(videoInfo=>{
|
infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{
|
||||||
extend(content.info, videoInfo);
|
extend(content.info, videoInfo);
|
||||||
def.resolve();
|
def.resolve();
|
||||||
}, error=>{
|
}, (error)=>{
|
||||||
content.msgtype = 'm.file';
|
content.msgtype = 'm.file';
|
||||||
def.resolve();
|
def.resolve();
|
||||||
});
|
});
|
||||||
|
@ -331,7 +331,7 @@ class ContentMessages {
|
||||||
this.inprogress.push(upload);
|
this.inprogress.push(upload);
|
||||||
dis.dispatch({action: 'upload_started'});
|
dis.dispatch({action: 'upload_started'});
|
||||||
|
|
||||||
var error;
|
let error;
|
||||||
|
|
||||||
function onProgress(ev) {
|
function onProgress(ev) {
|
||||||
upload.total = ev.total;
|
upload.total = ev.total;
|
||||||
|
@ -355,11 +355,11 @@ class ContentMessages {
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
error = err;
|
error = err;
|
||||||
if (!upload.canceled) {
|
if (!upload.canceled) {
|
||||||
var desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.';
|
let desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.';
|
||||||
if (err.http_status == 413) {
|
if (err.http_status == 413) {
|
||||||
desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
|
desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
|
||||||
}
|
}
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
||||||
title: _t('Upload Failed'),
|
title: _t('Upload Failed'),
|
||||||
description: desc,
|
description: desc,
|
||||||
|
@ -367,8 +367,8 @@ class ContentMessages {
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
const inprogressKeys = Object.keys(this.inprogress);
|
const inprogressKeys = Object.keys(this.inprogress);
|
||||||
for (var i = 0; i < this.inprogress.length; ++i) {
|
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||||
var k = inprogressKeys[i];
|
const k = inprogressKeys[i];
|
||||||
if (this.inprogress[k].promise === upload.promise) {
|
if (this.inprogress[k].promise === upload.promise) {
|
||||||
this.inprogress.splice(k, 1);
|
this.inprogress.splice(k, 1);
|
||||||
break;
|
break;
|
||||||
|
@ -376,8 +376,7 @@ class ContentMessages {
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
dis.dispatch({action: 'upload_failed', upload: upload});
|
dis.dispatch({action: 'upload_failed', upload: upload});
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
dis.dispatch({action: 'upload_finished', upload: upload});
|
dis.dispatch({action: 'upload_finished', upload: upload});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -389,9 +388,9 @@ class ContentMessages {
|
||||||
|
|
||||||
cancelUpload(promise) {
|
cancelUpload(promise) {
|
||||||
const inprogressKeys = Object.keys(this.inprogress);
|
const inprogressKeys = Object.keys(this.inprogress);
|
||||||
var upload;
|
let upload;
|
||||||
for (var i = 0; i < this.inprogress.length; ++i) {
|
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||||
var k = inprogressKeys[i];
|
const k = inprogressKeys[i];
|
||||||
if (this.inprogress[k].promise === promise) {
|
if (this.inprogress[k].promise === promise) {
|
||||||
upload = this.inprogress[k];
|
upload = this.inprogress[k];
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -65,7 +65,7 @@ module.exports = {
|
||||||
const days = getDaysArray();
|
const days = getDaysArray();
|
||||||
const months = getMonthsArray();
|
const months = getMonthsArray();
|
||||||
if (date.toDateString() === now.toDateString()) {
|
if (date.toDateString() === now.toDateString()) {
|
||||||
return this.formatTime(date);
|
return this.formatTime(date, showTwelveHour);
|
||||||
} else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
} else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
||||||
// TODO: use standard date localize function provided in counterpart
|
// TODO: use standard date localize function provided in counterpart
|
||||||
return _t('%(weekDayName)s %(time)s', {
|
return _t('%(weekDayName)s %(time)s', {
|
||||||
|
@ -78,7 +78,7 @@ module.exports = {
|
||||||
weekDayName: days[date.getDay()],
|
weekDayName: days[date.getDay()],
|
||||||
monthName: months[date.getMonth()],
|
monthName: months[date.getMonth()],
|
||||||
day: date.getDate(),
|
day: date.getDate(),
|
||||||
time: this.formatTime(date),
|
time: this.formatTime(date, showTwelveHour),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return this.formatFullDate(date, showTwelveHour);
|
return this.formatFullDate(date, showTwelveHour);
|
||||||
|
@ -92,13 +92,13 @@ module.exports = {
|
||||||
monthName: months[date.getMonth()],
|
monthName: months[date.getMonth()],
|
||||||
day: date.getDate(),
|
day: date.getDate(),
|
||||||
fullYear: date.getFullYear(),
|
fullYear: date.getFullYear(),
|
||||||
time: showTwelveHour ? twelveHourTime(date) : this.formatTime(date),
|
time: this.formatTime(date, showTwelveHour),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
formatTime: function(date, showTwelveHour=false) {
|
formatTime: function(date, showTwelveHour=false) {
|
||||||
if (showTwelveHour) {
|
if (showTwelveHour) {
|
||||||
return twelveHourTime(date);
|
return twelveHourTime(date);
|
||||||
}
|
}
|
||||||
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
},
|
},
|
||||||
|
|
157
src/GroupAddressPicker.js
Normal file
157
src/GroupAddressPicker.js
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Modal from './Modal';
|
||||||
|
import sdk from './';
|
||||||
|
import MultiInviter from './utils/MultiInviter';
|
||||||
|
import { _t } from './languageHandler';
|
||||||
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
import GroupStoreCache from './stores/GroupStoreCache';
|
||||||
|
|
||||||
|
export function showGroupInviteDialog(groupId) {
|
||||||
|
const description = <div>
|
||||||
|
<div>{ _t("Who would you like to add to this community?") }</div>
|
||||||
|
<div className="warning">
|
||||||
|
{ _t(
|
||||||
|
"Warning: any person you add to a community will be publicly "+
|
||||||
|
"visible to anyone who knows the community ID",
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
|
Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, {
|
||||||
|
title: _t("Invite new community members"),
|
||||||
|
description: description,
|
||||||
|
placeholder: _t("Name or matrix ID"),
|
||||||
|
button: _t("Invite to Community"),
|
||||||
|
validAddressTypes: ['mx-user-id'],
|
||||||
|
onFinished: (success, addrs) => {
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
_onGroupInviteFinished(groupId, addrs);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showGroupAddRoomDialog(groupId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const description = <div>
|
||||||
|
<div>{ _t("Which rooms would you like to add to this community?") }</div>
|
||||||
|
<div className="warning">
|
||||||
|
{ _t(
|
||||||
|
"Warning: any room you add to a community will be publicly "+
|
||||||
|
"visible to anyone who knows the community ID",
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
|
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
|
||||||
|
title: _t("Add rooms to the community"),
|
||||||
|
description: description,
|
||||||
|
placeholder: _t("Room name or alias"),
|
||||||
|
button: _t("Add to community"),
|
||||||
|
pickerType: 'room',
|
||||||
|
validAddressTypes: ['mx-room-id'],
|
||||||
|
onFinished: (success, addrs) => {
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
_onGroupAddRoomFinished(groupId, addrs).then(resolve, reject);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onGroupInviteFinished(groupId, addrs) {
|
||||||
|
const multiInviter = new MultiInviter(groupId);
|
||||||
|
|
||||||
|
const addrTexts = addrs.map((addr) => addr.address);
|
||||||
|
|
||||||
|
multiInviter.invite(addrTexts).then((completionStates) => {
|
||||||
|
// Show user any errors
|
||||||
|
const errorList = [];
|
||||||
|
for (const addr of Object.keys(completionStates)) {
|
||||||
|
if (addrs[addr] === "error") {
|
||||||
|
errorList.push(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorList.length > 0) {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Failed to invite the following users to the group', '', ErrorDialog, {
|
||||||
|
title: _t("Failed to invite the following users to %(groupId)s:", {groupId: groupId}),
|
||||||
|
description: errorList.join(", "),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
Modal.createTrackedDialog('Group invitations sent', '', QuestionDialog, {
|
||||||
|
title: _t("Invites sent"),
|
||||||
|
description: _t("Your community invitations have been sent."),
|
||||||
|
hasCancelButton: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Failed to invite users to group', '', ErrorDialog, {
|
||||||
|
title: _t("Failed to invite users to community"),
|
||||||
|
description: _t("Failed to invite users to %(groupId)s", {groupId: groupId}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onGroupAddRoomFinished(groupId, addrs) {
|
||||||
|
const matrixClient = MatrixClientPeg.get();
|
||||||
|
const groupStore = GroupStoreCache.getGroupStore(matrixClient, groupId);
|
||||||
|
const errorList = [];
|
||||||
|
return Promise.all(addrs.map((addr) => {
|
||||||
|
return groupStore
|
||||||
|
.addRoomToGroup(addr.address)
|
||||||
|
.catch(() => { errorList.push(addr.address); })
|
||||||
|
.then(() => {
|
||||||
|
const roomId = addr.address;
|
||||||
|
const room = matrixClient.getRoom(roomId);
|
||||||
|
// Can the user change related groups?
|
||||||
|
if (!room || !room.currentState.mayClientSendStateEvent("m.room.related_groups", matrixClient)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Get the related groups
|
||||||
|
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
|
||||||
|
const groups = relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [];
|
||||||
|
|
||||||
|
// Add this group as related
|
||||||
|
if (!groups.includes(groupId)) {
|
||||||
|
groups.push(groupId);
|
||||||
|
return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.related_groups', {groups}, '');
|
||||||
|
}
|
||||||
|
}).reflect();
|
||||||
|
})).then(() => {
|
||||||
|
if (errorList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog(
|
||||||
|
'Failed to add the following room to the group',
|
||||||
|
'', ErrorDialog,
|
||||||
|
{
|
||||||
|
title: _t(
|
||||||
|
"Failed to add the following rooms to %(groupId)s:",
|
||||||
|
{groupId},
|
||||||
|
),
|
||||||
|
description: errorList.join(", "),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
127
src/HtmlUtils.js
127
src/HtmlUtils.js
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 New Vector 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,10 +17,10 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
const React = require('react');
|
||||||
var sanitizeHtml = require('sanitize-html');
|
const sanitizeHtml = require('sanitize-html');
|
||||||
var highlight = require('highlight.js');
|
const highlight = require('highlight.js');
|
||||||
var linkifyMatrix = require('./linkify-matrix');
|
const 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';
|
||||||
|
@ -31,13 +32,33 @@ emojione.imagePathPNG = 'emojione/png/';
|
||||||
// Use SVGs for emojis
|
// Use SVGs for emojis
|
||||||
emojione.imageType = 'svg';
|
emojione.imageType = 'svg';
|
||||||
|
|
||||||
|
// Anything outside the basic multilingual plane will be a surrogate pair
|
||||||
|
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
|
||||||
|
// And there a bunch more symbol characters that emojione has within the
|
||||||
|
// BMP, so this includes the ranges from 'letterlike symbols' to
|
||||||
|
// 'miscellaneous symbols and arrows' which should catch all of them
|
||||||
|
// (with plenty of false positives, but that's OK)
|
||||||
|
const SYMBOL_PATTERN = /([\u2100-\u2bff])/;
|
||||||
|
|
||||||
|
// And this is emojione's complete regex
|
||||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
||||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return true if the given string contains emoji
|
||||||
|
* Uses a much, much simpler regex than emojione's so will give false
|
||||||
|
* positives, but useful for fast-path testing strings to see if they
|
||||||
|
* need emojification.
|
||||||
|
* unicodeToImage uses this function.
|
||||||
|
*/
|
||||||
|
export function containsEmoji(str) {
|
||||||
|
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
|
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
|
||||||
* because we want to include emoji shortnames in title text
|
* because we want to include emoji shortnames in title text
|
||||||
*/
|
*/
|
||||||
export function unicodeToImage(str) {
|
function unicodeToImage(str) {
|
||||||
let replaceWith, unicode, alt, short, fname;
|
let replaceWith, unicode, alt, short, fname;
|
||||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||||
|
|
||||||
|
@ -45,8 +66,7 @@ export function unicodeToImage(str) {
|
||||||
if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
|
if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
|
||||||
// if the unicodeChar doesnt exist just return the entire match
|
// if the unicodeChar doesnt exist just return the entire match
|
||||||
return unicodeChar;
|
return unicodeChar;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// get the unicode codepoint from the actual char
|
// get the unicode codepoint from the actual char
|
||||||
unicode = emojione.jsEscapeMap[unicodeChar];
|
unicode = emojione.jsEscapeMap[unicodeChar];
|
||||||
|
|
||||||
|
@ -127,7 +147,7 @@ export function processHtmlForSending(html: string): string {
|
||||||
* of that HTML.
|
* of that HTML.
|
||||||
*/
|
*/
|
||||||
export function sanitizedHtmlNode(insaneHtml) {
|
export function sanitizedHtmlNode(insaneHtml) {
|
||||||
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
||||||
|
|
||||||
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
||||||
}
|
}
|
||||||
|
@ -136,7 +156,7 @@ 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
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub',
|
||||||
'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', 'img',
|
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
|
||||||
],
|
],
|
||||||
|
@ -152,7 +172,7 @@ const sanitizeHtmlParams = {
|
||||||
// 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', 'magnet'],
|
||||||
|
|
||||||
allowProtocolRelative: false,
|
allowProtocolRelative: false,
|
||||||
|
|
||||||
|
@ -162,21 +182,19 @@ const sanitizeHtmlParams = {
|
||||||
if (attribs.href) {
|
if (attribs.href) {
|
||||||
attribs.target = '_blank'; // by default
|
attribs.target = '_blank'; // by default
|
||||||
|
|
||||||
var m;
|
let m;
|
||||||
// FIXME: horrible duplication with linkify-matrix
|
// FIXME: horrible duplication with linkify-matrix
|
||||||
m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN);
|
m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN);
|
||||||
if (m) {
|
if (m) {
|
||||||
attribs.href = m[1];
|
attribs.href = m[1];
|
||||||
delete attribs.target;
|
delete attribs.target;
|
||||||
}
|
} else {
|
||||||
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];
|
const entity = m[1];
|
||||||
if (entity[0] === '@') {
|
if (entity[0] === '@') {
|
||||||
attribs.href = '#/user/' + entity;
|
attribs.href = '#/user/' + entity;
|
||||||
}
|
} else if (entity[0] === '#' || entity[0] === '!') {
|
||||||
else if (entity[0] === '#' || entity[0] === '!') {
|
|
||||||
attribs.href = '#/room/' + entity;
|
attribs.href = '#/room/' + entity;
|
||||||
}
|
}
|
||||||
delete attribs.target;
|
delete attribs.target;
|
||||||
|
@ -184,7 +202,7 @@ const sanitizeHtmlParams = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
'img': function(tagName, attribs) {
|
||||||
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
||||||
|
@ -203,7 +221,7 @@ const sanitizeHtmlParams = {
|
||||||
'code': function(tagName, attribs) {
|
'code': function(tagName, attribs) {
|
||||||
if (typeof attribs.class !== 'undefined') {
|
if (typeof attribs.class !== 'undefined') {
|
||||||
// Filter out all classes other than ones starting with language- for syntax highlighting.
|
// Filter out all classes other than ones starting with language- for syntax highlighting.
|
||||||
let classes = attribs.class.split(/\s+/).filter(function(cl) {
|
const classes = attribs.class.split(/\s+/).filter(function(cl) {
|
||||||
return cl.startsWith('language-');
|
return cl.startsWith('language-');
|
||||||
});
|
});
|
||||||
attribs.class = classes.join(' ');
|
attribs.class = classes.join(' ');
|
||||||
|
@ -266,11 +284,11 @@ class BaseHighlighter {
|
||||||
* TextHighlighter).
|
* TextHighlighter).
|
||||||
*/
|
*/
|
||||||
applyHighlights(safeSnippet, safeHighlights) {
|
applyHighlights(safeSnippet, safeHighlights) {
|
||||||
var lastOffset = 0;
|
let lastOffset = 0;
|
||||||
var offset;
|
let offset;
|
||||||
var nodes = [];
|
let nodes = [];
|
||||||
|
|
||||||
var safeHighlight = safeHighlights[0];
|
const safeHighlight = safeHighlights[0];
|
||||||
while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
|
while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
|
||||||
// handle preamble
|
// handle preamble
|
||||||
if (offset > lastOffset) {
|
if (offset > lastOffset) {
|
||||||
|
@ -280,7 +298,7 @@ class BaseHighlighter {
|
||||||
|
|
||||||
// do highlight. use the original string rather than safeHighlight
|
// do highlight. use the original string rather than safeHighlight
|
||||||
// to preserve the original casing.
|
// to preserve the original casing.
|
||||||
var endOffset = offset + safeHighlight.length;
|
const endOffset = offset + safeHighlight.length;
|
||||||
nodes.push(this._processSnippet(safeSnippet.substring(offset, endOffset), true));
|
nodes.push(this._processSnippet(safeSnippet.substring(offset, endOffset), true));
|
||||||
|
|
||||||
lastOffset = endOffset;
|
lastOffset = endOffset;
|
||||||
|
@ -298,8 +316,7 @@ class BaseHighlighter {
|
||||||
if (safeHighlights[1]) {
|
if (safeHighlights[1]) {
|
||||||
// recurse into this range to check for the next set of highlight matches
|
// recurse into this range to check for the next set of highlight matches
|
||||||
return this.applyHighlights(safeSnippet, safeHighlights.slice(1));
|
return this.applyHighlights(safeSnippet, safeHighlights.slice(1));
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// no more highlights to be found, just return the unhighlighted string
|
// no more highlights to be found, just return the unhighlighted string
|
||||||
return [this._processSnippet(safeSnippet, false)];
|
return [this._processSnippet(safeSnippet, false)];
|
||||||
}
|
}
|
||||||
|
@ -320,7 +337,7 @@ class HtmlHighlighter extends BaseHighlighter {
|
||||||
return snippet;
|
return snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
var span = "<span class=\""+this.highlightClass+"\">"
|
let span = "<span class=\""+this.highlightClass+"\">"
|
||||||
+ snippet + "</span>";
|
+ snippet + "</span>";
|
||||||
|
|
||||||
if (this.highlightLink) {
|
if (this.highlightLink) {
|
||||||
|
@ -345,15 +362,15 @@ class TextHighlighter extends BaseHighlighter {
|
||||||
* returns a React node
|
* returns a React node
|
||||||
*/
|
*/
|
||||||
_processSnippet(snippet, highlight) {
|
_processSnippet(snippet, highlight) {
|
||||||
var key = this._key++;
|
const key = this._key++;
|
||||||
|
|
||||||
var node =
|
let node =
|
||||||
<span key={key} className={highlight ? this.highlightClass : null }>
|
<span key={key} className={highlight ? this.highlightClass : null}>
|
||||||
{ snippet }
|
{ snippet }
|
||||||
</span>;
|
</span>;
|
||||||
|
|
||||||
if (highlight && this.highlightLink) {
|
if (highlight && this.highlightLink) {
|
||||||
node = <a key={key} href={this.highlightLink}>{node}</a>;
|
node = <a key={key} href={this.highlightLink}>{ node }</a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
|
@ -368,22 +385,23 @@ class TextHighlighter extends BaseHighlighter {
|
||||||
* highlights: optional list of words to highlight, ordered by longest word first
|
* highlights: optional list of words to highlight, ordered by longest word first
|
||||||
*
|
*
|
||||||
* opts.highlightLink: optional href to add to highlighted words
|
* opts.highlightLink: optional href to add to highlighted words
|
||||||
|
* opts.disableBigEmoji: optional argument to disable the big emoji class.
|
||||||
*/
|
*/
|
||||||
export function bodyToHtml(content, highlights, opts) {
|
export function bodyToHtml(content, highlights, opts={}) {
|
||||||
opts = opts || {};
|
const isHtml = (content.format === "org.matrix.custom.html");
|
||||||
|
const body = isHtml ? content.formatted_body : escape(content.body);
|
||||||
|
|
||||||
var isHtml = (content.format === "org.matrix.custom.html");
|
let bodyHasEmoji = false;
|
||||||
let body = isHtml ? content.formatted_body : escape(content.body);
|
|
||||||
|
|
||||||
var safeBody;
|
let safeBody;
|
||||||
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
||||||
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
|
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
|
||||||
// are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
|
// are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
|
||||||
// by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
|
// by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
|
||||||
try {
|
try {
|
||||||
if (highlights && highlights.length > 0) {
|
if (highlights && highlights.length > 0) {
|
||||||
var highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
||||||
var safeHighlights = highlights.map(function(highlight) {
|
const safeHighlights = highlights.map(function(highlight) {
|
||||||
return sanitizeHtml(highlight, sanitizeHtmlParams);
|
return sanitizeHtml(highlight, sanitizeHtmlParams);
|
||||||
});
|
});
|
||||||
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
|
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
|
||||||
|
@ -392,17 +410,19 @@ export function bodyToHtml(content, highlights, opts) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
safeBody = sanitizeHtml(body, sanitizeHtmlParams);
|
safeBody = sanitizeHtml(body, sanitizeHtmlParams);
|
||||||
safeBody = unicodeToImage(safeBody);
|
bodyHasEmoji = containsEmoji(body);
|
||||||
safeBody = addCodeCopyButton(safeBody);
|
if (bodyHasEmoji) safeBody = unicodeToImage(safeBody);
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
delete sanitizeHtmlParams.textFilter;
|
delete sanitizeHtmlParams.textFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
EMOJI_REGEX.lastIndex = 0;
|
let emojiBody = false;
|
||||||
let contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
|
if (!opts.disableBigEmoji && bodyHasEmoji) {
|
||||||
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
EMOJI_REGEX.lastIndex = 0;
|
||||||
let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
const contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
|
||||||
|
const match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||||
|
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
||||||
|
}
|
||||||
|
|
||||||
const className = classNames({
|
const className = classNames({
|
||||||
'mx_EventTile_body': true,
|
'mx_EventTile_body': true,
|
||||||
|
@ -412,23 +432,6 @@ export function bodyToHtml(content, highlights, opts) {
|
||||||
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" />;
|
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) {
|
||||||
return {
|
return {
|
||||||
__html: unicodeToImage(escape(text)),
|
__html: unicodeToImage(escape(text)),
|
||||||
|
|
|
@ -42,13 +42,12 @@ module.exports = {
|
||||||
// no scaling needs to be applied
|
// no scaling needs to be applied
|
||||||
return fullHeight;
|
return fullHeight;
|
||||||
}
|
}
|
||||||
var widthMulti = thumbWidth / fullWidth;
|
const widthMulti = thumbWidth / fullWidth;
|
||||||
var heightMulti = thumbHeight / fullHeight;
|
const heightMulti = thumbHeight / fullHeight;
|
||||||
if (widthMulti < heightMulti) {
|
if (widthMulti < heightMulti) {
|
||||||
// width is the dominant dimension so scaling will be fixed on that
|
// width is the dominant dimension so scaling will be fixed on that
|
||||||
return Math.floor(widthMulti * fullHeight);
|
return Math.floor(widthMulti * fullHeight);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// height is the dominant dimension so scaling will be fixed on that
|
// height is the dominant dimension so scaling will be fixed on that
|
||||||
return Math.floor(heightMulti * fullHeight);
|
return Math.floor(heightMulti * fullHeight);
|
||||||
}
|
}
|
||||||
|
|
16
src/Login.js
16
src/Login.js
|
@ -59,8 +59,8 @@ export default class Login {
|
||||||
}
|
}
|
||||||
|
|
||||||
getFlows() {
|
getFlows() {
|
||||||
var self = this;
|
const self = this;
|
||||||
var client = this._createTemporaryClient();
|
const client = this._createTemporaryClient();
|
||||||
return client.loginFlows().then(function(result) {
|
return client.loginFlows().then(function(result) {
|
||||||
self._flows = result.flows;
|
self._flows = result.flows;
|
||||||
self._currentFlowIndex = 0;
|
self._currentFlowIndex = 0;
|
||||||
|
@ -77,12 +77,12 @@ export default class Login {
|
||||||
getCurrentFlowStep() {
|
getCurrentFlowStep() {
|
||||||
// technically the flow can have multiple steps, but no one does this
|
// technically the flow can have multiple steps, but no one does this
|
||||||
// for login so we can ignore it.
|
// for login so we can ignore it.
|
||||||
var flowStep = this._flows[this._currentFlowIndex];
|
const flowStep = this._flows[this._currentFlowIndex];
|
||||||
return flowStep ? flowStep.type : null;
|
return flowStep ? flowStep.type : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
loginAsGuest() {
|
loginAsGuest() {
|
||||||
var client = this._createTemporaryClient();
|
const client = this._createTemporaryClient();
|
||||||
return client.registerGuest({
|
return client.registerGuest({
|
||||||
body: {
|
body: {
|
||||||
initial_device_display_name: this._defaultDeviceDisplayName,
|
initial_device_display_name: this._defaultDeviceDisplayName,
|
||||||
|
@ -94,7 +94,7 @@ export default class Login {
|
||||||
accessToken: creds.access_token,
|
accessToken: creds.access_token,
|
||||||
homeserverUrl: this._hsUrl,
|
homeserverUrl: this._hsUrl,
|
||||||
identityServerUrl: this._isUrl,
|
identityServerUrl: this._isUrl,
|
||||||
guest: true
|
guest: true,
|
||||||
};
|
};
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -149,12 +149,12 @@ export default class Login {
|
||||||
identityServerUrl: self._isUrl,
|
identityServerUrl: self._isUrl,
|
||||||
userId: data.user_id,
|
userId: data.user_id,
|
||||||
deviceId: data.device_id,
|
deviceId: data.device_id,
|
||||||
accessToken: data.access_token
|
accessToken: data.access_token,
|
||||||
});
|
});
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
if (error.httpStatus === 403) {
|
if (error.httpStatus === 403) {
|
||||||
if (self._fallbackHsUrl) {
|
if (self._fallbackHsUrl) {
|
||||||
var fbClient = Matrix.createClient({
|
const fbClient = Matrix.createClient({
|
||||||
baseUrl: self._fallbackHsUrl,
|
baseUrl: self._fallbackHsUrl,
|
||||||
idBaseUrl: this._isUrl,
|
idBaseUrl: this._isUrl,
|
||||||
});
|
});
|
||||||
|
@ -165,7 +165,7 @@ export default class Login {
|
||||||
identityServerUrl: self._isUrl,
|
identityServerUrl: self._isUrl,
|
||||||
userId: data.user_id,
|
userId: data.user_id,
|
||||||
deviceId: data.device_id,
|
deviceId: data.device_id,
|
||||||
accessToken: data.access_token
|
accessToken: data.access_token,
|
||||||
});
|
});
|
||||||
}, function(fallback_error) {
|
}, function(fallback_error) {
|
||||||
// throw the original error
|
// throw the original error
|
||||||
|
|
|
@ -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', 'u'];
|
const ALLOWED_HTML_TAGS = ['sub', 'sup', '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'];
|
||||||
|
@ -48,7 +48,7 @@ function html_if_tag_allowed(node) {
|
||||||
* or false if it is only a single line.
|
* or false if it is only a single line.
|
||||||
*/
|
*/
|
||||||
function is_multi_line(node) {
|
function is_multi_line(node) {
|
||||||
var par = node;
|
let par = node;
|
||||||
while (par.parent) {
|
while (par.parent) {
|
||||||
par = par.parent;
|
par = par.parent;
|
||||||
}
|
}
|
||||||
|
@ -143,7 +143,7 @@ export default class Markdown {
|
||||||
if (isMultiLine) this.cr();
|
if (isMultiLine) this.cr();
|
||||||
html_if_tag_allowed.call(this, node);
|
html_if_tag_allowed.call(this, node);
|
||||||
if (isMultiLine) this.cr();
|
if (isMultiLine) this.cr();
|
||||||
}
|
};
|
||||||
|
|
||||||
return renderer.render(this.parsed);
|
return renderer.render(this.parsed);
|
||||||
}
|
}
|
||||||
|
@ -178,7 +178,7 @@ export default class Markdown {
|
||||||
renderer.html_block = function(node) {
|
renderer.html_block = function(node) {
|
||||||
this.lit(node.literal);
|
this.lit(node.literal);
|
||||||
if (is_multi_line(node) && node.next) this.lit('\n\n');
|
if (is_multi_line(node) && node.next) this.lit('\n\n');
|
||||||
}
|
};
|
||||||
|
|
||||||
return renderer.render(this.parsed);
|
return renderer.render(this.parsed);
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ class MatrixClientPeg {
|
||||||
opts.pendingEventOrdering = "detached";
|
opts.pendingEventOrdering = "detached";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let promise = this.matrixClient.store.startup();
|
const promise = this.matrixClient.store.startup();
|
||||||
console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
|
console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
|
||||||
await promise;
|
await promise;
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
|
@ -136,7 +136,7 @@ class MatrixClientPeg {
|
||||||
}
|
}
|
||||||
|
|
||||||
_createClient(creds: MatrixClientCreds) {
|
_createClient(creds: MatrixClientCreds) {
|
||||||
var opts = {
|
const opts = {
|
||||||
baseUrl: creds.homeserverUrl,
|
baseUrl: creds.homeserverUrl,
|
||||||
idBaseUrl: creds.identityServerUrl,
|
idBaseUrl: creds.identityServerUrl,
|
||||||
accessToken: creds.accessToken,
|
accessToken: creds.accessToken,
|
||||||
|
@ -153,8 +153,8 @@ class MatrixClientPeg {
|
||||||
|
|
||||||
this.matrixClient.setGuest(Boolean(creds.guest));
|
this.matrixClient.setGuest(Boolean(creds.guest));
|
||||||
|
|
||||||
var notifTimelineSet = new EventTimelineSet(null, {
|
const notifTimelineSet = new EventTimelineSet(null, {
|
||||||
timelineSupport: true
|
timelineSupport: true,
|
||||||
});
|
});
|
||||||
// XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync.
|
// XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync.
|
||||||
notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);
|
notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);
|
||||||
|
|
22
src/Modal.js
22
src/Modal.js
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
const React = require('react');
|
||||||
var ReactDOM = require('react-dom');
|
const ReactDOM = require('react-dom');
|
||||||
import Analytics from './Analytics';
|
import Analytics from './Analytics';
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
|
|
||||||
|
@ -137,15 +137,15 @@ class ModalManager {
|
||||||
* @param {String} className CSS class to apply to the modal wrapper
|
* @param {String} className CSS class to apply to the modal wrapper
|
||||||
*/
|
*/
|
||||||
createDialogAsync(loader, props, className) {
|
createDialogAsync(loader, props, className) {
|
||||||
var self = this;
|
const self = this;
|
||||||
const modal = {};
|
const modal = {};
|
||||||
|
|
||||||
// never call this from onFinished() otherwise it will loop
|
// never call this from onFinished() otherwise it will loop
|
||||||
//
|
//
|
||||||
// nb explicit function() rather than arrow function, to get `arguments`
|
// nb explicit function() rather than arrow function, to get `arguments`
|
||||||
var closeDialog = function() {
|
const closeDialog = function() {
|
||||||
if (props && props.onFinished) props.onFinished.apply(null, arguments);
|
if (props && props.onFinished) props.onFinished.apply(null, arguments);
|
||||||
var i = self._modals.indexOf(modal);
|
const i = self._modals.indexOf(modal);
|
||||||
if (i >= 0) {
|
if (i >= 0) {
|
||||||
self._modals.splice(i, 1);
|
self._modals.splice(i, 1);
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,7 @@ class ModalManager {
|
||||||
// property set here so you can't close the dialog from a button click!
|
// property set here so you can't close the dialog from a button click!
|
||||||
modal.elem = (
|
modal.elem = (
|
||||||
<AsyncWrapper key={modalCount} loader={loader} {...props}
|
<AsyncWrapper key={modalCount} loader={loader} {...props}
|
||||||
onFinished={closeDialog}/>
|
onFinished={closeDialog} />
|
||||||
);
|
);
|
||||||
modal.onFinished = props ? props.onFinished : null;
|
modal.onFinished = props ? props.onFinished : null;
|
||||||
modal.className = className;
|
modal.className = className;
|
||||||
|
@ -191,13 +191,13 @@ class ModalManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var modal = this._modals[0];
|
const modal = this._modals[0];
|
||||||
var dialog = (
|
const dialog = (
|
||||||
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '') }>
|
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
{modal.elem}
|
{ modal.elem }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_background" onClick={ this.closeAll }></div>
|
<div className="mx_Dialog_background" onClick={this.closeAll}></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2017 New Vector 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.
|
||||||
|
@ -79,10 +80,11 @@ const Notifier = {
|
||||||
if (ev.getContent().body) msg = ev.getContent().body;
|
if (ev.getContent().body) msg = ev.getContent().body;
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
|
if (!this.isBodyEnabled()) {
|
||||||
ev.sender, 40, 40, 'crop'
|
msg = '';
|
||||||
) : null;
|
}
|
||||||
|
|
||||||
|
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop') : null;
|
||||||
const notif = plaf.displayNotification(title, msg, avatarUrl, room);
|
const notif = plaf.displayNotification(title, msg, avatarUrl, room);
|
||||||
|
|
||||||
// if displayNotification returns non-null, the platform supports
|
// if displayNotification returns non-null, the platform supports
|
||||||
|
@ -96,17 +98,16 @@ const Notifier = {
|
||||||
_playAudioNotification: function(ev, room) {
|
_playAudioNotification: function(ev, room) {
|
||||||
const e = document.getElementById("messageAudio");
|
const e = document.getElementById("messageAudio");
|
||||||
if (e) {
|
if (e) {
|
||||||
e.load();
|
|
||||||
e.play();
|
e.play();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
start: function() {
|
start: function() {
|
||||||
this.boundOnRoomTimeline = this.onRoomTimeline.bind(this);
|
this.boundOnEvent = this.onEvent.bind(this);
|
||||||
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
|
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
|
||||||
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
|
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
|
||||||
this.boundOnEventDecrypted = this.onEventDecrypted.bind(this);
|
this.boundOnEventDecrypted = this.onEventDecrypted.bind(this);
|
||||||
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
|
MatrixClientPeg.get().on('event', this.boundOnEvent);
|
||||||
MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt);
|
MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt);
|
||||||
MatrixClientPeg.get().on('Event.decrypted', this.boundOnEventDecrypted);
|
MatrixClientPeg.get().on('Event.decrypted', this.boundOnEventDecrypted);
|
||||||
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
|
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
|
||||||
|
@ -116,7 +117,7 @@ const 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('Event', this.boundOnEvent);
|
||||||
MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt);
|
MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt);
|
||||||
MatrixClientPeg.get().removeListener('Event.decrypted', this.boundOnEventDecrypted);
|
MatrixClientPeg.get().removeListener('Event.decrypted', this.boundOnEventDecrypted);
|
||||||
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
||||||
|
@ -195,6 +196,19 @@ const Notifier = {
|
||||||
return enabled === 'true';
|
return enabled === 'true';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setBodyEnabled: function(enable) {
|
||||||
|
if (!global.localStorage) return;
|
||||||
|
global.localStorage.setItem('notifications_body_enabled', enable ? 'true' : 'false');
|
||||||
|
},
|
||||||
|
|
||||||
|
isBodyEnabled: function() {
|
||||||
|
if (!global.localStorage) return true;
|
||||||
|
const enabled = global.localStorage.getItem('notifications_body_enabled');
|
||||||
|
// default to true if the popups are enabled
|
||||||
|
if (enabled === null) return this.isEnabled();
|
||||||
|
return enabled === 'true';
|
||||||
|
},
|
||||||
|
|
||||||
setAudioEnabled: function(enable) {
|
setAudioEnabled: function(enable) {
|
||||||
if (!global.localStorage) return;
|
if (!global.localStorage) return;
|
||||||
global.localStorage.setItem('audio_notifications_enabled',
|
global.localStorage.setItem('audio_notifications_enabled',
|
||||||
|
@ -247,12 +261,9 @@ const Notifier = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
onEvent: function(ev) {
|
||||||
if (toStartOfTimeline) 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 it's an encrypted event and the type is still 'm.room.encrypted',
|
// If it's an encrypted event and the type is still 'm.room.encrypted',
|
||||||
// it hasn't yet been decrypted, so wait until it is.
|
// it hasn't yet been decrypted, so wait until it is.
|
||||||
|
@ -306,7 +317,7 @@ const Notifier = {
|
||||||
this._playAudioNotification(ev, room);
|
this._playAudioNotification(ev, room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!global.mxNotifier) {
|
if (!global.mxNotifier) {
|
||||||
|
|
|
@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
const MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
var dis = require("./dispatcher");
|
const dis = require("./dispatcher");
|
||||||
|
|
||||||
// Time in ms after that a user is considered as unavailable/away
|
// Time in ms after that a user is considered as unavailable/away
|
||||||
var UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
|
const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
|
||||||
var PRESENCE_STATES = ["online", "offline", "unavailable"];
|
const PRESENCE_STATES = ["online", "offline", "unavailable"];
|
||||||
|
|
||||||
class Presence {
|
class Presence {
|
||||||
|
|
||||||
|
@ -71,14 +71,14 @@ class Presence {
|
||||||
if (!this.running) {
|
if (!this.running) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var old_state = this.state;
|
const old_state = this.state;
|
||||||
this.state = newState;
|
this.state = newState;
|
||||||
|
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
return; // don't try to set presence when a guest; it won't work.
|
return; // don't try to set presence when a guest; it won't work.
|
||||||
}
|
}
|
||||||
|
|
||||||
var self = this;
|
const self = this;
|
||||||
MatrixClientPeg.get().setPresence(this.state).done(function() {
|
MatrixClientPeg.get().setPresence(this.state).done(function() {
|
||||||
console.log("Presence: %s", newState);
|
console.log("Presence: %s", newState);
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
|
@ -104,7 +104,7 @@ class Presence {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_resetTimer() {
|
_resetTimer() {
|
||||||
var self = this;
|
const self = this;
|
||||||
this.setState("online");
|
this.setState("online");
|
||||||
// Re-arm the timer
|
// Re-arm the timer
|
||||||
clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
|
|
|
@ -44,9 +44,9 @@ export const contentStateToHTML = (contentState: ContentState) => {
|
||||||
return stateToHTML(contentState, {
|
return stateToHTML(contentState, {
|
||||||
inlineStyles: {
|
inlineStyles: {
|
||||||
UNDERLINE: {
|
UNDERLINE: {
|
||||||
element: 'u'
|
element: 'u',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ function unicodeToEmojiUri(str) {
|
||||||
let replaceWith, unicode, alt;
|
let replaceWith, unicode, alt;
|
||||||
if ((!emojione.unicodeAlt) || (emojione.sprites)) {
|
if ((!emojione.unicodeAlt) || (emojione.sprites)) {
|
||||||
// if we are using the shortname as the alt tag then we need a reversed array to map unicode code point to shortnames
|
// if we are using the shortname as the alt tag then we need a reversed array to map unicode code point to shortnames
|
||||||
let mappedUnicode = emojione.mapUnicodeToShort();
|
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||||
}
|
}
|
||||||
|
|
||||||
str = str.replace(emojione.regUnicode, function(unicodeChar) {
|
str = str.replace(emojione.regUnicode, function(unicodeChar) {
|
||||||
|
@ -67,8 +67,14 @@ function unicodeToEmojiUri(str) {
|
||||||
// if the unicodeChar doesnt exist just return the entire match
|
// if the unicodeChar doesnt exist just return the entire match
|
||||||
return unicodeChar;
|
return unicodeChar;
|
||||||
} else {
|
} else {
|
||||||
|
// Remove variant selector VS16 (explicitly emoji) as it is unnecessary and leads to an incorrect URL below
|
||||||
|
if(unicodeChar.length == 2 && unicodeChar[1] == '\ufe0f') {
|
||||||
|
unicodeChar = unicodeChar[0];
|
||||||
|
}
|
||||||
|
|
||||||
// get the unicode codepoint from the actual char
|
// get the unicode codepoint from the actual char
|
||||||
unicode = emojione.jsEscapeMap[unicodeChar];
|
unicode = emojione.jsEscapeMap[unicodeChar];
|
||||||
|
|
||||||
return emojione.imagePathSVG+unicode+'.svg'+emojione.cacheBustParam;
|
return emojione.imagePathSVG+unicode+'.svg'+emojione.cacheBustParam;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -90,14 +96,14 @@ function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: numb
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workaround for https://github.com/facebook/draft-js/issues/414
|
// Workaround for https://github.com/facebook/draft-js/issues/414
|
||||||
let emojiDecorator = {
|
const emojiDecorator = {
|
||||||
strategy: (contentState, contentBlock, callback) => {
|
strategy: (contentState, contentBlock, callback) => {
|
||||||
findWithRegex(EMOJI_REGEX, contentBlock, callback);
|
findWithRegex(EMOJI_REGEX, contentBlock, callback);
|
||||||
},
|
},
|
||||||
component: (props) => {
|
component: (props) => {
|
||||||
let uri = unicodeToEmojiUri(props.children[0].props.text);
|
const uri = unicodeToEmojiUri(props.children[0].props.text);
|
||||||
let shortname = emojione.toShort(props.children[0].props.text);
|
const shortname = emojione.toShort(props.children[0].props.text);
|
||||||
let style = {
|
const style = {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
width: '1em',
|
width: '1em',
|
||||||
maxHeight: '1em',
|
maxHeight: '1em',
|
||||||
|
@ -106,7 +112,7 @@ let emojiDecorator = {
|
||||||
backgroundPosition: 'center center',
|
backgroundPosition: 'center center',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
};
|
};
|
||||||
return (<span title={shortname} style={style}><span style={{opacity: 0}}>{props.children}</span></span>);
|
return (<span title={shortname} style={style}><span style={{opacity: 0}}>{ props.children }</span></span>);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -118,16 +124,16 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||||
let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
|
const markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
|
||||||
(style) => ({
|
(style) => ({
|
||||||
strategy: (contentState, contentBlock, callback) => {
|
strategy: (contentState, contentBlock, callback) => {
|
||||||
return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);
|
return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);
|
||||||
},
|
},
|
||||||
component: (props) => (
|
component: (props) => (
|
||||||
<span className={"mx_MarkdownElement mx_Markdown_" + style}>
|
<span className={"mx_MarkdownElement mx_Markdown_" + style}>
|
||||||
{props.children}
|
{ props.children }
|
||||||
</span>
|
</span>
|
||||||
)
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
markdownDecorators.push({
|
markdownDecorators.push({
|
||||||
|
@ -136,9 +142,9 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||||
},
|
},
|
||||||
component: (props) => (
|
component: (props) => (
|
||||||
<a href="#" className="mx_MarkdownElement mx_Markdown_LINK">
|
<a href="#" className="mx_MarkdownElement mx_Markdown_LINK">
|
||||||
{props.children}
|
{ props.children }
|
||||||
</a>
|
</a>
|
||||||
)
|
),
|
||||||
});
|
});
|
||||||
// markdownDecorators.push(emojiDecorator);
|
// markdownDecorators.push(emojiDecorator);
|
||||||
// TODO Consider renabling "syntax highlighting" when we can do it properly
|
// TODO Consider renabling "syntax highlighting" when we can do it properly
|
||||||
|
@ -161,7 +167,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
|
||||||
for (let currentKey = startKey;
|
for (let currentKey = startKey;
|
||||||
currentKey && currentKey !== endKey;
|
currentKey && currentKey !== endKey;
|
||||||
currentKey = contentState.getKeyAfter(currentKey)) {
|
currentKey = contentState.getKeyAfter(currentKey)) {
|
||||||
let blockText = getText(currentKey);
|
const blockText = getText(currentKey);
|
||||||
text += blockText.substring(startOffset, blockText.length);
|
text += blockText.substring(startOffset, blockText.length);
|
||||||
|
|
||||||
// from now on, we'll take whole blocks
|
// from now on, we'll take whole blocks
|
||||||
|
@ -182,7 +188,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
|
||||||
export function selectionStateToTextOffsets(selectionState: SelectionState,
|
export function selectionStateToTextOffsets(selectionState: SelectionState,
|
||||||
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
|
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
|
||||||
let offset = 0, start = 0, end = 0;
|
let offset = 0, start = 0, end = 0;
|
||||||
for (let block of contentBlocks) {
|
for (const block of contentBlocks) {
|
||||||
if (selectionState.getStartKey() === block.getKey()) {
|
if (selectionState.getStartKey() === block.getKey()) {
|
||||||
start = offset + selectionState.getStartOffset();
|
start = offset + selectionState.getStartOffset();
|
||||||
}
|
}
|
||||||
|
@ -259,7 +265,7 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor
|
||||||
.set('focusOffset', end);
|
.set('focusOffset', end);
|
||||||
const emojiText = plainText.substring(start, end);
|
const emojiText = plainText.substring(start, end);
|
||||||
newContentState = newContentState.createEntity(
|
newContentState = newContentState.createEntity(
|
||||||
'emoji', 'IMMUTABLE', { emojiUnicode: emojiText }
|
'emoji', 'IMMUTABLE', { emojiUnicode: emojiText },
|
||||||
);
|
);
|
||||||
const entityKey = newContentState.getLastCreatedEntityKey();
|
const entityKey = newContentState.getLastCreatedEntityKey();
|
||||||
newContentState = Modifier.replaceText(
|
newContentState = Modifier.replaceText(
|
||||||
|
|
|
@ -28,7 +28,7 @@ export function inviteToRoom(roomId, addr) {
|
||||||
|
|
||||||
if (addrType == 'email') {
|
if (addrType == 'email') {
|
||||||
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
||||||
} else if (addrType == 'mx') {
|
} else if (addrType == 'mx-user-id') {
|
||||||
return MatrixClientPeg.get().invite(roomId, addr);
|
return MatrixClientPeg.get().invite(roomId, addr);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unsupported address');
|
throw new Error('Unsupported address');
|
||||||
|
@ -50,8 +50,8 @@ export function inviteMultipleToRoom(roomId, addrs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showStartChatInviteDialog() {
|
export function showStartChatInviteDialog() {
|
||||||
const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog");
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
Modal.createTrackedDialog('Start a chat', '', UserPickerDialog, {
|
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
|
||||||
title: _t('Start a chat'),
|
title: _t('Start a chat'),
|
||||||
description: _t("Who would you like to communicate with?"),
|
description: _t("Who would you like to communicate with?"),
|
||||||
placeholder: _t("Email, name or matrix ID"),
|
placeholder: _t("Email, name or matrix ID"),
|
||||||
|
@ -61,8 +61,8 @@ export function showStartChatInviteDialog() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showRoomInviteDialog(roomId) {
|
export function showRoomInviteDialog(roomId) {
|
||||||
const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog");
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
Modal.createTrackedDialog('Chat Invite', '', UserPickerDialog, {
|
Modal.createTrackedDialog('Chat Invite', '', AddressPickerDialog, {
|
||||||
title: _t('Invite new room members'),
|
title: _t('Invite new room members'),
|
||||||
description: _t('Who would you like to add to this room?'),
|
description: _t('Who would you like to add to this room?'),
|
||||||
button: _t('Send Invites'),
|
button: _t('Send Invites'),
|
||||||
|
@ -127,7 +127,7 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function _isDmChat(addrTexts) {
|
function _isDmChat(addrTexts) {
|
||||||
if (addrTexts.length === 1 && getAddressType(addrTexts[0])) {
|
if (addrTexts.length === 1 && getAddressType(addrTexts[0]) === 'mx') {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
|
@ -62,8 +62,7 @@ export function isConfCallRoom(room, me, conferenceHandler) {
|
||||||
|
|
||||||
export function looksLikeDirectMessageRoom(room, me) {
|
export function looksLikeDirectMessageRoom(room, me) {
|
||||||
if (me.membership == "join" || me.membership === "ban" ||
|
if (me.membership == "join" || me.membership === "ban" ||
|
||||||
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey()))
|
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) {
|
||||||
{
|
|
||||||
// Used to split rooms via tags
|
// Used to split rooms via tags
|
||||||
const tagNames = Object.keys(room.tags);
|
const tagNames = Object.keys(room.tags);
|
||||||
// Used for 1:1 direct chats
|
// Used for 1:1 direct chats
|
||||||
|
|
|
@ -15,10 +15,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
var request = require('browser-request');
|
const request = require('browser-request');
|
||||||
|
|
||||||
var SdkConfig = require('./SdkConfig');
|
const SdkConfig = require('./SdkConfig');
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
const MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
|
|
||||||
class ScalarAuthClient {
|
class ScalarAuthClient {
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ class ScalarAuthClient {
|
||||||
|
|
||||||
// Returns a scalar_token string
|
// Returns a scalar_token string
|
||||||
getScalarToken() {
|
getScalarToken() {
|
||||||
var tok = window.localStorage.getItem("mx_scalar_token");
|
const tok = window.localStorage.getItem("mx_scalar_token");
|
||||||
if (tok) return Promise.resolve(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
|
||||||
|
@ -53,9 +53,9 @@ class ScalarAuthClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
exchangeForScalarToken(openid_token_object) {
|
exchangeForScalarToken(openid_token_object) {
|
||||||
var defer = Promise.defer();
|
const defer = Promise.defer();
|
||||||
|
|
||||||
var scalar_rest_url = SdkConfig.get().integrations_rest_url;
|
const scalar_rest_url = SdkConfig.get().integrations_rest_url;
|
||||||
request({
|
request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
uri: scalar_rest_url+'/register',
|
uri: scalar_rest_url+'/register',
|
||||||
|
@ -77,7 +77,7 @@ class ScalarAuthClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
getScalarInterfaceUrlForRoom(roomId, screen, id) {
|
getScalarInterfaceUrlForRoom(roomId, screen, id) {
|
||||||
var url = SdkConfig.get().integrations_ui_url;
|
let 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 (id) {
|
if (id) {
|
||||||
|
|
|
@ -356,12 +356,12 @@ function getWidgets(event, roomId) {
|
||||||
}
|
}
|
||||||
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
||||||
// Only return widgets which have required fields
|
// Only return widgets which have required fields
|
||||||
let widgetStateEvents = [];
|
const widgetStateEvents = [];
|
||||||
stateEvents.forEach((ev) => {
|
stateEvents.forEach((ev) => {
|
||||||
if (ev.getContent().type && ev.getContent().url) {
|
if (ev.getContent().type && ev.getContent().url) {
|
||||||
widgetStateEvents.push(ev.event); // return the raw event
|
widgetStateEvents.push(ev.event); // return the raw event
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
sendResponse(event, widgetStateEvents);
|
sendResponse(event, widgetStateEvents);
|
||||||
}
|
}
|
||||||
|
@ -376,7 +376,7 @@ function setPlumbingState(event, roomId, status) {
|
||||||
sendError(event, _t('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(() => {
|
||||||
sendResponse(event, {
|
sendResponse(event, {
|
||||||
success: true,
|
success: true,
|
||||||
});
|
});
|
||||||
|
@ -415,11 +415,11 @@ function setBotPower(event, roomId, userId, level) {
|
||||||
}
|
}
|
||||||
|
|
||||||
client.getStateEvent(roomId, "m.room.power_levels", "").then((powerLevels) => {
|
client.getStateEvent(roomId, "m.room.power_levels", "").then((powerLevels) => {
|
||||||
let powerEvent = new MatrixEvent(
|
const powerEvent = new MatrixEvent(
|
||||||
{
|
{
|
||||||
type: "m.room.power_levels",
|
type: "m.room.power_levels",
|
||||||
content: powerLevels,
|
content: powerLevels,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
client.setPowerLevel(roomId, userId, level, powerEvent).done(() => {
|
client.setPowerLevel(roomId, userId, level, powerEvent).done(() => {
|
||||||
|
@ -485,8 +485,7 @@ function canSendEvent(event, roomId) {
|
||||||
let canSend = false;
|
let canSend = false;
|
||||||
if (isState) {
|
if (isState) {
|
||||||
canSend = room.currentState.maySendStateEvent(evType, me);
|
canSend = room.currentState.maySendStateEvent(evType, me);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
canSend = room.currentState.maySendEvent(evType, me);
|
canSend = room.currentState.maySendEvent(evType, me);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -517,8 +516,8 @@ function returnStateEvent(event, roomId, eventType, stateKey) {
|
||||||
sendResponse(event, stateEvent.getContent());
|
sendResponse(event, stateEvent.getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentRoomId = null;
|
let currentRoomId = null;
|
||||||
var currentRoomAlias = null;
|
let currentRoomAlias = null;
|
||||||
|
|
||||||
// Listen for when a room is viewed
|
// Listen for when a room is viewed
|
||||||
dis.register(onAction);
|
dis.register(onAction);
|
||||||
|
@ -542,7 +541,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;
|
const url = SdkConfig.get().integrations_ui_url;
|
||||||
if (event.origin.length === 0 || !url.startsWith(event.origin) || !event.data.action) {
|
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
|
||||||
}
|
}
|
||||||
|
@ -647,7 +646,7 @@ module.exports = {
|
||||||
// Make an error so we get a stack trace
|
// Make an error so we get a stack trace
|
||||||
const e = new Error(
|
const e = new Error(
|
||||||
"ScalarMessaging: mismatched startListening / stopListening detected." +
|
"ScalarMessaging: mismatched startListening / stopListening detected." +
|
||||||
" Negative count"
|
" Negative count",
|
||||||
);
|
);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,9 @@ class Skinner {
|
||||||
// behaviour with multiple copies of files etc. is erratic at best.
|
// behaviour with multiple copies of files etc. is erratic at best.
|
||||||
// XXX: We can still end up with the same file twice in the resulting
|
// XXX: We can still end up with the same file twice in the resulting
|
||||||
// JS bundle which is nonideal.
|
// JS bundle which is nonideal.
|
||||||
|
// See https://derickbailey.com/2016/03/09/creating-a-true-singleton-in-node-js-with-es6-symbols/
|
||||||
|
// or https://nodejs.org/api/modules.html#modules_module_caching_caveats
|
||||||
|
// ("Modules are cached based on their resolved filename")
|
||||||
if (global.mxSkinner === undefined) {
|
if (global.mxSkinner === undefined) {
|
||||||
global.mxSkinner = new Skinner();
|
global.mxSkinner = new Skinner();
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,6 +240,59 @@ const commands = {
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
ignore: new Command("ignore", "<userId>", function(roomId, args) {
|
||||||
|
if (args) {
|
||||||
|
const matches = args.match(/^(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
const userId = matches[1];
|
||||||
|
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||||
|
ignoredUsers.push(userId); // de-duped internally in the js-sdk
|
||||||
|
return success(
|
||||||
|
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
|
||||||
|
title: _t("Ignored user"),
|
||||||
|
description: (
|
||||||
|
<div>
|
||||||
|
<p>{ _t("You are now ignoring %(userId)s", {userId: userId}) }</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
hasCancelButton: false,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject(this.getUsage());
|
||||||
|
}),
|
||||||
|
|
||||||
|
unignore: new Command("unignore", "<userId>", function(roomId, args) {
|
||||||
|
if (args) {
|
||||||
|
const matches = args.match(/^(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
const userId = matches[1];
|
||||||
|
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||||
|
const index = ignoredUsers.indexOf(userId);
|
||||||
|
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||||
|
return success(
|
||||||
|
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
|
||||||
|
title: _t("Unignored user"),
|
||||||
|
description: (
|
||||||
|
<div>
|
||||||
|
<p>{ _t("You are no longer ignoring %(userId)s", {userId: userId}) }</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
hasCancelButton: false,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject(this.getUsage());
|
||||||
|
}),
|
||||||
|
|
||||||
// Define the power level of a user
|
// Define the power level of a user
|
||||||
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
|
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
|
@ -292,6 +345,13 @@ const commands = {
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Open developer tools
|
||||||
|
devtools: new Command("devtools", "", function(roomId) {
|
||||||
|
const DevtoolsDialog = sdk.getComponent("dialogs.DevtoolsDialog");
|
||||||
|
Modal.createDialog(DevtoolsDialog, { roomId });
|
||||||
|
return success();
|
||||||
|
}),
|
||||||
|
|
||||||
// Verify a user, device, and pubkey tuple
|
// Verify a user, device, and pubkey tuple
|
||||||
verify: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, args) {
|
verify: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
|
|
|
@ -13,56 +13,67 @@ 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";
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import CallHandler from "./CallHandler";
|
import CallHandler from './CallHandler';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import * as Roles from './Roles';
|
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"
|
||||||
var senderName = ev.sender ? ev.sender.name : ev.getSender();
|
const senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||||
var targetName = ev.target ? ev.target.name : ev.getStateKey();
|
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||||
var ConferenceHandler = CallHandler.getConferenceHandler();
|
const prevContent = ev.getPrevContent();
|
||||||
var reason = ev.getContent().reason ? (
|
const content = ev.getContent();
|
||||||
_t('Reason') + ': ' + ev.getContent().reason
|
|
||||||
) : "";
|
const ConferenceHandler = CallHandler.getConferenceHandler();
|
||||||
switch (ev.getContent().membership) {
|
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
||||||
case 'invite':
|
switch (content.membership) {
|
||||||
var threePidContent = ev.getContent().third_party_invite;
|
case 'invite': {
|
||||||
|
const threePidContent = content.third_party_invite;
|
||||||
if (threePidContent) {
|
if (threePidContent) {
|
||||||
if (threePidContent.display_name) {
|
if (threePidContent.display_name) {
|
||||||
return _t('%(targetName)s accepted the invitation for %(displayName)s.', {targetName: targetName, displayName: threePidContent.display_name});
|
return _t('%(targetName)s accepted the invitation for %(displayName)s.', {
|
||||||
|
targetName,
|
||||||
|
displayName: threePidContent.display_name,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return _t('%(targetName)s accepted an invitation.', {targetName: targetName});
|
return _t('%(targetName)s accepted an invitation.', {targetName});
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
||||||
return _t('%(senderName)s requested a VoIP conference.', {senderName: senderName});
|
return _t('%(senderName)s requested a VoIP conference.', {senderName});
|
||||||
}
|
} else {
|
||||||
else {
|
return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
||||||
return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case 'ban':
|
case 'ban':
|
||||||
return _t(
|
return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
||||||
'%(senderName)s banned %(targetName)s.',
|
|
||||||
{senderName: senderName, targetName: targetName}
|
|
||||||
) + ' ' + reason;
|
|
||||||
case 'join':
|
case 'join':
|
||||||
if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') {
|
if (prevContent && prevContent.membership === 'join') {
|
||||||
if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) {
|
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
||||||
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname, displayName: ev.getContent().displayname});
|
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {
|
||||||
} else if (!ev.getPrevContent().displayname && ev.getContent().displayname) {
|
senderName,
|
||||||
return _t('%(senderName)s set their display name to %(displayName)s.', {senderName: ev.getSender(), displayName: ev.getContent().displayname});
|
oldDisplayName: prevContent.displayname,
|
||||||
} else if (ev.getPrevContent().displayname && !ev.getContent().displayname) {
|
displayName: content.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 (!prevContent.displayname && content.displayname) {
|
||||||
return _t('%(senderName)s removed their profile picture.', {senderName: senderName});
|
return _t('%(senderName)s set their display name to %(displayName)s.', {
|
||||||
} else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) {
|
senderName,
|
||||||
return _t('%(senderName)s changed their profile picture.', {senderName: senderName});
|
displayName: content.displayname,
|
||||||
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
|
});
|
||||||
return _t('%(senderName)s set a profile picture.', {senderName: senderName});
|
} else if (prevContent.displayname && !content.displayname) {
|
||||||
|
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
|
||||||
|
senderName,
|
||||||
|
oldDisplayName: prevContent.displayname,
|
||||||
|
});
|
||||||
|
} else if (prevContent.avatar_url && !content.avatar_url) {
|
||||||
|
return _t('%(senderName)s removed their profile picture.', {senderName});
|
||||||
|
} else if (prevContent.avatar_url && content.avatar_url &&
|
||||||
|
prevContent.avatar_url !== content.avatar_url) {
|
||||||
|
return _t('%(senderName)s changed their profile picture.', {senderName});
|
||||||
|
} else if (!prevContent.avatar_url && content.avatar_url) {
|
||||||
|
return _t('%(senderName)s set a profile picture.', {senderName});
|
||||||
} else {
|
} else {
|
||||||
// suppress null rejoins
|
// suppress null rejoins
|
||||||
return '';
|
return '';
|
||||||
|
@ -71,73 +82,69 @@ function textForMemberEvent(ev) {
|
||||||
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 _t('VoIP conference started.');
|
return _t('VoIP conference started.');
|
||||||
}
|
} else {
|
||||||
else {
|
return _t('%(targetName)s joined the room.', {targetName});
|
||||||
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 _t('VoIP conference finished.');
|
return _t('VoIP conference finished.');
|
||||||
|
} else if (prevContent.membership === "invite") {
|
||||||
|
return _t('%(targetName)s rejected the invitation.', {targetName});
|
||||||
|
} else {
|
||||||
|
return _t('%(targetName)s left the room.', {targetName});
|
||||||
}
|
}
|
||||||
else if (ev.getPrevContent().membership === "invite") {
|
} else if (prevContent.membership === "ban") {
|
||||||
return _t('%(targetName)s rejected the invitation.', {targetName: targetName});
|
return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
|
||||||
}
|
} else if (prevContent.membership === "join") {
|
||||||
else {
|
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
||||||
return _t('%(targetName)s left the room.', {targetName: targetName});
|
} else if (prevContent.membership === "invite") {
|
||||||
}
|
return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
|
||||||
}
|
senderName,
|
||||||
else if (ev.getPrevContent().membership === "ban") {
|
targetName,
|
||||||
return _t('%(senderName)s unbanned %(targetName)s.', {senderName: senderName, targetName: targetName});
|
}) + ' ' + reason;
|
||||||
}
|
} else {
|
||||||
else if (ev.getPrevContent().membership === "join") {
|
return _t('%(targetName)s left the room.', {targetName});
|
||||||
return _t(
|
|
||||||
'%(senderName)s kicked %(targetName)s.',
|
|
||||||
{senderName: senderName, targetName: targetName}
|
|
||||||
) + ' ' + reason;
|
|
||||||
}
|
|
||||||
else if (ev.getPrevContent().membership === "invite") {
|
|
||||||
return _t(
|
|
||||||
'%(senderName)s withdrew %(targetName)s\'s invitation.',
|
|
||||||
{senderName: senderName, targetName: targetName}
|
|
||||||
) + ' ' + reason;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
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();
|
const 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 _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
|
||||||
|
senderDisplayName,
|
||||||
|
topic: ev.getContent().topic,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForRoomNameEvent(ev) {
|
function textForRoomNameEvent(ev) {
|
||||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
|
|
||||||
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
|
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
|
||||||
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName: senderDisplayName});
|
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
|
||||||
}
|
}
|
||||||
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {senderDisplayName: senderDisplayName, roomName: ev.getContent().name});
|
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
|
||||||
|
senderDisplayName,
|
||||||
|
roomName: ev.getContent().name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForMessageEvent(ev) {
|
function textForMessageEvent(ev) {
|
||||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
var message = senderDisplayName + ': ' + ev.getContent().body;
|
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||||
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 = _t('%(senderDisplayName)s sent an image.', {senderDisplayName: senderDisplayName});
|
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallAnswerEvent(event) {
|
function textForCallAnswerEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : _t('Someone');
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
||||||
return _t('%(senderName)s answered the call.', {senderName: senderName}) + ' ' + supported;
|
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallHangupEvent(event) {
|
function textForCallHangupEvent(event) {
|
||||||
|
@ -159,48 +166,52 @@ function textForCallHangupEvent(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallInviteEvent(event) {
|
function textForCallInviteEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : _t('Someone');
|
const 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";
|
let callType = "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";
|
callType = "video";
|
||||||
}
|
}
|
||||||
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
const supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
||||||
return _t('%(senderName)s placed a %(callType)s call.', {senderName: senderName, callType: type}) + ' ' + supported;
|
return _t('%(senderName)s placed a %(callType)s call.', {senderName, callType}) + ' ' + supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForThreePidInviteEvent(event) {
|
function textForThreePidInviteEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {senderName: senderName, targetDisplayName: event.getContent().display_name});
|
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
|
||||||
|
senderName,
|
||||||
|
targetDisplayName: event.getContent().display_name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForHistoryVisibilityEvent(event) {
|
function textForHistoryVisibilityEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
var vis = event.getContent().history_visibility;
|
switch (event.getContent().history_visibility) {
|
||||||
// XXX: This i18n just isn't going to work for languages with different sentence structure.
|
case 'invited':
|
||||||
var text = _t('%(senderName)s made future room history visible to', {senderName: senderName}) + ' ';
|
return _t('%(senderName)s made future room history visible to all room members, '
|
||||||
if (vis === "invited") {
|
+ 'from the point they are invited.', {senderName});
|
||||||
text += _t('all room members, from the point they are invited') + '.';
|
case 'joined':
|
||||||
|
return _t('%(senderName)s made future room history visible to all room members, '
|
||||||
|
+ 'from the point they joined.', {senderName});
|
||||||
|
case 'shared':
|
||||||
|
return _t('%(senderName)s made future room history visible to all room members.', {senderName});
|
||||||
|
case 'world_readable':
|
||||||
|
return _t('%(senderName)s made future room history visible to anyone.', {senderName});
|
||||||
|
default:
|
||||||
|
return _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
|
||||||
|
senderName,
|
||||||
|
visibility: event.getContent().history_visibility,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else if (vis === "joined") {
|
|
||||||
text += _t('all room members, from the point they joined') + '.';
|
|
||||||
}
|
|
||||||
else if (vis === "shared") {
|
|
||||||
text += _t('all room members') + '.';
|
|
||||||
}
|
|
||||||
else if (vis === "world_readable") {
|
|
||||||
text += _t('anyone') + '.';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
text += ' ' + _t('unknown') + ' (' + vis + ').';
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForEncryptionEvent(event) {
|
function textForEncryptionEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {senderName: senderName, algorithm: event.getContent().algorithm});
|
return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {
|
||||||
|
senderName,
|
||||||
|
algorithm: event.getContent().algorithm,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently will only display a change if a user's power level is changed
|
// Currently will only display a change if a user's power level is changed
|
||||||
|
@ -211,18 +222,18 @@ function textForPowerEvent(event) {
|
||||||
}
|
}
|
||||||
const userDefault = event.getContent().users_default || 0;
|
const userDefault = event.getContent().users_default || 0;
|
||||||
// Construct set of userIds
|
// Construct set of userIds
|
||||||
let users = [];
|
const users = [];
|
||||||
Object.keys(event.getContent().users).forEach(
|
Object.keys(event.getContent().users).forEach(
|
||||||
(userId) => {
|
(userId) => {
|
||||||
if (users.indexOf(userId) === -1) users.push(userId);
|
if (users.indexOf(userId) === -1) users.push(userId);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
Object.keys(event.getPrevContent().users).forEach(
|
Object.keys(event.getPrevContent().users).forEach(
|
||||||
(userId) => {
|
(userId) => {
|
||||||
if (users.indexOf(userId) === -1) users.push(userId);
|
if (users.indexOf(userId) === -1) users.push(userId);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
let diff = [];
|
const diff = [];
|
||||||
// XXX: This is also surely broken for i18n
|
// XXX: This is also surely broken for i18n
|
||||||
users.forEach((userId) => {
|
users.forEach((userId) => {
|
||||||
// Previous power level
|
// Previous power level
|
||||||
|
@ -231,11 +242,11 @@ function textForPowerEvent(event) {
|
||||||
const to = event.getContent().users[userId];
|
const to = event.getContent().users[userId];
|
||||||
if (to !== from) {
|
if (to !== from) {
|
||||||
diff.push(
|
diff.push(
|
||||||
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
||||||
userId: userId,
|
userId,
|
||||||
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
|
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
|
||||||
toPowerLevel: Roles.textualPowerLevel(to, userDefault)
|
toPowerLevel: Roles.textualPowerLevel(to, userDefault),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -243,16 +254,22 @@ function textForPowerEvent(event) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
|
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
|
||||||
senderName: senderName,
|
senderName,
|
||||||
powerLevelDiffText: diff.join(", ")
|
powerLevelDiffText: diff.join(", "),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function textForPinnedEvent(event) {
|
||||||
|
const senderName = event.getSender();
|
||||||
|
return _t("%(senderName)s changed the pinned messages for the room.", {senderName});
|
||||||
|
}
|
||||||
|
|
||||||
function textForWidgetEvent(event) {
|
function textForWidgetEvent(event) {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.getSender();
|
||||||
const previousContent = event.getPrevContent() || {};
|
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
|
||||||
const {name, type, url} = event.getContent() || {};
|
const {name, type, url} = event.getContent() || {};
|
||||||
let widgetName = name || previousContent.name || type || previousContent.type || '';
|
|
||||||
|
let widgetName = name || prevName || type || prevType || '';
|
||||||
// Apply sentence case to widget name
|
// Apply sentence case to widget name
|
||||||
if (widgetName && widgetName.length > 0) {
|
if (widgetName && widgetName.length > 0) {
|
||||||
widgetName = widgetName[0].toUpperCase() + widgetName.slice(1) + ' ';
|
widgetName = widgetName[0].toUpperCase() + widgetName.slice(1) + ' ';
|
||||||
|
@ -261,9 +278,15 @@ function textForWidgetEvent(event) {
|
||||||
// If the widget was removed, its content should be {}, but this is sufficiently
|
// If the widget was removed, its content should be {}, but this is sufficiently
|
||||||
// equivalent to that condition.
|
// equivalent to that condition.
|
||||||
if (url) {
|
if (url) {
|
||||||
return _t('%(widgetName)s widget added by %(senderName)s', {
|
if (prevUrl) {
|
||||||
widgetName, senderName,
|
return _t('%(widgetName)s widget modified by %(senderName)s', {
|
||||||
});
|
widgetName, senderName,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return _t('%(widgetName)s widget added by %(senderName)s', {
|
||||||
|
widgetName, senderName,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return _t('%(widgetName)s widget removed by %(senderName)s', {
|
return _t('%(widgetName)s widget removed by %(senderName)s', {
|
||||||
widgetName, senderName,
|
widgetName, senderName,
|
||||||
|
@ -271,26 +294,30 @@ function textForWidgetEvent(event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var handlers = {
|
const handlers = {
|
||||||
'm.room.message': textForMessageEvent,
|
'm.room.message': textForMessageEvent,
|
||||||
'm.room.name': textForRoomNameEvent,
|
'm.call.invite': textForCallInviteEvent,
|
||||||
'm.room.topic': textForTopicEvent,
|
'm.call.answer': textForCallAnswerEvent,
|
||||||
'm.room.member': textForMemberEvent,
|
'm.call.hangup': textForCallHangupEvent,
|
||||||
'm.call.invite': textForCallInviteEvent,
|
};
|
||||||
'm.call.answer': textForCallAnswerEvent,
|
|
||||||
'm.call.hangup': textForCallHangupEvent,
|
const stateHandlers = {
|
||||||
|
'm.room.name': textForRoomNameEvent,
|
||||||
|
'm.room.topic': textForTopicEvent,
|
||||||
|
'm.room.member': textForMemberEvent,
|
||||||
'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,
|
'm.room.power_levels': textForPowerEvent,
|
||||||
|
'm.room.pinned_events': textForPinnedEvent,
|
||||||
|
|
||||||
'im.vector.modular.widgets': textForWidgetEvent,
|
'im.vector.modular.widgets': textForWidgetEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
textForEvent: function(ev) {
|
textForEvent: function(ev) {
|
||||||
var hdlr = handlers[ev.getType()];
|
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||||
if (!hdlr) return "";
|
if (handler) return handler(ev);
|
||||||
return hdlr(ev);
|
return '';
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,10 +18,10 @@ limitations under the License.
|
||||||
// module.exports otherwise this will break when included by both
|
// module.exports otherwise this will break when included by both
|
||||||
// react-sdk and apps layered on top.
|
// react-sdk and apps layered on top.
|
||||||
|
|
||||||
var DEBUG = 0;
|
const DEBUG = 0;
|
||||||
|
|
||||||
// The colour keys to be replaced as referred to in CSS
|
// The colour keys to be replaced as referred to in CSS
|
||||||
var keyRgb = [
|
const keyRgb = [
|
||||||
"rgb(118, 207, 166)", // Vector Green
|
"rgb(118, 207, 166)", // Vector Green
|
||||||
"rgb(234, 245, 240)", // Vector Light Green
|
"rgb(234, 245, 240)", // Vector Light Green
|
||||||
"rgb(211, 239, 225)", // BottomLeftMenu overlay (20% Vector Green)
|
"rgb(211, 239, 225)", // BottomLeftMenu overlay (20% Vector Green)
|
||||||
|
@ -35,7 +35,7 @@ var keyRgb = [
|
||||||
// x = (255 - 234) / (255 - 118) = 0.16
|
// x = (255 - 234) / (255 - 118) = 0.16
|
||||||
|
|
||||||
// The colour keys to be replaced as referred to in SVGs
|
// The colour keys to be replaced as referred to in SVGs
|
||||||
var keyHex = [
|
const keyHex = [
|
||||||
"#76CFA6", // Vector Green
|
"#76CFA6", // Vector Green
|
||||||
"#EAF5F0", // Vector Light Green
|
"#EAF5F0", // Vector Light Green
|
||||||
"#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green)
|
"#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green)
|
||||||
|
@ -44,14 +44,14 @@ var keyHex = [
|
||||||
|
|
||||||
// cache of our replacement colours
|
// cache of our replacement colours
|
||||||
// defaults to our keys.
|
// defaults to our keys.
|
||||||
var colors = [
|
const colors = [
|
||||||
keyHex[0],
|
keyHex[0],
|
||||||
keyHex[1],
|
keyHex[1],
|
||||||
keyHex[2],
|
keyHex[2],
|
||||||
keyHex[3],
|
keyHex[3],
|
||||||
];
|
];
|
||||||
|
|
||||||
var cssFixups = [
|
const cssFixups = [
|
||||||
// {
|
// {
|
||||||
// style: a style object that should be fixed up taken from a stylesheet
|
// style: a style object that should be fixed up taken from a stylesheet
|
||||||
// attr: name of the attribute to be clobbered, e.g. 'color'
|
// attr: name of the attribute to be clobbered, e.g. 'color'
|
||||||
|
@ -60,7 +60,7 @@ var cssFixups = [
|
||||||
];
|
];
|
||||||
|
|
||||||
// CSS attributes to be fixed up
|
// CSS attributes to be fixed up
|
||||||
var cssAttrs = [
|
const cssAttrs = [
|
||||||
"color",
|
"color",
|
||||||
"backgroundColor",
|
"backgroundColor",
|
||||||
"borderColor",
|
"borderColor",
|
||||||
|
@ -69,17 +69,17 @@ var cssAttrs = [
|
||||||
"borderLeftColor",
|
"borderLeftColor",
|
||||||
];
|
];
|
||||||
|
|
||||||
var svgAttrs = [
|
const svgAttrs = [
|
||||||
"fill",
|
"fill",
|
||||||
"stroke",
|
"stroke",
|
||||||
];
|
];
|
||||||
|
|
||||||
var cached = false;
|
let cached = false;
|
||||||
|
|
||||||
function calcCssFixups() {
|
function calcCssFixups() {
|
||||||
if (DEBUG) console.log("calcSvgFixups start");
|
if (DEBUG) console.log("calcSvgFixups start");
|
||||||
for (var i = 0; i < document.styleSheets.length; i++) {
|
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||||
var ss = document.styleSheets[i];
|
const ss = document.styleSheets[i];
|
||||||
if (!ss) continue; // well done safari >:(
|
if (!ss) continue; // well done safari >:(
|
||||||
// Chromium apparently sometimes returns null here; unsure why.
|
// Chromium apparently sometimes returns null here; unsure why.
|
||||||
// see $14534907369972FRXBx:matrix.org in HQ
|
// see $14534907369972FRXBx:matrix.org in HQ
|
||||||
|
@ -104,12 +104,12 @@ function calcCssFixups() {
|
||||||
if (ss.href && !ss.href.match(/\/bundle.*\.css$/)) continue;
|
if (ss.href && !ss.href.match(/\/bundle.*\.css$/)) continue;
|
||||||
|
|
||||||
if (!ss.cssRules) continue;
|
if (!ss.cssRules) continue;
|
||||||
for (var j = 0; j < ss.cssRules.length; j++) {
|
for (let j = 0; j < ss.cssRules.length; j++) {
|
||||||
var rule = ss.cssRules[j];
|
const rule = ss.cssRules[j];
|
||||||
if (!rule.style) continue;
|
if (!rule.style) continue;
|
||||||
for (var k = 0; k < cssAttrs.length; k++) {
|
for (let k = 0; k < cssAttrs.length; k++) {
|
||||||
var attr = cssAttrs[k];
|
const attr = cssAttrs[k];
|
||||||
for (var l = 0; l < keyRgb.length; l++) {
|
for (let l = 0; l < keyRgb.length; l++) {
|
||||||
if (rule.style[attr] === keyRgb[l]) {
|
if (rule.style[attr] === keyRgb[l]) {
|
||||||
cssFixups.push({
|
cssFixups.push({
|
||||||
style: rule.style,
|
style: rule.style,
|
||||||
|
@ -126,8 +126,8 @@ function calcCssFixups() {
|
||||||
|
|
||||||
function applyCssFixups() {
|
function applyCssFixups() {
|
||||||
if (DEBUG) console.log("applyCssFixups start");
|
if (DEBUG) console.log("applyCssFixups start");
|
||||||
for (var i = 0; i < cssFixups.length; i++) {
|
for (let i = 0; i < cssFixups.length; i++) {
|
||||||
var cssFixup = cssFixups[i];
|
const cssFixup = cssFixups[i];
|
||||||
cssFixup.style[cssFixup.attr] = colors[cssFixup.index];
|
cssFixup.style[cssFixup.attr] = colors[cssFixup.index];
|
||||||
}
|
}
|
||||||
if (DEBUG) console.log("applyCssFixups end");
|
if (DEBUG) console.log("applyCssFixups end");
|
||||||
|
@ -140,15 +140,15 @@ function hexToRgb(color) {
|
||||||
color[1] + color[1] +
|
color[1] + color[1] +
|
||||||
color[2] + color[2];
|
color[2] + color[2];
|
||||||
}
|
}
|
||||||
var val = parseInt(color, 16);
|
const val = parseInt(color, 16);
|
||||||
var r = (val >> 16) & 255;
|
const r = (val >> 16) & 255;
|
||||||
var g = (val >> 8) & 255;
|
const g = (val >> 8) & 255;
|
||||||
var b = val & 255;
|
const b = val & 255;
|
||||||
return [r, g, b];
|
return [r, g, b];
|
||||||
}
|
}
|
||||||
|
|
||||||
function rgbToHex(rgb) {
|
function rgbToHex(rgb) {
|
||||||
var val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
|
const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
|
||||||
return '#' + (0x1000000 + val).toString(16).slice(1);
|
return '#' + (0x1000000 + val).toString(16).slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,12 +167,11 @@ module.exports = {
|
||||||
*
|
*
|
||||||
* @param {Function} tintable Function to call when the tint changes.
|
* @param {Function} tintable Function to call when the tint changes.
|
||||||
*/
|
*/
|
||||||
registerTintable : function(tintable) {
|
registerTintable: function(tintable) {
|
||||||
tintables.push(tintable);
|
tintables.push(tintable);
|
||||||
},
|
},
|
||||||
|
|
||||||
tint: function(primaryColor, secondaryColor, tertiaryColor) {
|
tint: function(primaryColor, secondaryColor, tertiaryColor) {
|
||||||
|
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
calcCssFixups();
|
calcCssFixups();
|
||||||
cached = true;
|
cached = true;
|
||||||
|
@ -185,7 +184,7 @@ module.exports = {
|
||||||
|
|
||||||
if (!secondaryColor) {
|
if (!secondaryColor) {
|
||||||
const x = 0.16; // average weighting factor calculated from vector green & light green
|
const x = 0.16; // average weighting factor calculated from vector green & light green
|
||||||
var rgb = hexToRgb(primaryColor);
|
const rgb = hexToRgb(primaryColor);
|
||||||
rgb[0] = x * rgb[0] + (1 - x) * 255;
|
rgb[0] = x * rgb[0] + (1 - x) * 255;
|
||||||
rgb[1] = x * rgb[1] + (1 - x) * 255;
|
rgb[1] = x * rgb[1] + (1 - x) * 255;
|
||||||
rgb[2] = x * rgb[2] + (1 - x) * 255;
|
rgb[2] = x * rgb[2] + (1 - x) * 255;
|
||||||
|
@ -194,8 +193,8 @@ module.exports = {
|
||||||
|
|
||||||
if (!tertiaryColor) {
|
if (!tertiaryColor) {
|
||||||
const x = 0.19;
|
const x = 0.19;
|
||||||
var rgb1 = hexToRgb(primaryColor);
|
const rgb1 = hexToRgb(primaryColor);
|
||||||
var rgb2 = hexToRgb(secondaryColor);
|
const rgb2 = hexToRgb(secondaryColor);
|
||||||
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
|
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
|
||||||
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
|
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
|
||||||
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
|
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
|
||||||
|
@ -204,8 +203,7 @@ module.exports = {
|
||||||
|
|
||||||
if (colors[0] === primaryColor &&
|
if (colors[0] === primaryColor &&
|
||||||
colors[1] === secondaryColor &&
|
colors[1] === secondaryColor &&
|
||||||
colors[2] === tertiaryColor)
|
colors[2] === tertiaryColor) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,14 +246,13 @@ module.exports = {
|
||||||
// key colour; cache the element and apply.
|
// key colour; cache the element and apply.
|
||||||
|
|
||||||
if (DEBUG) console.log("calcSvgFixups start for " + svgs);
|
if (DEBUG) console.log("calcSvgFixups start for " + svgs);
|
||||||
var fixups = [];
|
const fixups = [];
|
||||||
for (var i = 0; i < svgs.length; i++) {
|
for (let i = 0; i < svgs.length; i++) {
|
||||||
var svgDoc;
|
var svgDoc;
|
||||||
try {
|
try {
|
||||||
svgDoc = svgs[i].contentDocument;
|
svgDoc = svgs[i].contentDocument;
|
||||||
}
|
} catch(e) {
|
||||||
catch(e) {
|
let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
|
||||||
var msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
|
|
||||||
if (e.message) {
|
if (e.message) {
|
||||||
msg += e.message;
|
msg += e.message;
|
||||||
}
|
}
|
||||||
|
@ -265,12 +262,12 @@ module.exports = {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
if (!svgDoc) continue;
|
if (!svgDoc) continue;
|
||||||
var tags = svgDoc.getElementsByTagName("*");
|
const tags = svgDoc.getElementsByTagName("*");
|
||||||
for (var j = 0; j < tags.length; j++) {
|
for (let j = 0; j < tags.length; j++) {
|
||||||
var tag = tags[j];
|
const tag = tags[j];
|
||||||
for (var k = 0; k < svgAttrs.length; k++) {
|
for (let k = 0; k < svgAttrs.length; k++) {
|
||||||
var attr = svgAttrs[k];
|
const attr = svgAttrs[k];
|
||||||
for (var l = 0; l < keyHex.length; l++) {
|
for (let l = 0; l < keyHex.length; l++) {
|
||||||
if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) {
|
if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) {
|
||||||
fixups.push({
|
fixups.push({
|
||||||
node: tag,
|
node: tag,
|
||||||
|
@ -289,10 +286,10 @@ module.exports = {
|
||||||
|
|
||||||
applySvgFixups: function(fixups) {
|
applySvgFixups: function(fixups) {
|
||||||
if (DEBUG) console.log("applySvgFixups start for " + fixups);
|
if (DEBUG) console.log("applySvgFixups start for " + fixups);
|
||||||
for (var i = 0; i < fixups.length; i++) {
|
for (let i = 0; i < fixups.length; i++) {
|
||||||
var svgFixup = fixups[i];
|
const svgFixup = fixups[i];
|
||||||
svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]);
|
svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]);
|
||||||
}
|
}
|
||||||
if (DEBUG) console.log("applySvgFixups end");
|
if (DEBUG) console.log("applySvgFixups end");
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
const MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
import UserSettingsStore from './UserSettingsStore';
|
import UserSettingsStore from './UserSettingsStore';
|
||||||
import shouldHideEvent from './shouldHideEvent';
|
import shouldHideEvent from './shouldHideEvent';
|
||||||
var sdk = require('./index');
|
const sdk = require('./index');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
|
@ -34,17 +34,17 @@ module.exports = {
|
||||||
} 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;
|
||||||
}
|
}
|
||||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
return EventTile.haveTileForEvent(ev);
|
return EventTile.haveTileForEvent(ev);
|
||||||
},
|
},
|
||||||
|
|
||||||
doesRoomHaveUnreadMessages: function(room) {
|
doesRoomHaveUnreadMessages: function(room) {
|
||||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
|
|
||||||
// get the most recent read receipt sent by our account.
|
// get the most recent read receipt sent by our account.
|
||||||
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
|
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
|
||||||
// despite the name of the method :((
|
// despite the name of the method :((
|
||||||
var readUpToId = room.getEventReadUpTo(myUserId);
|
const readUpToId = room.getEventReadUpTo(myUserId);
|
||||||
|
|
||||||
// as we don't send RRs for our own messages, make sure we special case that
|
// 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!
|
// if *we* sent the last message into the room, we consider it not unread!
|
||||||
|
@ -54,8 +54,7 @@ module.exports = {
|
||||||
// https://github.com/vector-im/riot-web/issues/3363
|
// https://github.com/vector-im/riot-web/issues/3363
|
||||||
if (room.timeline.length &&
|
if (room.timeline.length &&
|
||||||
room.timeline[room.timeline.length - 1].sender &&
|
room.timeline[room.timeline.length - 1].sender &&
|
||||||
room.timeline[room.timeline.length - 1].sender.userId === myUserId)
|
room.timeline[room.timeline.length - 1].sender.userId === myUserId) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,8 +66,8 @@ module.exports = {
|
||||||
|
|
||||||
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
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 (let i = room.timeline.length - 1; i >= 0; --i) {
|
||||||
var ev = room.timeline[i];
|
const ev = room.timeline[i];
|
||||||
|
|
||||||
if (ev.getId() == readUpToId) {
|
if (ev.getId() == readUpToId) {
|
||||||
// If we've read up to this event, there's nothing more recents
|
// If we've read up to this event, there's nothing more recents
|
||||||
|
@ -86,5 +85,5 @@ module.exports = {
|
||||||
// is unread on the theory that false positives are better than
|
// is unread on the theory that false positives are better than
|
||||||
// false negatives here.
|
// false negatives here.
|
||||||
return true;
|
return true;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,11 +16,12 @@ limitations under the License.
|
||||||
|
|
||||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||||
|
|
||||||
const mxidRegex = /^@\S+:\S+$/;
|
const mxUserIdRegex = /^@\S+:\S+$/;
|
||||||
|
const mxRoomIdRegex = /^!\S+:\S+$/;
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
export const addressTypes = [
|
export const addressTypes = [
|
||||||
'mx', 'email',
|
'mx-user-id', 'mx-room-id', 'email',
|
||||||
];
|
];
|
||||||
|
|
||||||
// PropType definition for an object describing
|
// PropType definition for an object describing
|
||||||
|
@ -41,13 +42,16 @@ export const UserAddressType = PropTypes.shape({
|
||||||
|
|
||||||
export function getAddressType(inputText) {
|
export function getAddressType(inputText) {
|
||||||
const isEmailAddress = emailRegex.test(inputText);
|
const isEmailAddress = emailRegex.test(inputText);
|
||||||
const isMatrixId = mxidRegex.test(inputText);
|
const isUserId = mxUserIdRegex.test(inputText);
|
||||||
|
const isRoomId = mxRoomIdRegex.test(inputText);
|
||||||
|
|
||||||
// sanity check the input for user IDs
|
// sanity check the input for user IDs
|
||||||
if (isEmailAddress) {
|
if (isEmailAddress) {
|
||||||
return 'email';
|
return 'email';
|
||||||
} else if (isMatrixId) {
|
} else if (isUserId) {
|
||||||
return 'mx';
|
return 'mx-user-id';
|
||||||
|
} else if (isRoomId) {
|
||||||
|
return 'mx-room-id';
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 New Vector 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,27 +18,55 @@ limitations under the License.
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import Notifier from './Notifier';
|
import Notifier from './Notifier';
|
||||||
import { _t } from './languageHandler';
|
import { _t, _td } from './languageHandler';
|
||||||
|
import SdkConfig from './SdkConfig';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const FEATURES = [
|
||||||
|
{
|
||||||
|
id: 'feature_groups',
|
||||||
|
name: _td("Communities"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'feature_pinning',
|
||||||
|
name: _td("Message Pinning"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
LABS_FEATURES: [
|
getLabsFeatures() {
|
||||||
{
|
const featuresConfig = SdkConfig.get()['features'] || {};
|
||||||
name: "-",
|
|
||||||
id: 'matrix_apps',
|
|
||||||
default: true,
|
|
||||||
|
|
||||||
// XXX: Always use default, ignore localStorage and remove from labs
|
// The old flag: honourned for backwards compat
|
||||||
override: true,
|
const enableLabs = SdkConfig.get()['enableLabs'];
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
// horrible but it works. The locality makes this somewhat more palatable.
|
let labsFeatures;
|
||||||
doTranslations: function() {
|
if (enableLabs) {
|
||||||
this.LABS_FEATURES[0].name = _t("Matrix Apps");
|
labsFeatures = FEATURES;
|
||||||
|
} else {
|
||||||
|
labsFeatures = FEATURES.filter((f) => {
|
||||||
|
const sdkConfigValue = featuresConfig[f.id];
|
||||||
|
if (sdkConfigValue === 'labs') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return labsFeatures.map((f) => {
|
||||||
|
return f.id;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
translatedNameForFeature(featureId) {
|
||||||
|
const feature = FEATURES.filter((f) => {
|
||||||
|
return f.id === featureId;
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
if (feature === undefined) return null;
|
||||||
|
|
||||||
|
return _t(feature.name);
|
||||||
},
|
},
|
||||||
|
|
||||||
loadProfileInfo: function() {
|
loadProfileInfo: function() {
|
||||||
|
@ -73,6 +102,17 @@ export default {
|
||||||
Notifier.setEnabled(enable);
|
Notifier.setEnabled(enable);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getEnableNotificationBody: function() {
|
||||||
|
return Notifier.isBodyEnabled();
|
||||||
|
},
|
||||||
|
|
||||||
|
setEnableNotificationBody: function(enable) {
|
||||||
|
if (!Notifier.supportsDesktopNotifications()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Notifier.setBodyEnabled(enable);
|
||||||
|
},
|
||||||
|
|
||||||
getEnableAudioNotifications: function() {
|
getEnableAudioNotifications: function() {
|
||||||
return Notifier.isAudioEnabled();
|
return Notifier.isAudioEnabled();
|
||||||
},
|
},
|
||||||
|
@ -174,33 +214,33 @@ export default {
|
||||||
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
|
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
|
||||||
},
|
},
|
||||||
|
|
||||||
getFeatureById(feature: string) {
|
|
||||||
for (let i = 0; i < this.LABS_FEATURES.length; i++) {
|
|
||||||
const f = this.LABS_FEATURES[i];
|
|
||||||
if (f.id === feature) {
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
isFeatureEnabled: function(featureId: string): boolean {
|
isFeatureEnabled: function(featureId: string): boolean {
|
||||||
// Disable labs for guests.
|
const featuresConfig = SdkConfig.get()['features'];
|
||||||
if (MatrixClientPeg.get().isGuest()) return false;
|
|
||||||
|
|
||||||
const feature = this.getFeatureById(featureId);
|
// The old flag: honourned for backwards compat
|
||||||
if (!feature) {
|
const enableLabs = SdkConfig.get()['enableLabs'];
|
||||||
console.warn(`Unknown feature "${featureId}"`);
|
|
||||||
|
let sdkConfigValue = enableLabs ? 'labs' : 'disable';
|
||||||
|
if (featuresConfig && featuresConfig[featureId] !== undefined) {
|
||||||
|
sdkConfigValue = featuresConfig[featureId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sdkConfigValue === 'enable') {
|
||||||
|
return true;
|
||||||
|
} else if (sdkConfigValue === 'disable') {
|
||||||
|
return false;
|
||||||
|
} else if (sdkConfigValue === 'labs') {
|
||||||
|
if (!MatrixClientPeg.get().isGuest()) {
|
||||||
|
// Make it explicit that guests get the defaults (although they shouldn't
|
||||||
|
// have been able to ever toggle the flags anyway)
|
||||||
|
const userValue = localStorage.getItem(`mx_labs_feature_${featureId}`);
|
||||||
|
return userValue === 'true';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
console.warn(`Unknown features config for ${featureId}: ${sdkConfigValue}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Return the default if this feature has an override to be the default value or
|
|
||||||
// if the feature has never been toggled and is therefore not in localStorage
|
|
||||||
if (Object.keys(feature).includes('override') ||
|
|
||||||
localStorage.getItem(`mx_labs_feature_${featureId}`) === null
|
|
||||||
) {
|
|
||||||
return feature.default;
|
|
||||||
}
|
|
||||||
return localStorage.getItem(`mx_labs_feature_${featureId}`) === 'true';
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setFeatureEnabled: function(featureId: string, enabled: boolean) {
|
setFeatureEnabled: function(featureId: string, enabled: boolean) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
var React = require('react');
|
const React = require('react');
|
||||||
var ReactDom = require('react-dom');
|
const ReactDom = require('react-dom');
|
||||||
var Velocity = require('velocity-vector');
|
const Velocity = require('velocity-vector');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Velociraptor contains components and animates transitions with velocity.
|
* The Velociraptor contains components and animates transitions with velocity.
|
||||||
|
@ -46,13 +46,13 @@ module.exports = React.createClass({
|
||||||
* update `this.children` according to the new list of children given
|
* update `this.children` according to the new list of children given
|
||||||
*/
|
*/
|
||||||
_updateChildren: function(newChildren) {
|
_updateChildren: function(newChildren) {
|
||||||
var self = this;
|
const self = this;
|
||||||
var oldChildren = this.children || {};
|
const oldChildren = this.children || {};
|
||||||
this.children = {};
|
this.children = {};
|
||||||
React.Children.toArray(newChildren).forEach(function(c) {
|
React.Children.toArray(newChildren).forEach(function(c) {
|
||||||
if (oldChildren[c.key]) {
|
if (oldChildren[c.key]) {
|
||||||
var old = oldChildren[c.key];
|
const old = oldChildren[c.key];
|
||||||
var oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
|
const oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
|
||||||
|
|
||||||
if (oldNode && oldNode.style.left != c.props.style.left) {
|
if (oldNode && oldNode.style.left != c.props.style.left) {
|
||||||
Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() {
|
Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() {
|
||||||
|
@ -71,18 +71,18 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
// new element. If we have a startStyle, use that as the style and go through
|
// new element. If we have a startStyle, use that as the style and go through
|
||||||
// the enter animations
|
// the enter animations
|
||||||
var newProps = {};
|
const newProps = {};
|
||||||
var restingStyle = c.props.style;
|
const restingStyle = c.props.style;
|
||||||
|
|
||||||
var startStyles = self.props.startStyles;
|
const startStyles = self.props.startStyles;
|
||||||
if (startStyles.length > 0) {
|
if (startStyles.length > 0) {
|
||||||
var startStyle = startStyles[0];
|
const startStyle = startStyles[0];
|
||||||
newProps.style = startStyle;
|
newProps.style = startStyle;
|
||||||
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
|
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
|
||||||
}
|
}
|
||||||
|
|
||||||
newProps.ref = (n => self._collectNode(
|
newProps.ref = ((n) => self._collectNode(
|
||||||
c.key, n, restingStyle
|
c.key, n, restingStyle,
|
||||||
));
|
));
|
||||||
|
|
||||||
self.children[c.key] = React.cloneElement(c, newProps);
|
self.children[c.key] = React.cloneElement(c, newProps);
|
||||||
|
@ -103,8 +103,8 @@ module.exports = React.createClass({
|
||||||
this.nodes[k] === undefined &&
|
this.nodes[k] === undefined &&
|
||||||
this.props.startStyles.length > 0
|
this.props.startStyles.length > 0
|
||||||
) {
|
) {
|
||||||
var startStyles = this.props.startStyles;
|
const startStyles = this.props.startStyles;
|
||||||
var transitionOpts = this.props.enterTransitionOpts;
|
const transitionOpts = this.props.enterTransitionOpts;
|
||||||
const domNode = ReactDom.findDOMNode(node);
|
const domNode = ReactDom.findDOMNode(node);
|
||||||
// start from startStyle 1: 0 is the one we gave it
|
// start from startStyle 1: 0 is the one we gave it
|
||||||
// to start with, so now we animate 1 etc.
|
// to start with, so now we animate 1 etc.
|
||||||
|
@ -154,7 +154,7 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{Object.values(this.children)}
|
{ Object.values(this.children) }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
var Velocity = require('velocity-vector');
|
const Velocity = require('velocity-vector');
|
||||||
|
|
||||||
// courtesy of https://github.com/julianshapiro/velocity/issues/283
|
// courtesy of https://github.com/julianshapiro/velocity/issues/283
|
||||||
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
|
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
|
||||||
function bounce( p ) {
|
function bounce( p ) {
|
||||||
var pow2,
|
let pow2,
|
||||||
bounce = 4;
|
bounce = 4;
|
||||||
|
|
||||||
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {
|
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {
|
||||||
|
|
|
@ -14,13 +14,19 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
const MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
usersTypingApartFromMeAndIgnored: function(room) {
|
||||||
|
return this.usersTyping(
|
||||||
|
room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
usersTypingApartFromMe: function(room) {
|
usersTypingApartFromMe: function(room) {
|
||||||
return this.usersTyping(
|
return this.usersTyping(
|
||||||
room, [MatrixClientPeg.get().credentials.userId]
|
room, [MatrixClientPeg.get().credentials.userId],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -29,15 +35,15 @@ module.exports = {
|
||||||
* to exclude, return a list of user objects who are typing.
|
* to exclude, return a list of user objects who are typing.
|
||||||
*/
|
*/
|
||||||
usersTyping: function(room, exclude) {
|
usersTyping: function(room, exclude) {
|
||||||
var whoIsTyping = [];
|
const whoIsTyping = [];
|
||||||
|
|
||||||
if (exclude === undefined) {
|
if (exclude === undefined) {
|
||||||
exclude = [];
|
exclude = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var memberKeys = Object.keys(room.currentState.members);
|
const memberKeys = Object.keys(room.currentState.members);
|
||||||
for (var i = 0; i < memberKeys.length; ++i) {
|
for (let i = 0; i < memberKeys.length; ++i) {
|
||||||
var userId = memberKeys[i];
|
const userId = memberKeys[i];
|
||||||
|
|
||||||
if (room.currentState.members[userId].typing) {
|
if (room.currentState.members[userId].typing) {
|
||||||
if (exclude.indexOf(userId) == -1) {
|
if (exclude.indexOf(userId) == -1) {
|
||||||
|
@ -70,5 +76,5 @@ module.exports = {
|
||||||
const lastPerson = names.pop();
|
const lastPerson = names.pop();
|
||||||
return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson});
|
return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
const React = require("react");
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
var sdk = require('../../../index');
|
const sdk = require('../../../index');
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'EncryptedEventDialog',
|
displayName: 'EncryptedEventDialog',
|
||||||
|
@ -33,7 +33,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
var client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
// first try to load the device from our store.
|
// first try to load the device from our store.
|
||||||
//
|
//
|
||||||
|
@ -60,7 +60,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
this._unmounted = true;
|
this._unmounted = true;
|
||||||
var client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (client) {
|
if (client) {
|
||||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||||
}
|
}
|
||||||
|
@ -89,12 +89,12 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderDeviceInfo: function() {
|
_renderDeviceInfo: function() {
|
||||||
var device = this.state.device;
|
const device = this.state.device;
|
||||||
if (!device) {
|
if (!device) {
|
||||||
return (<i>{ _t('unknown device') }</i>);
|
return (<i>{ _t('unknown device') }</i>);
|
||||||
}
|
}
|
||||||
|
|
||||||
var verificationStatus = (<b>{ _t('NOT verified') }</b>);
|
let verificationStatus = (<b>{ _t('NOT verified') }</b>);
|
||||||
if (device.isBlocked()) {
|
if (device.isBlocked()) {
|
||||||
verificationStatus = (<b>{ _t('Blacklisted') }</b>);
|
verificationStatus = (<b>{ _t('Blacklisted') }</b>);
|
||||||
} else if (device.isVerified()) {
|
} else if (device.isVerified()) {
|
||||||
|
@ -118,7 +118,7 @@ module.exports = React.createClass({
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{ _t('Ed25519 fingerprint') }</td>
|
<td>{ _t('Ed25519 fingerprint') }</td>
|
||||||
<td><code>{device.getFingerprint()}</code></td>
|
<td><code>{ device.getFingerprint() }</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -126,7 +126,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderEventInfo: function() {
|
_renderEventInfo: function() {
|
||||||
var event = this.props.event;
|
const event = this.props.event;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table>
|
<table>
|
||||||
|
@ -165,36 +165,36 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
|
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
|
||||||
|
|
||||||
var buttons = null;
|
let buttons = null;
|
||||||
if (this.state.device) {
|
if (this.state.device) {
|
||||||
buttons = (
|
buttons = (
|
||||||
<DeviceVerifyButtons device={ this.state.device }
|
<DeviceVerifyButtons device={this.state.device}
|
||||||
userId={ this.props.event.getSender() }
|
userId={this.props.event.getSender()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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">
|
||||||
{ _t('End-to-end encryption information') }
|
{ _t('End-to-end encryption information') }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<h4>{ _t('Event information') }</h4>
|
<h4>{ _t('Event information') }</h4>
|
||||||
{this._renderEventInfo()}
|
{ this._renderEventInfo() }
|
||||||
|
|
||||||
<h4>{ _t('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}>
|
||||||
{ _t('OK') }
|
{ _t('OK') }
|
||||||
</button>
|
</button>
|
||||||
{buttons}
|
{ buttons }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -136,13 +136,13 @@ export default React.createClass({
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<div className='error'>
|
<div className='error'>
|
||||||
{this.state.errStr}
|
{ this.state.errStr }
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputTable'>
|
<div className='mx_E2eKeysDialog_inputTable'>
|
||||||
<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'>
|
||||||
{_t("Enter passphrase")}
|
{ _t("Enter passphrase") }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
@ -155,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'>
|
||||||
{_t("Confirm passphrase")}
|
{ _t("Confirm passphrase") }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
@ -172,7 +172,7 @@ export default React.createClass({
|
||||||
disabled={disableForm}
|
disabled={disableForm}
|
||||||
/>
|
/>
|
||||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
{_t("Cancel")}
|
{ _t("Cancel") }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -134,13 +134,13 @@ export default React.createClass({
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<div className='error'>
|
<div className='error'>
|
||||||
{this.state.errStr}
|
{ this.state.errStr }
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputTable'>
|
<div className='mx_E2eKeysDialog_inputTable'>
|
||||||
<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'>
|
||||||
{_t("File to import")}
|
{ _t("File to import") }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
@ -153,14 +153,14 @@ 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'>
|
||||||
{_t("Enter passphrase")}
|
{ _t("Enter passphrase") }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
<input ref='passphrase' id='passphrase'
|
<input ref='passphrase' id='passphrase'
|
||||||
size='64' type='password'
|
size='64' type='password'
|
||||||
onChange={this._onFormChange}
|
onChange={this._onFormChange}
|
||||||
disabled={disableForm}/>
|
disabled={disableForm} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -170,7 +170,7 @@ export default React.createClass({
|
||||||
disabled={!this.state.enableSubmit || disableForm}
|
disabled={!this.state.enableSubmit || disableForm}
|
||||||
/>
|
/>
|
||||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
{_t("Cancel")}
|
{ _t("Cancel") }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -45,7 +45,7 @@ const PROVIDERS = [
|
||||||
EmojiProvider,
|
EmojiProvider,
|
||||||
CommandProvider,
|
CommandProvider,
|
||||||
DuckDuckGoProvider,
|
DuckDuckGoProvider,
|
||||||
].map(completer => completer.getInstance());
|
].map((completer) => completer.getInstance());
|
||||||
|
|
||||||
// Providers will get rejected if they take longer than this.
|
// Providers will get rejected if they take longer than this.
|
||||||
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../languageHandler';
|
import { _t, _td } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import FuzzyMatcher from './FuzzyMatcher';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import {TextualCompletion} from './Components';
|
import {TextualCompletion} from './Components';
|
||||||
|
@ -27,72 +27,82 @@ const COMMANDS = [
|
||||||
{
|
{
|
||||||
command: '/me',
|
command: '/me',
|
||||||
args: '<message>',
|
args: '<message>',
|
||||||
description: 'Displays action',
|
description: _td('Displays action'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/ban',
|
command: '/ban',
|
||||||
args: '<user-id> [reason]',
|
args: '<user-id> [reason]',
|
||||||
description: 'Bans user with given id',
|
description: _td('Bans user with given id'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/unban',
|
command: '/unban',
|
||||||
args: '<user-id>',
|
args: '<user-id>',
|
||||||
description: 'Unbans user with given id',
|
description: _td('Unbans user with given id'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/op',
|
command: '/op',
|
||||||
args: '<user-id> [<power-level>]',
|
args: '<user-id> [<power-level>]',
|
||||||
description: 'Define the power level of a user',
|
description: _td('Define the power level of a user'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/deop',
|
command: '/deop',
|
||||||
args: '<user-id>',
|
args: '<user-id>',
|
||||||
description: 'Deops user with given id',
|
description: _td('Deops user with given id'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/invite',
|
command: '/invite',
|
||||||
args: '<user-id>',
|
args: '<user-id>',
|
||||||
description: 'Invites user with given id to current room',
|
description: _td('Invites user with given id to current room'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/join',
|
command: '/join',
|
||||||
args: '<room-alias>',
|
args: '<room-alias>',
|
||||||
description: 'Joins room with given alias',
|
description: _td('Joins room with given alias'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/part',
|
command: '/part',
|
||||||
args: '[<room-alias>]',
|
args: '[<room-alias>]',
|
||||||
description: 'Leave room',
|
description: _td('Leave room'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/topic',
|
command: '/topic',
|
||||||
args: '<topic>',
|
args: '<topic>',
|
||||||
description: 'Sets the room topic',
|
description: _td('Sets the room topic'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/kick',
|
command: '/kick',
|
||||||
args: '<user-id> [reason]',
|
args: '<user-id> [reason]',
|
||||||
description: 'Kicks user with given id',
|
description: _td('Kicks user with given id'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/nick',
|
command: '/nick',
|
||||||
args: '<display-name>',
|
args: '<display-name>',
|
||||||
description: 'Changes your display nickname',
|
description: _td('Changes your display nickname'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/ddg',
|
command: '/ddg',
|
||||||
args: '<query>',
|
args: '<query>',
|
||||||
description: 'Searches DuckDuckGo for results',
|
description: _td('Searches DuckDuckGo for results'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/tint',
|
command: '/tint',
|
||||||
args: '<color1> [<color2>]',
|
args: '<color1> [<color2>]',
|
||||||
description: 'Changes colour scheme of current room',
|
description: _td('Changes colour scheme of current room'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/verify',
|
command: '/verify',
|
||||||
args: '<user-id> <device-id> <device-signing-key>',
|
args: '<user-id> <device-id> <device-signing-key>',
|
||||||
description: 'Verifies a user, device, and pubkey tuple',
|
description: _td('Verifies a user, device, and pubkey tuple'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/ignore',
|
||||||
|
args: '<user-id>',
|
||||||
|
description: _td('Ignores a user, hiding their messages from you'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/unignore',
|
||||||
|
args: '<user-id>',
|
||||||
|
description: _td('Stops ignoring a user, showing their messages going forward'),
|
||||||
},
|
},
|
||||||
// Omitting `/markdown` as it only seems to apply to OldComposer
|
// Omitting `/markdown` as it only seems to apply to OldComposer
|
||||||
];
|
];
|
||||||
|
@ -119,7 +129,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
component: (<TextualCompletion
|
component: (<TextualCompletion
|
||||||
title={result.command}
|
title={result.command}
|
||||||
subtitle={result.args}
|
subtitle={result.args}
|
||||||
description={ _t(result.description) }
|
description={_t(result.description)}
|
||||||
/>),
|
/>),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
|
@ -140,7 +150,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_block">
|
return <div className="mx_Autocomplete_Completion_container_block">
|
||||||
{completions}
|
{ completions }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,13 @@ export class TextualCompletion extends React.Component {
|
||||||
subtitle,
|
subtitle,
|
||||||
description,
|
description,
|
||||||
className,
|
className,
|
||||||
...restProps,
|
...restProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={classNames('mx_Autocomplete_Completion_block', className)} {...restProps}>
|
<div className={classNames('mx_Autocomplete_Completion_block', className)} {...restProps}>
|
||||||
<span className="mx_Autocomplete_Completion_title">{title}</span>
|
<span className="mx_Autocomplete_Completion_title">{ title }</span>
|
||||||
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
|
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
|
||||||
<span className="mx_Autocomplete_Completion_description">{description}</span>
|
<span className="mx_Autocomplete_Completion_description">{ description }</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -56,14 +56,14 @@ export class PillCompletion extends React.Component {
|
||||||
description,
|
description,
|
||||||
initialComponent,
|
initialComponent,
|
||||||
className,
|
className,
|
||||||
...restProps,
|
...restProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={classNames('mx_Autocomplete_Completion_pill', className)} {...restProps}>
|
<div className={classNames('mx_Autocomplete_Completion_pill', className)} {...restProps}>
|
||||||
{initialComponent}
|
{ initialComponent }
|
||||||
<span className="mx_Autocomplete_Completion_title">{title}</span>
|
<span className="mx_Autocomplete_Completion_title">{ title }</span>
|
||||||
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
|
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
|
||||||
<span className="mx_Autocomplete_Completion_description">{description}</span>
|
<span className="mx_Autocomplete_Completion_description">{ description }</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: {start: number, end: number}) {
|
async getCompletions(query: string, selection: {start: number, end: number}) {
|
||||||
let {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (!query || !command) {
|
if (!query || !command) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
let results = json.Results.map(result => {
|
const results = json.Results.map((result) => {
|
||||||
return {
|
return {
|
||||||
completion: result.Text,
|
completion: result.Text,
|
||||||
component: (
|
component: (
|
||||||
|
@ -105,7 +105,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_block">
|
return <div className="mx_Autocomplete_Completion_container_block">
|
||||||
{completions}
|
{ completions }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {PillCompletion} from './Components';
|
||||||
import type {SelectionRange, Completion} from './Autocompleter';
|
import type {SelectionRange, Completion} from './Autocompleter';
|
||||||
import _uniq from 'lodash/uniq';
|
import _uniq from 'lodash/uniq';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
|
import UserSettingsStore from '../UserSettingsStore';
|
||||||
|
|
||||||
import EmojiData from '../stripped-emoji.json';
|
import EmojiData from '../stripped-emoji.json';
|
||||||
|
|
||||||
|
@ -96,6 +97,10 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: SelectionRange) {
|
async getCompletions(query: string, selection: SelectionRange) {
|
||||||
|
if (UserSettingsStore.getSyncedSetting("MessageComposerInput.dontSuggestEmoji")) {
|
||||||
|
return []; // don't give any suggestions if the user doesn't want them
|
||||||
|
}
|
||||||
|
|
||||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
|
@ -133,7 +138,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
return {
|
return {
|
||||||
completion: unicode,
|
completion: unicode,
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{unicode}</EmojiText>} />
|
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{ unicode }</EmojiText>} />
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
|
@ -147,14 +152,13 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance() {
|
static getInstance() {
|
||||||
if (instance == null)
|
if (instance == null) {instance = new EmojiProvider();}
|
||||||
{instance = new EmojiProvider();}
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
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">
|
||||||
{completions}
|
{ completions }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,7 @@ 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 mx_Autocomplete_Completion_container_truncate">
|
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||||
{completions}
|
{ completions }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,8 @@ 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> = [];
|
users: Array<RoomMember> = null;
|
||||||
|
room: Room = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(USER_REGEX, {
|
super(USER_REGEX, {
|
||||||
|
@ -54,8 +55,11 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lazy-load user list into matcher
|
||||||
|
if (this.users === null) this._makeUsers();
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection, force);
|
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
completions = this.matcher.match(command[0]).map((user) => {
|
completions = this.matcher.match(command[0]).map((user) => {
|
||||||
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
||||||
|
@ -67,7 +71,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
href: 'https://matrix.to/#/' + user.userId,
|
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} />}
|
||||||
title={displayName}
|
title={displayName}
|
||||||
description={user.userId} />
|
description={user.userId} />
|
||||||
),
|
),
|
||||||
|
@ -83,7 +87,12 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserListFromRoom(room: Room) {
|
setUserListFromRoom(room: Room) {
|
||||||
const events = room.getLiveTimeline().getEvents();
|
this.room = room;
|
||||||
|
this.users = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_makeUsers() {
|
||||||
|
const events = this.room.getLiveTimeline().getEvents();
|
||||||
const lastSpoken = {};
|
const lastSpoken = {};
|
||||||
|
|
||||||
for(const event of events) {
|
for(const event of events) {
|
||||||
|
@ -91,7 +100,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUserId = MatrixClientPeg.get().credentials.userId;
|
const currentUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
this.users = room.getJoinedMembers().filter((member) => {
|
this.users = this.room.getJoinedMembers().filter((member) => {
|
||||||
if (member.userId !== currentUserId) return true;
|
if (member.userId !== currentUserId) return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -103,7 +112,8 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserSpoke(user: RoomMember) {
|
onUserSpoke(user: RoomMember) {
|
||||||
if(user.userId === MatrixClientPeg.get().credentials.userId) return;
|
if (this.users === null) return;
|
||||||
|
if (user.userId === MatrixClientPeg.get().credentials.userId) return;
|
||||||
|
|
||||||
// Move the user that spoke to the front of the array
|
// Move the user that spoke to the front of the array
|
||||||
this.users.splice(
|
this.users.splice(
|
||||||
|
@ -122,7 +132,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 mx_Autocomplete_Completion_container_truncate">
|
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||||
{completions}
|
{ completions }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,9 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var classNames = require('classnames');
|
const classNames = require('classnames');
|
||||||
var React = require('react');
|
const React = require('react');
|
||||||
var ReactDOM = require('react-dom');
|
const ReactDOM = require('react-dom');
|
||||||
|
|
||||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||||
|
@ -36,7 +36,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getOrCreateContainer: function() {
|
getOrCreateContainer: function() {
|
||||||
var container = document.getElementById(this.ContextualMenuContainerId);
|
let container = document.getElementById(this.ContextualMenuContainerId);
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
container = document.createElement("div");
|
container = document.createElement("div");
|
||||||
|
@ -48,9 +48,9 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
createMenu: function(Element, props) {
|
createMenu: function(Element, props) {
|
||||||
var self = this;
|
const self = this;
|
||||||
|
|
||||||
var closeMenu = function() {
|
const closeMenu = function() {
|
||||||
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
||||||
|
|
||||||
if (props && props.onFinished) {
|
if (props && props.onFinished) {
|
||||||
|
@ -58,17 +58,17 @@ module.exports = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var position = {
|
const position = {
|
||||||
top: props.top,
|
top: props.top,
|
||||||
};
|
};
|
||||||
|
|
||||||
var chevronOffset = {};
|
const chevronOffset = {};
|
||||||
if (props.chevronOffset) {
|
if (props.chevronOffset) {
|
||||||
chevronOffset.top = props.chevronOffset;
|
chevronOffset.top = props.chevronOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
// To override the default chevron colour, if it's been set
|
// To override the default chevron colour, if it's been set
|
||||||
var chevronCSS = "";
|
let chevronCSS = "";
|
||||||
if (props.menuColour) {
|
if (props.menuColour) {
|
||||||
chevronCSS = `
|
chevronCSS = `
|
||||||
.mx_ContextualMenu_chevron_left:after {
|
.mx_ContextualMenu_chevron_left:after {
|
||||||
|
@ -81,7 +81,7 @@ module.exports = {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
var chevron = null;
|
let chevron = null;
|
||||||
if (props.left) {
|
if (props.left) {
|
||||||
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>;
|
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>;
|
||||||
position.left = props.left;
|
position.left = props.left;
|
||||||
|
@ -90,15 +90,15 @@ module.exports = {
|
||||||
position.right = props.right;
|
position.right = props.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
var className = 'mx_ContextualMenu_wrapper';
|
const className = 'mx_ContextualMenu_wrapper';
|
||||||
|
|
||||||
var menuClasses = classNames({
|
const menuClasses = classNames({
|
||||||
'mx_ContextualMenu': true,
|
'mx_ContextualMenu': true,
|
||||||
'mx_ContextualMenu_left': props.left,
|
'mx_ContextualMenu_left': props.left,
|
||||||
'mx_ContextualMenu_right': !props.left,
|
'mx_ContextualMenu_right': !props.left,
|
||||||
});
|
});
|
||||||
|
|
||||||
var menuStyle = {};
|
const menuStyle = {};
|
||||||
if (props.menuWidth) {
|
if (props.menuWidth) {
|
||||||
menuStyle.width = props.menuWidth;
|
menuStyle.width = props.menuWidth;
|
||||||
}
|
}
|
||||||
|
@ -113,14 +113,14 @@ module.exports = {
|
||||||
|
|
||||||
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
||||||
// property set here so you can't close the menu from a button click!
|
// property set here so you can't close the menu from a button click!
|
||||||
var menu = (
|
const menu = (
|
||||||
<div className={className} style={position}>
|
<div className={className} style={position}>
|
||||||
<div className={menuClasses} style={menuStyle}>
|
<div className={menuClasses} style={menuStyle}>
|
||||||
{chevron}
|
{ chevron }
|
||||||
<Element {...props} onFinished={closeMenu}/>
|
<Element {...props} onFinished={closeMenu} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
|
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
|
||||||
<style>{chevronCSS}</style>
|
<style>{ chevronCSS }</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onCreateRoom: function() {
|
onCreateRoom: function() {
|
||||||
var options = {};
|
const options = {};
|
||||||
|
|
||||||
if (this.state.room_name) {
|
if (this.state.room_name) {
|
||||||
options.name = this.state.room_name;
|
options.name = this.state.room_name;
|
||||||
|
@ -79,14 +79,14 @@ module.exports = React.createClass({
|
||||||
{
|
{
|
||||||
type: "m.room.join_rules",
|
type: "m.room.join_rules",
|
||||||
content: {
|
content: {
|
||||||
"join_rule": this.state.is_private ? "invite" : "public"
|
"join_rule": this.state.is_private ? "invite" : "public",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "m.room.history_visibility",
|
type: "m.room.history_visibility",
|
||||||
content: {
|
content: {
|
||||||
"history_visibility": this.state.share_history ? "shared" : "invited"
|
"history_visibility": this.state.share_history ? "shared" : "invited",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -94,19 +94,19 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
options.invite = this.state.invited_users;
|
options.invite = this.state.invited_users;
|
||||||
|
|
||||||
var alias = this.getAliasLocalpart();
|
const alias = this.getAliasLocalpart();
|
||||||
if (alias) {
|
if (alias) {
|
||||||
options.room_alias_name = alias;
|
options.room_alias_name = alias;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (!cli) {
|
if (!cli) {
|
||||||
// TODO: Error.
|
// TODO: Error.
|
||||||
console.error("Cannot create room: No matrix client.");
|
console.error("Cannot create room: No matrix client.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var deferred = cli.createRoom(options);
|
const deferred = cli.createRoom(options);
|
||||||
|
|
||||||
if (this.state.encrypt) {
|
if (this.state.encrypt) {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -116,7 +116,7 @@ module.exports = React.createClass({
|
||||||
phase: this.phases.CREATING,
|
phase: this.phases.CREATING,
|
||||||
});
|
});
|
||||||
|
|
||||||
var self = this;
|
const self = this;
|
||||||
|
|
||||||
deferred.then(function(resp) {
|
deferred.then(function(resp) {
|
||||||
self.setState({
|
self.setState({
|
||||||
|
@ -209,7 +209,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onAliasChanged: function(alias) {
|
onAliasChanged: function(alias) {
|
||||||
this.setState({
|
this.setState({
|
||||||
alias: alias
|
alias: alias,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -220,64 +220,64 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var curr_phase = this.state.phase;
|
const curr_phase = this.state.phase;
|
||||||
if (curr_phase == this.phases.CREATING) {
|
if (curr_phase == this.phases.CREATING) {
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
return (
|
return (
|
||||||
<Loader/>
|
<Loader />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
var error_box = "";
|
let error_box = "";
|
||||||
if (curr_phase == this.phases.ERROR) {
|
if (curr_phase == this.phases.ERROR) {
|
||||||
error_box = (
|
error_box = (
|
||||||
<div className="mx_Error">
|
<div className="mx_Error">
|
||||||
{_t('An error occurred: %(error_string)s', {error_string: this.state.error_string})}
|
{ _t('An error occurred: %(error_string)s', {error_string: this.state.error_string}) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton");
|
const CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton");
|
||||||
var RoomAlias = sdk.getComponent("create_room.RoomAlias");
|
const RoomAlias = sdk.getComponent("create_room.RoomAlias");
|
||||||
var Presets = sdk.getComponent("create_room.Presets");
|
const Presets = sdk.getComponent("create_room.Presets");
|
||||||
var UserSelector = sdk.getComponent("elements.UserSelector");
|
const UserSelector = sdk.getComponent("elements.UserSelector");
|
||||||
var SimpleRoomHeader = sdk.getComponent("rooms.SimpleRoomHeader");
|
const SimpleRoomHeader = sdk.getComponent("rooms.SimpleRoomHeader");
|
||||||
|
|
||||||
var domain = MatrixClientPeg.get().getDomain();
|
const domain = MatrixClientPeg.get().getDomain();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CreateRoom">
|
<div className="mx_CreateRoom">
|
||||||
<SimpleRoomHeader title={_t("Create Room")} 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={_t('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={_t('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} />
|
||||||
{_t('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} />
|
||||||
{_t('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} />
|
||||||
{_t('Encrypt room')}
|
{ _t('Encrypt room') }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CreateRoomButton onCreateRoom={this.onCreateRoom} /> <br />
|
<CreateRoomButton onCreateRoom={this.onCreateRoom} /> <br />
|
||||||
</div>
|
</div>
|
||||||
{error_box}
|
{ error_box }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { _t, _tJsx } from '../../languageHandler';
|
||||||
/*
|
/*
|
||||||
* Component which shows the filtered file using a TimelinePanel
|
* Component which shows the filtered file using a TimelinePanel
|
||||||
*/
|
*/
|
||||||
var FilePanel = React.createClass({
|
const FilePanel = React.createClass({
|
||||||
displayName: 'FilePanel',
|
displayName: 'FilePanel',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -55,33 +55,33 @@ var FilePanel = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTimelineSet: function(roomId) {
|
updateTimelineSet: function(roomId) {
|
||||||
var client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
var room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
|
|
||||||
this.noRoom = !room;
|
this.noRoom = !room;
|
||||||
|
|
||||||
if (room) {
|
if (room) {
|
||||||
var filter = new Matrix.Filter(client.credentials.userId);
|
const filter = new Matrix.Filter(client.credentials.userId);
|
||||||
filter.setDefinition(
|
filter.setDefinition(
|
||||||
{
|
{
|
||||||
"room": {
|
"room": {
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"contains_url": true
|
"contains_url": true,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// FIXME: we shouldn't be doing this every time we change room - see comment above.
|
// FIXME: we shouldn't be doing this every time we change room - see comment above.
|
||||||
client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then(
|
client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then(
|
||||||
(filterId)=>{
|
(filterId)=>{
|
||||||
filter.filterId = filterId;
|
filter.filterId = filterId;
|
||||||
var timelineSet = room.getOrCreateFilteredTimelineSet(filter);
|
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
|
||||||
this.setState({ timelineSet: timelineSet });
|
this.setState({ timelineSet: timelineSet });
|
||||||
},
|
},
|
||||||
(error)=>{
|
(error)=>{
|
||||||
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!");
|
||||||
|
@ -92,18 +92,18 @@ var FilePanel = React.createClass({
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||||
<div className="mx_RoomView_empty">
|
<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>)}
|
{ _tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{ sub }</a>) }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.noRoom) {
|
} else if (this.noRoom) {
|
||||||
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
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 className="mx_RoomView_empty">{ _t("You must join the room to see its files") }</div>
|
||||||
</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");
|
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
if (this.state.timelineSet) {
|
if (this.state.timelineSet) {
|
||||||
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
|
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
|
||||||
|
@ -114,17 +114,16 @@ var FilePanel = React.createClass({
|
||||||
manageReadReceipts={false}
|
manageReadReceipts={false}
|
||||||
manageReadMarkers={false}
|
manageReadMarkers={false}
|
||||||
timelineSet={this.state.timelineSet}
|
timelineSet={this.state.timelineSet}
|
||||||
showUrlPreview = { false }
|
showUrlPreview = {false}
|
||||||
tileShape="file_grid"
|
tileShape="file_grid"
|
||||||
opacity={ this.props.opacity }
|
opacity={this.props.opacity}
|
||||||
empty={_t('There are no visible files in this room')}
|
empty={_t('There are no visible files in this room')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_FilePanel">
|
<div className="mx_FilePanel">
|
||||||
<Loader/>
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd.
|
Copyright 2017 Vector Creations Ltd.
|
||||||
|
Copyright 2017 New Vector 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,6 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import Promise from 'bluebird';
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
|
@ -25,6 +27,11 @@ import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
import Modal from '../../Modal';
|
import Modal from '../../Modal';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import GroupStoreCache from '../../stores/GroupStoreCache';
|
||||||
|
import GroupStore from '../../stores/GroupStore';
|
||||||
|
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||||
|
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
|
|
||||||
const RoomSummaryType = PropTypes.shape({
|
const RoomSummaryType = PropTypes.shape({
|
||||||
room_id: PropTypes.string.isRequired,
|
room_id: PropTypes.string.isRequired,
|
||||||
profile: PropTypes.shape({
|
profile: PropTypes.shape({
|
||||||
|
@ -37,6 +44,9 @@ const RoomSummaryType = PropTypes.shape({
|
||||||
const UserSummaryType = PropTypes.shape({
|
const UserSummaryType = PropTypes.shape({
|
||||||
summaryInfo: PropTypes.shape({
|
summaryInfo: PropTypes.shape({
|
||||||
user_id: PropTypes.string.isRequired,
|
user_id: PropTypes.string.isRequired,
|
||||||
|
role_id: PropTypes.string,
|
||||||
|
avatar_url: PropTypes.string,
|
||||||
|
displayname: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -50,19 +60,81 @@ const CategoryRoomList = React.createClass({
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
}),
|
}),
|
||||||
|
groupId: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
// Whether the list should be editable
|
||||||
|
editing: PropTypes.bool.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
onAddRoomsToSummaryClicked: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
|
Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
|
||||||
|
title: _t('Add rooms to the community summary'),
|
||||||
|
description: _t("Which rooms would you like to add to this summary?"),
|
||||||
|
placeholder: _t("Room name or alias"),
|
||||||
|
button: _t("Add to summary"),
|
||||||
|
pickerType: 'room',
|
||||||
|
validAddressTypes: ['mx-room-id'],
|
||||||
|
groupId: this.props.groupId,
|
||||||
|
onFinished: (success, addrs) => {
|
||||||
|
if (!success) return;
|
||||||
|
const errorList = [];
|
||||||
|
Promise.all(addrs.map((addr) => {
|
||||||
|
return this.context.groupStore
|
||||||
|
.addRoomToGroupSummary(addr.address)
|
||||||
|
.catch(() => { errorList.push(addr.address); })
|
||||||
|
.reflect();
|
||||||
|
})).then(() => {
|
||||||
|
if (errorList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog(
|
||||||
|
'Failed to add the following room to the group summary',
|
||||||
|
'', ErrorDialog,
|
||||||
|
{
|
||||||
|
title: _t(
|
||||||
|
"Failed to add the following rooms to the summary of %(groupId)s:",
|
||||||
|
{groupId: this.props.groupId},
|
||||||
|
),
|
||||||
|
description: errorList.join(", "),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
const addButton = this.props.editing ?
|
||||||
|
(<AccessibleButton className="mx_GroupView_featuredThings_addButton"
|
||||||
|
onClick={this.onAddRoomsToSummaryClicked}
|
||||||
|
>
|
||||||
|
<TintableSvg src="img/icons-create-room.svg" width="64" height="64" />
|
||||||
|
<div className="mx_GroupView_featuredThings_addButton_label">
|
||||||
|
{ _t('Add a Room') }
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>) : <div />;
|
||||||
|
|
||||||
const roomNodes = this.props.rooms.map((r) => {
|
const roomNodes = this.props.rooms.map((r) => {
|
||||||
return <FeaturedRoom key={r.room_id} summaryInfo={r} />;
|
return <FeaturedRoom
|
||||||
|
key={r.room_id}
|
||||||
|
groupId={this.props.groupId}
|
||||||
|
editing={this.props.editing}
|
||||||
|
summaryInfo={r} />;
|
||||||
});
|
});
|
||||||
let catHeader = null;
|
|
||||||
|
let catHeader = <div />;
|
||||||
if (this.props.category && this.props.category.profile) {
|
if (this.props.category && this.props.category.profile) {
|
||||||
catHeader = <div className="mx_GroupView_featuredThings_category">{this.props.category.profile.name}</div>;
|
catHeader = <div className="mx_GroupView_featuredThings_category">
|
||||||
|
{ this.props.category.profile.name }
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
return <div>
|
return <div className="mx_GroupView_featuredThings_container">
|
||||||
{catHeader}
|
{ catHeader }
|
||||||
{roomNodes}
|
{ roomNodes }
|
||||||
|
{ addButton }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -72,6 +144,8 @@ const FeaturedRoom = React.createClass({
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
summaryInfo: RoomSummaryType.isRequired,
|
summaryInfo: RoomSummaryType.isRequired,
|
||||||
|
editing: PropTypes.bool.isRequired,
|
||||||
|
groupId: PropTypes.string.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick: function(e) {
|
onClick: function(e) {
|
||||||
|
@ -85,28 +159,69 @@ const FeaturedRoom = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onDeleteClicked: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.context.groupStore.removeRoomFromGroupSummary(
|
||||||
|
this.props.summaryInfo.room_id,
|
||||||
|
).catch((err) => {
|
||||||
|
console.error('Error whilst removing room from group summary', err);
|
||||||
|
const roomName = this.props.summaryInfo.name ||
|
||||||
|
this.props.summaryInfo.canonical_alias ||
|
||||||
|
this.props.summaryInfo.room_id;
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog(
|
||||||
|
'Failed to remove room from group summary',
|
||||||
|
'', ErrorDialog,
|
||||||
|
{
|
||||||
|
title: _t(
|
||||||
|
"Failed to remove the room from the summary of %(groupId)s",
|
||||||
|
{groupId: this.props.groupId},
|
||||||
|
),
|
||||||
|
description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||||
|
|
||||||
|
const roomName = this.props.summaryInfo.profile.name ||
|
||||||
|
this.props.summaryInfo.profile.canonical_alias ||
|
||||||
|
_t("Unnamed Room");
|
||||||
|
|
||||||
const oobData = {
|
const oobData = {
|
||||||
roomId: this.props.summaryInfo.room_id,
|
roomId: this.props.summaryInfo.room_id,
|
||||||
avatarUrl: this.props.summaryInfo.profile.avatar_url,
|
avatarUrl: this.props.summaryInfo.profile.avatar_url,
|
||||||
name: this.props.summaryInfo.profile.name,
|
name: roomName,
|
||||||
};
|
};
|
||||||
|
|
||||||
let permalink = null;
|
let permalink = null;
|
||||||
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
|
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
|
||||||
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
|
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomNameNode = null;
|
let roomNameNode = null;
|
||||||
if (permalink) {
|
if (permalink) {
|
||||||
roomNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.profile.name}</a>;
|
roomNameNode = <a href={permalink} onClick={this.onClick} >{ roomName }</a>;
|
||||||
} else {
|
} else {
|
||||||
roomNameNode = <span>{this.props.summaryInfo.profile.name}</span>;
|
roomNameNode = <span>{ roomName }</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteButton = this.props.editing ?
|
||||||
|
<img
|
||||||
|
className="mx_GroupView_featuredThing_deleteButton"
|
||||||
|
src="img/cancel-small.svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
alt="Delete"
|
||||||
|
onClick={this.onDeleteClicked} />
|
||||||
|
: <div />;
|
||||||
|
|
||||||
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
|
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
|
||||||
<RoomAvatar oobData={oobData} width={64} height={64} />
|
<RoomAvatar oobData={oobData} width={64} height={64} />
|
||||||
<div className="mx_GroupView_featuredThing_name">{roomNameNode}</div>
|
<div className="mx_GroupView_featuredThing_name">{ roomNameNode }</div>
|
||||||
|
{ deleteButton }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -121,19 +236,75 @@ const RoleUserList = React.createClass({
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
}),
|
}),
|
||||||
|
groupId: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
// Whether the list should be editable
|
||||||
|
editing: PropTypes.bool.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
onAddUsersClicked: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
|
Modal.createTrackedDialog('Add Users to Group Summary', '', AddressPickerDialog, {
|
||||||
|
title: _t('Add users to the community summary'),
|
||||||
|
description: _t("Who would you like to add to this summary?"),
|
||||||
|
placeholder: _t("Name or matrix ID"),
|
||||||
|
button: _t("Add to summary"),
|
||||||
|
validAddressTypes: ['mx-user-id'],
|
||||||
|
groupId: this.props.groupId,
|
||||||
|
shouldOmitSelf: false,
|
||||||
|
onFinished: (success, addrs) => {
|
||||||
|
if (!success) return;
|
||||||
|
const errorList = [];
|
||||||
|
Promise.all(addrs.map((addr) => {
|
||||||
|
return this.context.groupStore
|
||||||
|
.addUserToGroupSummary(addr.address)
|
||||||
|
.catch(() => { errorList.push(addr.address); })
|
||||||
|
.reflect();
|
||||||
|
})).then(() => {
|
||||||
|
if (errorList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog(
|
||||||
|
'Failed to add the following users to the community summary',
|
||||||
|
'', ErrorDialog,
|
||||||
|
{
|
||||||
|
title: _t(
|
||||||
|
"Failed to add the following users to the summary of %(groupId)s:",
|
||||||
|
{groupId: this.props.groupId},
|
||||||
|
),
|
||||||
|
description: errorList.join(", "),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
const addButton = this.props.editing ?
|
||||||
|
(<AccessibleButton className="mx_GroupView_featuredThings_addButton" onClick={this.onAddUsersClicked}>
|
||||||
|
<TintableSvg src="img/icons-create-room.svg" width="64" height="64" />
|
||||||
|
<div className="mx_GroupView_featuredThings_addButton_label">
|
||||||
|
{ _t('Add a User') }
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>) : <div />;
|
||||||
const userNodes = this.props.users.map((u) => {
|
const userNodes = this.props.users.map((u) => {
|
||||||
return <FeaturedUser key={u.user_id} summaryInfo={u} />;
|
return <FeaturedUser
|
||||||
|
key={u.user_id}
|
||||||
|
summaryInfo={u}
|
||||||
|
editing={this.props.editing}
|
||||||
|
groupId={this.props.groupId} />;
|
||||||
});
|
});
|
||||||
let roleHeader = null;
|
let roleHeader = <div />;
|
||||||
if (this.props.role && this.props.role.profile) {
|
if (this.props.role && this.props.role.profile) {
|
||||||
roleHeader = <div className="mx_GroupView_featuredThings_category">{this.props.role.profile.name}</div>;
|
roleHeader = <div className="mx_GroupView_featuredThings_category">{ this.props.role.profile.name }</div>;
|
||||||
}
|
}
|
||||||
return <div>
|
return <div className="mx_GroupView_featuredThings_container">
|
||||||
{roleHeader}
|
{ roleHeader }
|
||||||
{userNodes}
|
{ userNodes }
|
||||||
|
{ addButton }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -143,6 +314,8 @@ const FeaturedUser = React.createClass({
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
summaryInfo: UserSummaryType.isRequired,
|
summaryInfo: UserSummaryType.isRequired,
|
||||||
|
editing: PropTypes.bool.isRequired,
|
||||||
|
groupId: PropTypes.string.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick: function(e) {
|
onClick: function(e) {
|
||||||
|
@ -156,19 +329,64 @@ const FeaturedUser = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onDeleteClicked: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.context.groupStore.removeUserFromGroupSummary(
|
||||||
|
this.props.summaryInfo.user_id,
|
||||||
|
).catch((err) => {
|
||||||
|
console.error('Error whilst removing user from group summary', err);
|
||||||
|
const displayName = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog(
|
||||||
|
'Failed to remove user from community summary',
|
||||||
|
'', ErrorDialog,
|
||||||
|
{
|
||||||
|
title: _t(
|
||||||
|
"Failed to remove a user from the summary of %(groupId)s",
|
||||||
|
{groupId: this.props.groupId},
|
||||||
|
),
|
||||||
|
description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
// Add avatar once we get profile info inline in the summary response
|
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
//const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
const name = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
|
||||||
|
|
||||||
const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id;
|
const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id;
|
||||||
const userNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.user_id}</a>;
|
const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
|
||||||
|
const httpUrl = MatrixClientPeg.get()
|
||||||
|
.mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
|
||||||
|
|
||||||
|
const deleteButton = this.props.editing ?
|
||||||
|
<img
|
||||||
|
className="mx_GroupView_featuredThing_deleteButton"
|
||||||
|
src="img/cancel-small.svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
alt="Delete"
|
||||||
|
onClick={this.onDeleteClicked} />
|
||||||
|
: <div />;
|
||||||
|
|
||||||
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
|
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
|
||||||
<div className="mx_GroupView_featuredThing_name">{userNameNode}</div>
|
<BaseAvatar name={name} url={httpUrl} width={64} height={64} />
|
||||||
|
<div className="mx_GroupView_featuredThing_name">{ userNameNode }</div>
|
||||||
|
{ deleteButton }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const GroupContext = {
|
||||||
|
groupStore: React.PropTypes.instanceOf(GroupStore).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
CategoryRoomList.contextTypes = GroupContext;
|
||||||
|
FeaturedRoom.contextTypes = GroupContext;
|
||||||
|
RoleUserList.contextTypes = GroupContext;
|
||||||
|
FeaturedUser.contextTypes = GroupContext;
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'GroupView',
|
displayName: 'GroupView',
|
||||||
|
|
||||||
|
@ -176,6 +394,16 @@ export default React.createClass({
|
||||||
groupId: PropTypes.string.isRequired,
|
groupId: PropTypes.string.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
childContextTypes: {
|
||||||
|
groupStore: React.PropTypes.instanceOf(GroupStore),
|
||||||
|
},
|
||||||
|
|
||||||
|
getChildContext: function() {
|
||||||
|
return {
|
||||||
|
groupStore: this._groupStore,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
summary: null,
|
summary: null,
|
||||||
|
@ -183,12 +411,21 @@ export default React.createClass({
|
||||||
editing: false,
|
editing: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
uploadingAvatar: false,
|
uploadingAvatar: false,
|
||||||
|
membershipBusy: false,
|
||||||
|
publicityBusy: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._changeAvatarComponent = null;
|
this._changeAvatarComponent = null;
|
||||||
this._loadGroupFromServer(this.props.groupId);
|
this._initGroupStore(this.props.groupId);
|
||||||
|
|
||||||
|
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
this._groupStore.removeAllListeners();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
componentWillReceiveProps: function(newProps) {
|
||||||
|
@ -197,18 +434,37 @@ export default React.createClass({
|
||||||
summary: null,
|
summary: null,
|
||||||
error: null,
|
error: null,
|
||||||
}, () => {
|
}, () => {
|
||||||
this._loadGroupFromServer(newProps.groupId);
|
this._initGroupStore(newProps.groupId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_loadGroupFromServer: function(groupId) {
|
_onGroupMyMembership: function(group) {
|
||||||
MatrixClientPeg.get().getGroupSummary(groupId).done((res) => {
|
if (group.groupId !== this.props.groupId) return;
|
||||||
|
|
||||||
|
this.setState({membershipBusy: false});
|
||||||
|
},
|
||||||
|
|
||||||
|
_initGroupStore: function(groupId) {
|
||||||
|
this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
|
||||||
|
this._groupStore.on('update', () => {
|
||||||
|
const summary = this._groupStore.getSummary();
|
||||||
|
if (summary.profile) {
|
||||||
|
// Default profile fields should be "" for later sending to the server (which
|
||||||
|
// requires that the fields are strings, not null)
|
||||||
|
["avatar_url", "long_description", "name", "short_description"].forEach((k) => {
|
||||||
|
summary.profile[k] = summary.profile[k] || "";
|
||||||
|
});
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
summary: res,
|
summary,
|
||||||
|
isGroupPublicised: this._groupStore.getGroupPublicity(),
|
||||||
|
isUserPrivileged: this._groupStore.isUserPrivileged(),
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
}, (err) => {
|
});
|
||||||
|
this._groupStore.on('error', (err) => {
|
||||||
|
console.error(err);
|
||||||
this.setState({
|
this.setState({
|
||||||
summary: null,
|
summary: null,
|
||||||
error: err,
|
error: err,
|
||||||
|
@ -216,6 +472,10 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onShowRhsClick: function(ev) {
|
||||||
|
dis.dispatch({ action: 'show_right_panel' });
|
||||||
|
},
|
||||||
|
|
||||||
_onEditClick: function() {
|
_onEditClick: function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
editing: true,
|
editing: true,
|
||||||
|
@ -230,15 +490,15 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onNameChange: function(e) {
|
_onNameChange: function(value) {
|
||||||
const newProfileForm = Object.assign(this.state.profileForm, { name: e.target.value });
|
const newProfileForm = Object.assign(this.state.profileForm, { name: value });
|
||||||
this.setState({
|
this.setState({
|
||||||
profileForm: newProfileForm,
|
profileForm: newProfileForm,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onShortDescChange: function(e) {
|
_onShortDescChange: function(value) {
|
||||||
const newProfileForm = Object.assign(this.state.profileForm, { short_description: e.target.value });
|
const newProfileForm = Object.assign(this.state.profileForm, { short_description: value });
|
||||||
this.setState({
|
this.setState({
|
||||||
profileForm: newProfileForm,
|
profileForm: newProfileForm,
|
||||||
});
|
});
|
||||||
|
@ -281,24 +541,113 @@ export default React.createClass({
|
||||||
editing: false,
|
editing: false,
|
||||||
summary: null,
|
summary: null,
|
||||||
});
|
});
|
||||||
this._loadGroupFromServer(this.props.groupId);
|
this._initGroupStore(this.props.groupId);
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
saving: false,
|
saving: false,
|
||||||
});
|
});
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Failed to save group profile", e);
|
console.error("Failed to save community profile", e);
|
||||||
Modal.createTrackedDialog('Failed to update group', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to update group', '', ErrorDialog, {
|
||||||
title: _t('Error'),
|
title: _t('Error'),
|
||||||
description: _t('Failed to update group'),
|
description: _t('Failed to update community'),
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
},
|
},
|
||||||
|
|
||||||
_getFeaturedRoomsNode() {
|
_onAcceptInviteClick: function() {
|
||||||
const summary = this.state.summary;
|
this.setState({membershipBusy: true});
|
||||||
|
MatrixClientPeg.get().acceptGroupInvite(this.props.groupId).then(() => {
|
||||||
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({membershipBusy: false});
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Error accepting invite', '', ErrorDialog, {
|
||||||
|
title: _t("Error"),
|
||||||
|
description: _t("Unable to accept invite"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
if (summary.rooms_section.rooms.length == 0) return null;
|
_onRejectInviteClick: function() {
|
||||||
|
this.setState({membershipBusy: true});
|
||||||
|
MatrixClientPeg.get().leaveGroup(this.props.groupId).then(() => {
|
||||||
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({membershipBusy: false});
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, {
|
||||||
|
title: _t("Error"),
|
||||||
|
description: _t("Unable to reject invite"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onLeaveClick: function() {
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
|
||||||
|
title: _t("Leave Community"),
|
||||||
|
description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}),
|
||||||
|
button: _t("Leave"),
|
||||||
|
danger: true,
|
||||||
|
onFinished: (confirmed) => {
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
this.setState({membershipBusy: true});
|
||||||
|
MatrixClientPeg.get().leaveGroup(this.props.groupId).then(() => {
|
||||||
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({membershipBusy: false});
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Error leaving room', '', ErrorDialog, {
|
||||||
|
title: _t("Error"),
|
||||||
|
description: _t("Unable to leave room"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onAddRoomsClick: function() {
|
||||||
|
showGroupAddRoomDialog(this.props.groupId);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onPublicityToggle: function() {
|
||||||
|
this.setState({
|
||||||
|
publicityBusy: true,
|
||||||
|
});
|
||||||
|
const publicity = !this.state.isGroupPublicised;
|
||||||
|
this._groupStore.setGroupPublicity(publicity).then(() => {
|
||||||
|
this.setState({
|
||||||
|
publicityBusy: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_getRoomsNode: function() {
|
||||||
|
const RoomDetailList = sdk.getComponent('rooms.RoomDetailList');
|
||||||
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||||
|
const addButton = this.state.editing ?
|
||||||
|
(<AccessibleButton onClick={this._onAddRoomsClick} >
|
||||||
|
<div className="mx_GroupView_rooms_header_addButton" >
|
||||||
|
<TintableSvg src="img/icons-room-add.svg" width="24" height="24" />
|
||||||
|
</div>
|
||||||
|
<div className="mx_GroupView_rooms_header_addButton_label">
|
||||||
|
{ _t('Add rooms to this community') }
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>) : <div />;
|
||||||
|
return <div className="mx_GroupView_rooms">
|
||||||
|
<div className="mx_GroupView_rooms_header">
|
||||||
|
<h3>{ _t('Rooms') }</h3>
|
||||||
|
{ addButton }
|
||||||
|
</div>
|
||||||
|
<RoomDetailList rooms={this._groupStore.getGroupRooms()} />
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getFeaturedRoomsNode: function() {
|
||||||
|
const summary = this.state.summary;
|
||||||
|
|
||||||
const defaultCategoryRooms = [];
|
const defaultCategoryRooms = [];
|
||||||
const categoryRooms = {};
|
const categoryRooms = {};
|
||||||
|
@ -315,29 +664,32 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let defaultCategoryNode = null;
|
const defaultCategoryNode = <CategoryRoomList
|
||||||
if (defaultCategoryRooms.length > 0) {
|
rooms={defaultCategoryRooms}
|
||||||
defaultCategoryNode = <CategoryRoomList rooms={defaultCategoryRooms} />;
|
groupId={this.props.groupId}
|
||||||
}
|
editing={this.state.editing} />;
|
||||||
const categoryRoomNodes = Object.keys(categoryRooms).map((catId) => {
|
const categoryRoomNodes = Object.keys(categoryRooms).map((catId) => {
|
||||||
const cat = summary.rooms_section.categories[catId];
|
const cat = summary.rooms_section.categories[catId];
|
||||||
return <CategoryRoomList key={catId} rooms={categoryRooms[catId]} category={cat} />;
|
return <CategoryRoomList
|
||||||
|
key={catId}
|
||||||
|
rooms={categoryRooms[catId]}
|
||||||
|
category={cat}
|
||||||
|
groupId={this.props.groupId}
|
||||||
|
editing={this.state.editing} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className="mx_GroupView_featuredThings">
|
return <div className="mx_GroupView_featuredThings">
|
||||||
<div className="mx_GroupView_featuredThings_header">
|
<div className="mx_GroupView_featuredThings_header">
|
||||||
{_t('Featured Rooms:')}
|
{ _t('Featured Rooms:') }
|
||||||
</div>
|
</div>
|
||||||
{defaultCategoryNode}
|
{ defaultCategoryNode }
|
||||||
{categoryRoomNodes}
|
{ categoryRoomNodes }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
_getFeaturedUsersNode() {
|
_getFeaturedUsersNode: function() {
|
||||||
const summary = this.state.summary;
|
const summary = this.state.summary;
|
||||||
|
|
||||||
if (summary.users_section.users.length == 0) return null;
|
|
||||||
|
|
||||||
const noRoleUsers = [];
|
const noRoleUsers = [];
|
||||||
const roleUsers = {};
|
const roleUsers = {};
|
||||||
summary.users_section.users.forEach((u) => {
|
summary.users_section.users.forEach((u) => {
|
||||||
|
@ -353,46 +705,156 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let noRoleNode = null;
|
const noRoleNode = <RoleUserList
|
||||||
if (noRoleUsers.length > 0) {
|
users={noRoleUsers}
|
||||||
noRoleNode = <RoleUserList users={noRoleUsers} />;
|
groupId={this.props.groupId}
|
||||||
}
|
editing={this.state.editing} />;
|
||||||
const roleUserNodes = Object.keys(roleUsers).map((roleId) => {
|
const roleUserNodes = Object.keys(roleUsers).map((roleId) => {
|
||||||
const role = summary.users_section.roles[roleId];
|
const role = summary.users_section.roles[roleId];
|
||||||
return <RoleUserList key={roleId} users={roleUsers[roleId]} role={role} />;
|
return <RoleUserList
|
||||||
|
key={roleId}
|
||||||
|
users={roleUsers[roleId]}
|
||||||
|
role={role}
|
||||||
|
groupId={this.props.groupId}
|
||||||
|
editing={this.state.editing} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className="mx_GroupView_featuredThings">
|
return <div className="mx_GroupView_featuredThings">
|
||||||
<div className="mx_GroupView_featuredThings_header">
|
<div className="mx_GroupView_featuredThings_header">
|
||||||
{_t('Featured Users:')}
|
{ _t('Featured Users:') }
|
||||||
</div>
|
</div>
|
||||||
{noRoleNode}
|
{ noRoleNode }
|
||||||
{roleUserNodes}
|
{ roleUserNodes }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_getMembershipSection: function() {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
|
const group = MatrixClientPeg.get().getGroup(this.props.groupId);
|
||||||
|
if (!group) return null;
|
||||||
|
|
||||||
|
if (group.myMembership === 'invite') {
|
||||||
|
if (this.state.membershipBusy) {
|
||||||
|
return <div className="mx_GroupView_membershipSection">
|
||||||
|
<Spinner />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_invited">
|
||||||
|
<div className="mx_GroupView_membershipSubSection">
|
||||||
|
<div className="mx_GroupView_membershipSection_description">
|
||||||
|
{ _t("%(inviter)s has invited you to join this community", {inviter: group.inviter.userId}) }
|
||||||
|
</div>
|
||||||
|
<div className="mx_GroupView_membership_buttonContainer">
|
||||||
|
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||||
|
onClick={this._onAcceptInviteClick}
|
||||||
|
>
|
||||||
|
{ _t("Accept") }
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||||
|
onClick={this._onRejectInviteClick}
|
||||||
|
>
|
||||||
|
{ _t("Decline") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
} else if (group.myMembership === 'join' && this.state.editing) {
|
||||||
|
const leaveButtonTooltip = this.state.isUserPrivileged ?
|
||||||
|
_t("You are a member of this community") :
|
||||||
|
_t("You are an administrator of this community");
|
||||||
|
const leaveButtonClasses = classnames({
|
||||||
|
"mx_RoomHeader_textButton": true,
|
||||||
|
"mx_GroupView_textButton": true,
|
||||||
|
"mx_GroupView_leaveButton": true,
|
||||||
|
"mx_RoomHeader_textButton_danger": this.state.isUserPrivileged,
|
||||||
|
});
|
||||||
|
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_joined">
|
||||||
|
<div className="mx_GroupView_membershipSubSection">
|
||||||
|
{ /* Empty div for flex alignment */ }
|
||||||
|
<div />
|
||||||
|
<div className="mx_GroupView_membership_buttonContainer">
|
||||||
|
<AccessibleButton
|
||||||
|
className={leaveButtonClasses}
|
||||||
|
onClick={this._onLeaveClick}
|
||||||
|
title={leaveButtonTooltip}
|
||||||
|
>
|
||||||
|
{ _t("Leave") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getMemberSettingsSection: function() {
|
||||||
|
return <div className="mx_GroupView_memberSettings">
|
||||||
|
<h3> { _t("Community Member Settings") } </h3>
|
||||||
|
<div className="mx_GroupView_memberSettings_toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
onClick={this._onPublicityToggle}
|
||||||
|
checked={this.state.isGroupPublicised}
|
||||||
|
tabIndex="3"
|
||||||
|
id="isGroupPublicised"
|
||||||
|
/>
|
||||||
|
<label htmlFor="isGroupPublicised"
|
||||||
|
onClick={this._onPublicityToggle}
|
||||||
|
>
|
||||||
|
{ _t("Publish this community on your profile") }
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getLongDescriptionNode: function() {
|
||||||
|
const summary = this.state.summary;
|
||||||
|
let description = null;
|
||||||
|
if (summary.profile && summary.profile.long_description) {
|
||||||
|
description = sanitizedHtmlNode(summary.profile.long_description);
|
||||||
|
}
|
||||||
|
return this.state.editing && this.state.isUserPrivileged ?
|
||||||
|
<div className="mx_GroupView_groupDesc">
|
||||||
|
<h3> { _t("Long Description (HTML)") } </h3>
|
||||||
|
<textarea
|
||||||
|
value={this.state.profileForm.long_description}
|
||||||
|
onChange={this._onLongDescChange}
|
||||||
|
tabIndex="4"
|
||||||
|
key="editLongDesc"
|
||||||
|
/>
|
||||||
|
</div> :
|
||||||
|
<div className="mx_GroupView_groupDesc">
|
||||||
|
{ description }
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
|
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
if (this.state.summary === null && this.state.error === null || this.state.saving) {
|
if (this.state.summary === null && this.state.error === null || this.state.saving) {
|
||||||
return <Loader />;
|
return <Spinner />;
|
||||||
} else if (this.state.summary) {
|
} else if (this.state.summary) {
|
||||||
const summary = this.state.summary;
|
const summary = this.state.summary;
|
||||||
|
|
||||||
let avatarNode;
|
let avatarNode;
|
||||||
let nameNode;
|
let nameNode;
|
||||||
let shortDescNode;
|
let shortDescNode;
|
||||||
let rightButtons;
|
const bodyNodes = [
|
||||||
let roomBody;
|
this._getMembershipSection(),
|
||||||
|
this.state.editing ? this._getMemberSettingsSection() : null,
|
||||||
|
this._getLongDescriptionNode(),
|
||||||
|
this._getRoomsNode(),
|
||||||
|
];
|
||||||
|
const rightButtons = [];
|
||||||
const headerClasses = {
|
const headerClasses = {
|
||||||
mx_GroupView_header: true,
|
mx_GroupView_header: true,
|
||||||
};
|
};
|
||||||
if (this.state.editing) {
|
if (this.state.editing) {
|
||||||
let avatarImage;
|
let avatarImage;
|
||||||
if (this.state.uploadingAvatar) {
|
if (this.state.uploadingAvatar) {
|
||||||
avatarImage = <Loader />;
|
avatarImage = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
|
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
|
||||||
avatarImage = <GroupAvatar groupId={this.props.groupId}
|
avatarImage = <GroupAvatar groupId={this.props.groupId}
|
||||||
|
@ -404,45 +866,53 @@ export default React.createClass({
|
||||||
avatarNode = (
|
avatarNode = (
|
||||||
<div className="mx_GroupView_avatarPicker">
|
<div className="mx_GroupView_avatarPicker">
|
||||||
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||||
{avatarImage}
|
{ avatarImage }
|
||||||
</label>
|
</label>
|
||||||
<div className="mx_GroupView_avatarPicker_edit">
|
<div className="mx_GroupView_avatarPicker_edit">
|
||||||
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||||
<img src="img/camera.svg"
|
<img src="img/camera.svg"
|
||||||
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
|
alt={_t("Upload avatar")} title={_t("Upload avatar")}
|
||||||
width="17" height="15" />
|
width="17" height="15" />
|
||||||
</label>
|
</label>
|
||||||
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected}/>
|
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
nameNode = <input type="text"
|
|
||||||
value={this.state.profileForm.name}
|
const EditableText = sdk.getComponent("elements.EditableText");
|
||||||
onChange={this._onNameChange}
|
|
||||||
placeholder={_t('Group Name')}
|
nameNode = <EditableText ref="nameEditor"
|
||||||
tabIndex="1"
|
className="mx_GroupView_editable"
|
||||||
/>;
|
placeholderClassName="mx_GroupView_placeholder"
|
||||||
shortDescNode = <input type="text"
|
placeholder={_t('Community Name')}
|
||||||
value={this.state.profileForm.short_description}
|
blurToCancel={false}
|
||||||
onChange={this._onShortDescChange}
|
initialValue={this.state.profileForm.name}
|
||||||
placeholder={_t('Description')}
|
onValueChanged={this._onNameChange}
|
||||||
tabIndex="2"
|
tabIndex="1"
|
||||||
/>;
|
dir="auto" />;
|
||||||
rightButtons = <span>
|
|
||||||
<AccessibleButton className="mx_GroupView_saveButton mx_RoomHeader_textButton" onClick={this._onSaveClick}>
|
shortDescNode = <EditableText ref="descriptionEditor"
|
||||||
{_t('Save')}
|
className="mx_GroupView_editable"
|
||||||
</AccessibleButton>
|
placeholderClassName="mx_GroupView_placeholder"
|
||||||
<AccessibleButton className='mx_GroupView_cancelButton' onClick={this._onCancelClick}>
|
placeholder={_t("Description")}
|
||||||
<img src="img/cancel.svg" className='mx_filterFlipColor'
|
blurToCancel={false}
|
||||||
width="18" height="18" alt={_t("Cancel")}/>
|
initialValue={this.state.profileForm.short_description}
|
||||||
</AccessibleButton>
|
onValueChanged={this._onShortDescChange}
|
||||||
</span>;
|
tabIndex="2"
|
||||||
roomBody = <div>
|
dir="auto" />;
|
||||||
<textarea className="mx_GroupView_editLongDesc" value={this.state.profileForm.long_description}
|
rightButtons.push(
|
||||||
onChange={this._onLongDescChange}
|
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||||
tabIndex="3"
|
onClick={this._onSaveClick} key="_saveButton"
|
||||||
/>
|
>
|
||||||
</div>;
|
{ _t('Save') }
|
||||||
|
</AccessibleButton>,
|
||||||
|
);
|
||||||
|
rightButtons.push(
|
||||||
|
<AccessibleButton className="mx_RoomHeader_cancelButton" onClick={this._onCancelClick} key="_cancelButton">
|
||||||
|
<img src="img/cancel.svg" className="mx_filterFlipColor"
|
||||||
|
width="18" height="18" alt={_t("Cancel")} />
|
||||||
|
</AccessibleButton>,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
|
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
|
||||||
avatarNode = <GroupAvatar
|
avatarNode = <GroupAvatar
|
||||||
|
@ -451,32 +921,34 @@ export default React.createClass({
|
||||||
width={48} height={48}
|
width={48} height={48}
|
||||||
/>;
|
/>;
|
||||||
if (summary.profile && summary.profile.name) {
|
if (summary.profile && summary.profile.name) {
|
||||||
nameNode = <div>
|
nameNode = <div onClick={this._onEditClick}>
|
||||||
<span>{summary.profile.name}</span>
|
<span>{ summary.profile.name }</span>
|
||||||
<span className="mx_GroupView_header_groupid">
|
<span className="mx_GroupView_header_groupid">
|
||||||
({this.props.groupId})
|
({ this.props.groupId })
|
||||||
</span>
|
</span>
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
nameNode = <span>{this.props.groupId}</span>;
|
nameNode = <span onClick={this._onEditClick}>{ this.props.groupId }</span>;
|
||||||
}
|
}
|
||||||
shortDescNode = <span>{summary.profile.short_description}</span>;
|
if (summary.profile && summary.profile.short_description) {
|
||||||
|
shortDescNode = <span onClick={this._onEditClick}>{ summary.profile.short_description }</span>;
|
||||||
let description = null;
|
}
|
||||||
if (summary.profile && summary.profile.long_description) {
|
rightButtons.push(
|
||||||
description = sanitizedHtmlNode(summary.profile.long_description);
|
<AccessibleButton className="mx_GroupHeader_button"
|
||||||
|
onClick={this._onEditClick} title={_t("Community Settings")} key="_editButton"
|
||||||
|
>
|
||||||
|
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16" />
|
||||||
|
</AccessibleButton>,
|
||||||
|
);
|
||||||
|
if (this.props.collapsedRhs) {
|
||||||
|
rightButtons.push(
|
||||||
|
<AccessibleButton className="mx_GroupHeader_button"
|
||||||
|
onClick={this._onShowRhsClick} title={_t('Show panel')} key="_maximiseButton"
|
||||||
|
>
|
||||||
|
<TintableSvg src="img/maximise.svg" width="10" height="16" />
|
||||||
|
</AccessibleButton>,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
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;
|
headerClasses.mx_GroupView_header_view = true;
|
||||||
}
|
}
|
||||||
|
@ -486,40 +958,42 @@ export default React.createClass({
|
||||||
<div className={classnames(headerClasses)}>
|
<div className={classnames(headerClasses)}>
|
||||||
<div className="mx_GroupView_header_leftCol">
|
<div className="mx_GroupView_header_leftCol">
|
||||||
<div className="mx_GroupView_header_avatar">
|
<div className="mx_GroupView_header_avatar">
|
||||||
{avatarNode}
|
{ avatarNode }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_GroupView_header_info">
|
<div className="mx_GroupView_header_info">
|
||||||
<div className="mx_GroupView_header_name">
|
<div className="mx_GroupView_header_name">
|
||||||
{nameNode}
|
{ nameNode }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_GroupView_header_shortDesc">
|
<div className="mx_GroupView_header_shortDesc">
|
||||||
{shortDescNode}
|
{ shortDescNode }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_GroupView_header_rightCol">
|
<div className="mx_GroupView_header_rightCol">
|
||||||
{rightButtons}
|
{ rightButtons }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{roomBody}
|
<GeminiScrollbar className="mx_GroupView_body">
|
||||||
|
{ bodyNodes }
|
||||||
|
</GeminiScrollbar>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.state.error) {
|
} else if (this.state.error) {
|
||||||
if (this.state.error.httpStatus === 404) {
|
if (this.state.error.httpStatus === 404) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_GroupView_error">
|
<div className="mx_GroupView_error">
|
||||||
Group {this.props.groupId} not found
|
{ _t('Community %(groupId)s not found', {groupId: this.props.groupId}) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let extraText;
|
let extraText;
|
||||||
if (this.state.error.errcode === 'M_UNRECOGNIZED') {
|
if (this.state.error.errcode === 'M_UNRECOGNIZED') {
|
||||||
extraText = <div>{_t('This Home server does not support groups')}</div>;
|
extraText = <div>{ _t('This Home server does not support communities') }</div>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx_GroupView_error">
|
<div className="mx_GroupView_error">
|
||||||
Failed to load {this.props.groupId}
|
{ _t('Failed to load %(groupId)', {groupId: this.props.groupId }) }
|
||||||
{extraText}
|
{ extraText }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ export default React.createClass({
|
||||||
|
|
||||||
const msg = error.message || error.toString();
|
const msg = error.message || error.toString();
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: msg
|
errorText: msg,
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
|
|
||||||
|
@ -207,7 +207,7 @@ export default React.createClass({
|
||||||
if (this.state.errorText) {
|
if (this.state.errorText) {
|
||||||
error = (
|
error = (
|
||||||
<div className="error">
|
<div className="error">
|
||||||
{this.state.errorText}
|
{ this.state.errorText }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -215,8 +215,8 @@ export default React.createClass({
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
{this._renderCurrentStage()}
|
{ this._renderCurrentStage() }
|
||||||
{error}
|
{ error }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2017 New Vector 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.
|
||||||
|
@ -81,10 +82,6 @@ export default React.createClass({
|
||||||
// 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;
|
||||||
|
|
||||||
// _scrollStateMap is a map from room id to the scroll state returned by
|
|
||||||
// RoomView.getScrollState()
|
|
||||||
this._scrollStateMap = {};
|
|
||||||
|
|
||||||
CallMediaHandler.loadDevices();
|
CallMediaHandler.loadDevices();
|
||||||
|
|
||||||
document.addEventListener('keydown', this._onKeyDown);
|
document.addEventListener('keydown', this._onKeyDown);
|
||||||
|
@ -116,10 +113,6 @@ export default React.createClass({
|
||||||
return Boolean(MatrixClientPeg.get());
|
return Boolean(MatrixClientPeg.get());
|
||||||
},
|
},
|
||||||
|
|
||||||
getScrollStateForRoom: function(roomId) {
|
|
||||||
return this._scrollStateMap[roomId];
|
|
||||||
},
|
|
||||||
|
|
||||||
canResetTimelineInRoom: function(roomId) {
|
canResetTimelineInRoom: function(roomId) {
|
||||||
if (!this.refs.roomView) {
|
if (!this.refs.roomView) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -139,6 +132,9 @@ export default React.createClass({
|
||||||
useCompactLayout: event.getContent().useCompactLayout,
|
useCompactLayout: event.getContent().useCompactLayout,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (event.getType() === "m.ignored_user_list") {
|
||||||
|
dis.dispatch({action: "ignore_state_changed"});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onKeyDown: function(ev) {
|
_onKeyDown: function(ev) {
|
||||||
|
@ -169,7 +165,7 @@ export default React.createClass({
|
||||||
case KeyCode.UP:
|
case KeyCode.UP:
|
||||||
case KeyCode.DOWN:
|
case KeyCode.DOWN:
|
||||||
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||||
let action = ev.keyCode == KeyCode.UP ?
|
const 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;
|
||||||
|
@ -211,8 +207,7 @@ export default React.createClass({
|
||||||
_onScrollKeyPressed: function(ev) {
|
_onScrollKeyPressed: function(ev) {
|
||||||
if (this.refs.roomView) {
|
if (this.refs.roomView) {
|
||||||
this.refs.roomView.handleScrollKey(ev);
|
this.refs.roomView.handleScrollKey(ev);
|
||||||
}
|
} else if (this.refs.roomDirectory) {
|
||||||
else if (this.refs.roomDirectory) {
|
|
||||||
this.refs.roomDirectory.handleScrollKey(ev);
|
this.refs.roomDirectory.handleScrollKey(ev);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -246,22 +241,20 @@ export default React.createClass({
|
||||||
eventPixelOffset={this.props.initialEventPixelOffset}
|
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||||
key={this.props.currentRoomId || 'roomview'}
|
key={this.props.currentRoomId || 'roomview'}
|
||||||
opacity={this.props.middleOpacity}
|
opacity={this.props.middleOpacity}
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
collapsedRhs={this.props.collapseRhs}
|
||||||
ConferenceHandler={this.props.ConferenceHandler}
|
ConferenceHandler={this.props.ConferenceHandler}
|
||||||
scrollStateMap={this._scrollStateMap}
|
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
|
if (!this.props.collapseRhs) 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}
|
||||||
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.rightOpacity}/>;
|
if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.MyGroups:
|
case PageTypes.MyGroups:
|
||||||
|
@ -271,9 +264,9 @@ export default React.createClass({
|
||||||
case PageTypes.CreateRoom:
|
case PageTypes.CreateRoom:
|
||||||
page_element = <CreateRoom
|
page_element = <CreateRoom
|
||||||
onRoomCreated={this.props.onRoomCreated}
|
onRoomCreated={this.props.onRoomCreated}
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
collapsedRhs={this.props.collapseRhs}
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.RoomDirectory:
|
case PageTypes.RoomDirectory:
|
||||||
|
@ -306,8 +299,9 @@ export default React.createClass({
|
||||||
case PageTypes.GroupView:
|
case PageTypes.GroupView:
|
||||||
page_element = <GroupView
|
page_element = <GroupView
|
||||||
groupId={this.props.currentGroupId}
|
groupId={this.props.currentGroupId}
|
||||||
|
collapsedRhs={this.props.collapseRhs}
|
||||||
/>;
|
/>;
|
||||||
//right_panel = <RightPanel opacity={this.props.rightOpacity} />;
|
if (!this.props.collapseRhs) right_panel = <RightPanel groupId={this.props.currentGroupId} opacity={this.props.rightOpacity} />;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,7 +319,7 @@ export default React.createClass({
|
||||||
topBar = <MatrixToolbar />;
|
topBar = <MatrixToolbar />;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bodyClasses = 'mx_MatrixChat';
|
let bodyClasses = 'mx_MatrixChat';
|
||||||
if (topBar) {
|
if (topBar) {
|
||||||
bodyClasses += ' mx_MatrixChat_toolbarShowing';
|
bodyClasses += ' mx_MatrixChat_toolbarShowing';
|
||||||
}
|
}
|
||||||
|
@ -335,17 +329,17 @@ export default React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mx_MatrixChat_wrapper'>
|
<div className='mx_MatrixChat_wrapper'>
|
||||||
{topBar}
|
{ topBar }
|
||||||
<div className={bodyClasses}>
|
<div className={bodyClasses}>
|
||||||
<LeftPanel
|
<LeftPanel
|
||||||
selectedRoom={this.props.currentRoomId}
|
selectedRoom={this.props.currentRoomId}
|
||||||
collapsed={this.props.collapse_lhs || false}
|
collapsed={this.props.collapseLhs || false}
|
||||||
opacity={this.props.leftOpacity}
|
opacity={this.props.leftOpacity}
|
||||||
/>
|
/>
|
||||||
<main className='mx_MatrixChat_middlePanel'>
|
<main className='mx_MatrixChat_middlePanel'>
|
||||||
{page_element}
|
{ page_element }
|
||||||
</main>
|
</main>
|
||||||
{right_panel}
|
{ right_panel }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -32,13 +32,12 @@ import dis from "../../dispatcher";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import Tinter from "../../Tinter";
|
import Tinter from "../../Tinter";
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../Invite';
|
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite';
|
||||||
import * as Rooms from '../../Rooms';
|
import * as Rooms from '../../Rooms';
|
||||||
import linkifyMatrix from "../../linkify-matrix";
|
import linkifyMatrix from "../../linkify-matrix";
|
||||||
import * as Lifecycle from '../../Lifecycle';
|
import * as Lifecycle from '../../Lifecycle';
|
||||||
// LifecycleStore is not used but does listen to and dispatch actions
|
// LifecycleStore is not used but does listen to and dispatch actions
|
||||||
require('../../stores/LifecycleStore');
|
require('../../stores/LifecycleStore');
|
||||||
import RoomViewStore from '../../stores/RoomViewStore';
|
|
||||||
import PageTypes from '../../PageTypes';
|
import PageTypes from '../../PageTypes';
|
||||||
|
|
||||||
import createRoom from "../../createRoom";
|
import createRoom from "../../createRoom";
|
||||||
|
@ -144,8 +143,8 @@ module.exports = React.createClass({
|
||||||
// If we're trying to just view a user ID (i.e. /user URL), this is it
|
// If we're trying to just view a user ID (i.e. /user URL), this is it
|
||||||
viewUserId: null,
|
viewUserId: null,
|
||||||
|
|
||||||
collapse_lhs: false,
|
collapseLhs: false,
|
||||||
collapse_rhs: false,
|
collapseRhs: false,
|
||||||
leftOpacity: 1.0,
|
leftOpacity: 1.0,
|
||||||
middleOpacity: 1.0,
|
middleOpacity: 1.0,
|
||||||
rightOpacity: 1.0,
|
rightOpacity: 1.0,
|
||||||
|
@ -214,9 +213,6 @@ module.exports = React.createClass({
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
SdkConfig.put(this.props.config);
|
SdkConfig.put(this.props.config);
|
||||||
|
|
||||||
this._roomViewStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdated);
|
|
||||||
this._onRoomViewStoreUpdated();
|
|
||||||
|
|
||||||
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
|
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
|
||||||
|
|
||||||
// Used by _viewRoom before getting state from sync
|
// Used by _viewRoom before getting state from sync
|
||||||
|
@ -353,7 +349,6 @@ module.exports = React.createClass({
|
||||||
UDEHandler.stopListening();
|
UDEHandler.stopListening();
|
||||||
window.removeEventListener("focus", this.onFocus);
|
window.removeEventListener("focus", this.onFocus);
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
this._roomViewStoreToken.remove();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function() {
|
||||||
|
@ -439,7 +434,7 @@ module.exports = React.createClass({
|
||||||
break;
|
break;
|
||||||
case 'view_user':
|
case 'view_user':
|
||||||
// FIXME: ugly hack to expand the RightPanel and then re-dispatch.
|
// FIXME: ugly hack to expand the RightPanel and then re-dispatch.
|
||||||
if (this.state.collapse_rhs) {
|
if (this.state.collapseRhs) {
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'show_right_panel',
|
action: 'show_right_panel',
|
||||||
|
@ -521,22 +516,22 @@ module.exports = React.createClass({
|
||||||
break;
|
break;
|
||||||
case 'hide_left_panel':
|
case 'hide_left_panel':
|
||||||
this.setState({
|
this.setState({
|
||||||
collapse_lhs: true,
|
collapseLhs: true,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'show_left_panel':
|
case 'show_left_panel':
|
||||||
this.setState({
|
this.setState({
|
||||||
collapse_lhs: false,
|
collapseLhs: false,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'hide_right_panel':
|
case 'hide_right_panel':
|
||||||
this.setState({
|
this.setState({
|
||||||
collapse_rhs: true,
|
collapseRhs: true,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'show_right_panel':
|
case 'show_right_panel':
|
||||||
this.setState({
|
this.setState({
|
||||||
collapse_rhs: false,
|
collapseRhs: false,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'ui_opacity': {
|
case 'ui_opacity': {
|
||||||
|
@ -587,10 +582,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onRoomViewStoreUpdated: function() {
|
|
||||||
this.setState({ currentRoomId: RoomViewStore.getRoomId() });
|
|
||||||
},
|
|
||||||
|
|
||||||
_setPage: function(pageType) {
|
_setPage: function(pageType) {
|
||||||
this.setState({
|
this.setState({
|
||||||
page_type: pageType,
|
page_type: pageType,
|
||||||
|
@ -677,10 +668,10 @@ module.exports = React.createClass({
|
||||||
this.focusComposer = true;
|
this.focusComposer = true;
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
|
currentRoomId: roomInfo.room_id || null,
|
||||||
page_type: PageTypes.RoomView,
|
page_type: PageTypes.RoomView,
|
||||||
thirdPartyInvite: roomInfo.third_party_invite,
|
thirdPartyInvite: roomInfo.third_party_invite,
|
||||||
roomOobData: roomInfo.oob_data,
|
roomOobData: roomInfo.oob_data,
|
||||||
autoJoin: roomInfo.auto_join,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (roomInfo.room_alias) {
|
if (roomInfo.room_alias) {
|
||||||
|
@ -782,15 +773,13 @@ module.exports = React.createClass({
|
||||||
dis.dispatch({action: 'view_set_mxid'});
|
dis.dispatch({action: 'view_set_mxid'});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
||||||
Modal.createTrackedDialog('Create Room', '', TextInputDialog, {
|
Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
|
||||||
title: _t('Create Room'),
|
onFinished: (shouldCreate, name, noFederate) => {
|
||||||
description: _t('Room name (optional)'),
|
|
||||||
button: _t('Create Room'),
|
|
||||||
onFinished: (shouldCreate, name) => {
|
|
||||||
if (shouldCreate) {
|
if (shouldCreate) {
|
||||||
const createOpts = {};
|
const createOpts = {};
|
||||||
if (name) createOpts.name = name;
|
if (name) createOpts.name = name;
|
||||||
|
if (noFederate) createOpts.creation_content = {'m.federate': false};
|
||||||
createRoom({createOpts}).done();
|
createRoom({createOpts}).done();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -862,7 +851,7 @@ module.exports = React.createClass({
|
||||||
title: _t("Leave room"),
|
title: _t("Leave room"),
|
||||||
description: (
|
description: (
|
||||||
<span>
|
<span>
|
||||||
{_t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name})}
|
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
onFinished: (shouldLeave) => {
|
onFinished: (shouldLeave) => {
|
||||||
|
@ -1002,8 +991,8 @@ module.exports = React.createClass({
|
||||||
this.setStateForNewView({
|
this.setStateForNewView({
|
||||||
view: VIEWS.LOGIN,
|
view: VIEWS.LOGIN,
|
||||||
ready: false,
|
ready: false,
|
||||||
collapse_lhs: false,
|
collapseLhs: false,
|
||||||
collapse_rhs: false,
|
collapseRhs: false,
|
||||||
currentRoomId: null,
|
currentRoomId: null,
|
||||||
page_type: PageTypes.RoomDirectory,
|
page_type: PageTypes.RoomDirectory,
|
||||||
});
|
});
|
||||||
|
@ -1459,7 +1448,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_MatrixChat_splash">
|
<div className="mx_MatrixChat_splash">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
|
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
|
||||||
{ _t('Logout') }
|
{ _t('Logout') }
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -65,7 +65,7 @@ module.exports = React.createClass({
|
||||||
suppressFirstDateSeparator: React.PropTypes.bool,
|
suppressFirstDateSeparator: React.PropTypes.bool,
|
||||||
|
|
||||||
// whether to show read receipts
|
// whether to show read receipts
|
||||||
manageReadReceipts: React.PropTypes.bool,
|
showReadReceipts: React.PropTypes.bool,
|
||||||
|
|
||||||
// true if updates to the event list should cause the scroll panel to
|
// true if updates to the event list should cause the scroll panel to
|
||||||
// scroll down when we are at the bottom of the window. See ScrollPanel
|
// scroll down when we are at the bottom of the window. See ScrollPanel
|
||||||
|
@ -154,15 +154,15 @@ module.exports = React.createClass({
|
||||||
// 0: read marker is within the window
|
// 0: read marker is within the window
|
||||||
// +1: read marker is below the window
|
// +1: read marker is below the window
|
||||||
getReadMarkerPosition: function() {
|
getReadMarkerPosition: function() {
|
||||||
var readMarker = this.refs.readMarkerNode;
|
const readMarker = this.refs.readMarkerNode;
|
||||||
var messageWrapper = this.refs.scrollPanel;
|
const messageWrapper = this.refs.scrollPanel;
|
||||||
|
|
||||||
if (!readMarker || !messageWrapper) {
|
if (!readMarker || !messageWrapper) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
|
const wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
|
||||||
var readMarkerRect = readMarker.getBoundingClientRect();
|
const readMarkerRect = readMarker.getBoundingClientRect();
|
||||||
|
|
||||||
// the read-marker pretends to have zero height when it is actually
|
// the read-marker pretends to have zero height when it is actually
|
||||||
// two pixels high; +2 here to account for that.
|
// two pixels high; +2 here to account for that.
|
||||||
|
@ -241,6 +241,10 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// TODO: Implement granular (per-room) hide options
|
// TODO: Implement granular (per-room) hide options
|
||||||
_shouldShowEvent: function(mxEv) {
|
_shouldShowEvent: function(mxEv) {
|
||||||
|
if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
|
||||||
|
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
||||||
|
}
|
||||||
|
|
||||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
return false; // no tile = no show
|
return false; // no tile = no show
|
||||||
|
@ -258,7 +262,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
this.eventNodes = {};
|
this.eventNodes = {};
|
||||||
|
|
||||||
var i;
|
let i;
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -268,9 +272,9 @@ module.exports = React.createClass({
|
||||||
// a local echo, to manage the read-marker.
|
// a local echo, to manage the read-marker.
|
||||||
let lastShownEvent;
|
let lastShownEvent;
|
||||||
|
|
||||||
var lastShownNonLocalEchoIndex = -1;
|
let 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];
|
const mxEv = this.props.events[i];
|
||||||
if (!this._shouldShowEvent(mxEv)) {
|
if (!this._shouldShowEvent(mxEv)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -288,12 +292,12 @@ module.exports = React.createClass({
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ret = [];
|
const ret = [];
|
||||||
|
|
||||||
var prevEvent = null; // the last event we showed
|
let prevEvent = null; // the last event we showed
|
||||||
|
|
||||||
// assume there is no read marker until proven otherwise
|
// assume there is no read marker until proven otherwise
|
||||||
var readMarkerVisible = false;
|
let readMarkerVisible = false;
|
||||||
|
|
||||||
// if the readmarker has moved, cancel any active ghost.
|
// if the readmarker has moved, cancel any active ghost.
|
||||||
if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
|
if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
|
||||||
|
@ -305,16 +309,16 @@ module.exports = React.createClass({
|
||||||
const isMembershipChange = (e) => e.getType() === 'm.room.member';
|
const isMembershipChange = (e) => e.getType() === 'm.room.member';
|
||||||
|
|
||||||
for (i = 0; i < this.props.events.length; i++) {
|
for (i = 0; i < this.props.events.length; i++) {
|
||||||
let mxEv = this.props.events[i];
|
const mxEv = this.props.events[i];
|
||||||
let eventId = mxEv.getId();
|
const eventId = mxEv.getId();
|
||||||
let last = (mxEv === lastShownEvent);
|
const last = (mxEv === lastShownEvent);
|
||||||
|
|
||||||
const wantTile = this._shouldShowEvent(mxEv);
|
const wantTile = this._shouldShowEvent(mxEv);
|
||||||
|
|
||||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||||
if (isMembershipChange(mxEv) && wantTile) {
|
if (isMembershipChange(mxEv) && wantTile) {
|
||||||
let readMarkerInMels = false;
|
let readMarkerInMels = false;
|
||||||
let ts1 = mxEv.getTs();
|
const 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
|
||||||
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
|
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
|
||||||
|
@ -326,7 +330,7 @@ module.exports = React.createClass({
|
||||||
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
|
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
|
||||||
|
|
||||||
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
||||||
let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
|
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>;
|
||||||
ret.push(dateSeparator);
|
ret.push(dateSeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,7 +339,7 @@ module.exports = React.createClass({
|
||||||
readMarkerInMels = true;
|
readMarkerInMels = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let summarisedEvents = [mxEv];
|
const summarisedEvents = [mxEv];
|
||||||
for (;i + 1 < this.props.events.length; i++) {
|
for (;i + 1 < this.props.events.length; i++) {
|
||||||
const collapsedMxEv = this.props.events[i + 1];
|
const collapsedMxEv = this.props.events[i + 1];
|
||||||
|
|
||||||
|
@ -361,8 +365,13 @@ module.exports = React.createClass({
|
||||||
summarisedEvents.push(collapsedMxEv);
|
summarisedEvents.push(collapsedMxEv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let highlightInMels = false;
|
||||||
|
|
||||||
// At this point, i = the index of the last event in the summary sequence
|
// At this point, i = the index of the last event in the summary sequence
|
||||||
let eventTiles = summarisedEvents.map((e) => {
|
let eventTiles = summarisedEvents.map((e) => {
|
||||||
|
if (e.getId() === this.props.highlightedEventId) {
|
||||||
|
highlightInMels = true;
|
||||||
|
}
|
||||||
// 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
|
||||||
|
@ -376,15 +385,13 @@ module.exports = React.createClass({
|
||||||
eventTiles = null;
|
eventTiles = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.push(
|
ret.push(<MemberEventListSummary key={key}
|
||||||
<MemberEventListSummary
|
events={summarisedEvents}
|
||||||
key={key}
|
onToggle={this._onWidgetLoad} // Update scroll state
|
||||||
events={summarisedEvents}
|
startExpanded={highlightInMels}
|
||||||
onToggle={this._onWidgetLoad} // Update scroll state
|
>
|
||||||
>
|
{ eventTiles }
|
||||||
{eventTiles}
|
</MemberEventListSummary>);
|
||||||
</MemberEventListSummary>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (readMarkerInMels) {
|
if (readMarkerInMels) {
|
||||||
ret.push(this._getReadMarkerTile(visible));
|
ret.push(this._getReadMarkerTile(visible));
|
||||||
|
@ -401,7 +408,7 @@ module.exports = React.createClass({
|
||||||
prevEvent = mxEv;
|
prevEvent = mxEv;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isVisibleReadMarker = false;
|
let isVisibleReadMarker = false;
|
||||||
|
|
||||||
if (eventId == this.props.readMarkerEventId) {
|
if (eventId == this.props.readMarkerEventId) {
|
||||||
var visible = this.props.readMarkerVisible;
|
var visible = this.props.readMarkerVisible;
|
||||||
|
@ -441,10 +448,10 @@ module.exports = React.createClass({
|
||||||
_getTilesForEvent: function(prevEvent, mxEv, last) {
|
_getTilesForEvent: function(prevEvent, mxEv, last) {
|
||||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
var ret = [];
|
const ret = [];
|
||||||
|
|
||||||
// is this a continuation of the previous message?
|
// is this a continuation of the previous message?
|
||||||
var continuation = false;
|
let continuation = false;
|
||||||
|
|
||||||
if (prevEvent !== null
|
if (prevEvent !== null
|
||||||
&& prevEvent.sender && mxEv.sender
|
&& prevEvent.sender && mxEv.sender
|
||||||
|
@ -469,8 +476,8 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// local echoes have a fake date, which could even be yesterday. Treat them
|
// local echoes have a fake date, which could even be yesterday. Treat them
|
||||||
// as 'today' for the date separators.
|
// as 'today' for the date separators.
|
||||||
var ts1 = mxEv.getTs();
|
let ts1 = mxEv.getTs();
|
||||||
var eventDate = mxEv.getDate();
|
let eventDate = mxEv.getDate();
|
||||||
if (mxEv.status) {
|
if (mxEv.status) {
|
||||||
eventDate = new Date();
|
eventDate = new Date();
|
||||||
ts1 = eventDate.getTime();
|
ts1 = eventDate.getTime();
|
||||||
|
@ -478,20 +485,20 @@ 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} showTwelveHour={this.props.isTwelveHour}/></li>;
|
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>;
|
||||||
ret.push(dateSeparator);
|
ret.push(dateSeparator);
|
||||||
continuation = false;
|
continuation = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var eventId = mxEv.getId();
|
const eventId = mxEv.getId();
|
||||||
var highlight = (eventId == this.props.highlightedEventId);
|
const highlight = (eventId == this.props.highlightedEventId);
|
||||||
|
|
||||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
// we can't use local echoes as scroll tokens, because their event IDs change.
|
||||||
// Local echos have a send "status".
|
// Local echos have a send "status".
|
||||||
var scrollToken = mxEv.status ? undefined : eventId;
|
const scrollToken = mxEv.status ? undefined : eventId;
|
||||||
|
|
||||||
var readReceipts;
|
let readReceipts;
|
||||||
if (this.props.manageReadReceipts) {
|
if (this.props.showReadReceipts) {
|
||||||
readReceipts = this._getReadReceiptsForEvent(mxEv);
|
readReceipts = this._getReadReceiptsForEvent(mxEv);
|
||||||
}
|
}
|
||||||
ret.push(
|
ret.push(
|
||||||
|
@ -508,8 +515,8 @@ module.exports = React.createClass({
|
||||||
eventSendStatus={mxEv.status}
|
eventSendStatus={mxEv.status}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
isTwelveHour={this.props.isTwelveHour}
|
isTwelveHour={this.props.isTwelveHour}
|
||||||
last={last} isSelectedEvent={highlight}/>
|
last={last} isSelectedEvent={highlight} />
|
||||||
</li>
|
</li>,
|
||||||
);
|
);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -544,12 +551,15 @@ module.exports = React.createClass({
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let receipts = [];
|
const receipts = [];
|
||||||
room.getReceiptsForEvent(event).forEach((r) => {
|
room.getReceiptsForEvent(event).forEach((r) => {
|
||||||
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
|
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
|
||||||
return; // ignore non-read receipts and receipts from self.
|
return; // ignore non-read receipts and receipts from self.
|
||||||
}
|
}
|
||||||
let member = room.getMember(r.userId);
|
if (MatrixClientPeg.get().isUserIgnored(r.userId)) {
|
||||||
|
return; // ignore ignored users
|
||||||
|
}
|
||||||
|
const member = room.getMember(r.userId);
|
||||||
if (!member) {
|
if (!member) {
|
||||||
return; // ignore unknown user IDs
|
return; // ignore unknown user IDs
|
||||||
}
|
}
|
||||||
|
@ -565,7 +575,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_getReadMarkerTile: function(visible) {
|
_getReadMarkerTile: function(visible) {
|
||||||
var hr;
|
let hr;
|
||||||
if (visible) {
|
if (visible) {
|
||||||
hr = <hr className="mx_RoomView_myReadMarker"
|
hr = <hr className="mx_RoomView_myReadMarker"
|
||||||
style={{opacity: 1, width: '99%'}}
|
style={{opacity: 1, width: '99%'}}
|
||||||
|
@ -575,7 +585,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<li key="_readupto" ref="readMarkerNode"
|
<li key="_readupto" ref="readMarkerNode"
|
||||||
className="mx_RoomView_myReadMarker_container">
|
className="mx_RoomView_myReadMarker_container">
|
||||||
{hr}
|
{ hr }
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -594,7 +604,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_getReadMarkerGhostTile: function() {
|
_getReadMarkerGhostTile: function() {
|
||||||
var hr = <hr className="mx_RoomView_myReadMarker"
|
const hr = <hr className="mx_RoomView_myReadMarker"
|
||||||
style={{opacity: 1, width: '99%'}}
|
style={{opacity: 1, width: '99%'}}
|
||||||
ref={this._startAnimation}
|
ref={this._startAnimation}
|
||||||
/>;
|
/>;
|
||||||
|
@ -605,7 +615,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<li key={"_readuptoghost_"+this.currentGhostEventId}
|
<li key={"_readuptoghost_"+this.currentGhostEventId}
|
||||||
className="mx_RoomView_myReadMarker_container">
|
className="mx_RoomView_myReadMarker_container">
|
||||||
{hr}
|
{ hr }
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -617,7 +627,7 @@ module.exports = React.createClass({
|
||||||
// once dynamic content in the events load, make the scrollPanel check the
|
// once dynamic content in the events load, make the scrollPanel check the
|
||||||
// scroll offsets.
|
// scroll offsets.
|
||||||
_onWidgetLoad: function() {
|
_onWidgetLoad: function() {
|
||||||
var scrollPanel = this.refs.scrollPanel;
|
const scrollPanel = this.refs.scrollPanel;
|
||||||
if (scrollPanel) {
|
if (scrollPanel) {
|
||||||
scrollPanel.forceUpdate();
|
scrollPanel.forceUpdate();
|
||||||
}
|
}
|
||||||
|
@ -628,9 +638,9 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||||
var Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
var topSpinner, bottomSpinner;
|
let topSpinner, bottomSpinner;
|
||||||
if (this.props.backPaginating) {
|
if (this.props.backPaginating) {
|
||||||
topSpinner = <li key="_topSpinner"><Spinner /></li>;
|
topSpinner = <li key="_topSpinner"><Spinner /></li>;
|
||||||
}
|
}
|
||||||
|
@ -638,25 +648,25 @@ module.exports = React.createClass({
|
||||||
bottomSpinner = <li key="_bottomSpinner"><Spinner /></li>;
|
bottomSpinner = <li key="_bottomSpinner"><Spinner /></li>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var style = this.props.hidden ? { display: 'none' } : {};
|
const style = this.props.hidden ? { display: 'none' } : {};
|
||||||
style.opacity = this.props.opacity;
|
style.opacity = this.props.opacity;
|
||||||
|
|
||||||
var className = this.props.className + " mx_fadable";
|
let className = this.props.className + " mx_fadable";
|
||||||
if (this.props.alwaysShowTimestamps) {
|
if (this.props.alwaysShowTimestamps) {
|
||||||
className += " mx_MessagePanel_alwaysShowTimestamps";
|
className += " mx_MessagePanel_alwaysShowTimestamps";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollPanel ref="scrollPanel" className={ className }
|
<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}
|
||||||
onUnfillRequest={ this.props.onUnfillRequest }
|
onUnfillRequest={this.props.onUnfillRequest}
|
||||||
style={ style }
|
style={style}
|
||||||
stickyBottom={ this.props.stickyBottom }>
|
stickyBottom={this.props.stickyBottom}>
|
||||||
{topSpinner}
|
{ topSpinner }
|
||||||
{this._getEventTiles()}
|
{ this._getEventTiles() }
|
||||||
{bottomSpinner}
|
{ bottomSpinner }
|
||||||
</ScrollPanel>
|
</ScrollPanel>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -39,7 +39,7 @@ const GroupTile = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
return <a onClick={this.onClick} href="#">{this.props.groupId}</a>;
|
return <a onClick={this.onClick} href="#">{ this.props.groupId }</a>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ export default withMatrixClient(React.createClass({
|
||||||
|
|
||||||
_onCreateGroupClick: function() {
|
_onCreateGroupClick: function() {
|
||||||
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
|
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
|
||||||
Modal.createTrackedDialog('Create Group', '', CreateGroupDialog);
|
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
|
||||||
},
|
},
|
||||||
|
|
||||||
_fetch: function() {
|
_fetch: function() {
|
||||||
|
@ -90,51 +90,51 @@ export default withMatrixClient(React.createClass({
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
content = <div>
|
content = <div>
|
||||||
<div>{_t('You are a member of these groups:')}</div>
|
<div>{ _t('You are a member of these communities:') }</div>
|
||||||
{groupNodes}
|
{ groupNodes }
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.state.error) {
|
} else if (this.state.error) {
|
||||||
content = <div className="mx_MyGroups_error">
|
content = <div className="mx_MyGroups_error">
|
||||||
{_t('Error whilst fetching joined groups')}
|
{ _t('Error whilst fetching joined communities') }
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
content = <Loader />;
|
content = <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_MyGroups">
|
return <div className="mx_MyGroups">
|
||||||
<SimpleRoomHeader title={ _t("Groups") } />
|
<SimpleRoomHeader title={_t("Communities")} icon="img/icons-groups.svg" />
|
||||||
<div className='mx_MyGroups_joinCreateBox'>
|
<div className='mx_MyGroups_joinCreateBox'>
|
||||||
<div className="mx_MyGroups_createBox">
|
<div className="mx_MyGroups_createBox">
|
||||||
<div className="mx_MyGroups_joinCreateHeader">
|
<div className="mx_MyGroups_joinCreateHeader">
|
||||||
{_t('Create a new group')}
|
{ _t('Create a new community') }
|
||||||
</div>
|
</div>
|
||||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onCreateGroupClick}>
|
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onCreateGroupClick}>
|
||||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{_t(
|
{ _t(
|
||||||
'Create a group to represent your community! '+
|
'Create a community to represent your community! '+
|
||||||
'Define a set of rooms and your own custom homepage '+
|
'Define a set of rooms and your own custom homepage '+
|
||||||
'to mark out your space in the Matrix universe.',
|
'to mark out your space in the Matrix universe.',
|
||||||
)}
|
) }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MyGroups_joinBox">
|
<div className="mx_MyGroups_joinBox">
|
||||||
<div className="mx_MyGroups_joinCreateHeader">
|
<div className="mx_MyGroups_joinCreateHeader">
|
||||||
{_t('Join an existing group')}
|
{ _t('Join an existing community') }
|
||||||
</div>
|
</div>
|
||||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onJoinGroupClick}>
|
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onJoinGroupClick}>
|
||||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{_tJsx(
|
{ _tJsx(
|
||||||
'To join an exisitng group you\'ll have to '+
|
'To join an existing community you\'ll have to '+
|
||||||
'know its group identifier; this will look '+
|
'know its community identifier; this will look '+
|
||||||
'something like <i>+example:matrix.org</i>.',
|
'something like <i>+example:matrix.org</i>.',
|
||||||
/<i>(.*)<\/i>/,
|
/<i>(.*)<\/i>/,
|
||||||
(sub) => <i>{sub}</i>,
|
(sub) => <i>{ sub }</i>,
|
||||||
)}
|
) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MyGroups_content">
|
<div className="mx_MyGroups_content">
|
||||||
{content}
|
{ content }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require('react');
|
const React = require('react');
|
||||||
var ReactDOM = require("react-dom");
|
const ReactDOM = require("react-dom");
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
var Matrix = require("matrix-js-sdk");
|
const Matrix = require("matrix-js-sdk");
|
||||||
var sdk = require('../../index');
|
const sdk = require('../../index');
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
var dis = require("../../dispatcher");
|
const dis = require("../../dispatcher");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Component which shows the global notification list using a TimelinePanel
|
* Component which shows the global notification list using a TimelinePanel
|
||||||
*/
|
*/
|
||||||
var NotificationPanel = React.createClass({
|
const NotificationPanel = React.createClass({
|
||||||
displayName: 'NotificationPanel',
|
displayName: 'NotificationPanel',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -33,10 +33,10 @@ var NotificationPanel = React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
// 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");
|
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
var timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||||
if (timelineSet) {
|
if (timelineSet) {
|
||||||
return (
|
return (
|
||||||
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
|
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
|
||||||
|
@ -44,18 +44,17 @@ var NotificationPanel = React.createClass({
|
||||||
manageReadReceipts={false}
|
manageReadReceipts={false}
|
||||||
manageReadMarkers={false}
|
manageReadMarkers={false}
|
||||||
timelineSet={timelineSet}
|
timelineSet={timelineSet}
|
||||||
showUrlPreview = { false }
|
showUrlPreview = {false}
|
||||||
opacity={ this.props.opacity }
|
opacity={this.props.opacity}
|
||||||
tileShape="notif"
|
tileShape="notif"
|
||||||
empty={ _t('You have no visible notifications') }
|
empty={_t('You have no visible notifications')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
console.error("No notifTimelineSet available!");
|
console.error("No notifTimelineSet available!");
|
||||||
return (
|
return (
|
||||||
<div className="mx_NotificationPanel">
|
<div className="mx_NotificationPanel">
|
||||||
<Loader/>
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,10 @@ module.exports = React.createClass({
|
||||||
// the end of the live timeline.
|
// the end of the live timeline.
|
||||||
atEndOfLiveTimeline: React.PropTypes.bool,
|
atEndOfLiveTimeline: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// This is true when the user is alone in the room, but has also sent a message.
|
||||||
|
// Used to suggest to the user to invite someone
|
||||||
|
sentMessageAndIsAlone: React.PropTypes.bool,
|
||||||
|
|
||||||
// true if there is an active call in this room (means we show
|
// true if there is an active call in this room (means we show
|
||||||
// the 'Active Call' text in the status bar if there is nothing
|
// the 'Active Call' text in the status bar if there is nothing
|
||||||
// more interesting)
|
// more interesting)
|
||||||
|
@ -60,6 +64,14 @@ module.exports = React.createClass({
|
||||||
// 'unsent messages' bar
|
// 'unsent messages' bar
|
||||||
onCancelAllClick: React.PropTypes.func,
|
onCancelAllClick: React.PropTypes.func,
|
||||||
|
|
||||||
|
// callback for when the user clicks on the 'invite others' button in the
|
||||||
|
// 'you are alone' bar
|
||||||
|
onInviteClick: React.PropTypes.func,
|
||||||
|
|
||||||
|
// callback for when the user clicks on the 'stop warning me' button in the
|
||||||
|
// 'you are alone' bar
|
||||||
|
onStopWarningClick: React.PropTypes.func,
|
||||||
|
|
||||||
// callback for when the user clicks on the 'scroll to bottom' button
|
// callback for when the user clicks on the 'scroll to bottom' button
|
||||||
onScrollToBottomClick: React.PropTypes.func,
|
onScrollToBottomClick: React.PropTypes.func,
|
||||||
|
|
||||||
|
@ -103,7 +115,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
||||||
var client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (client) {
|
if (client) {
|
||||||
client.removeListener("sync", this.onSyncStateChange);
|
client.removeListener("sync", this.onSyncStateChange);
|
||||||
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
||||||
|
@ -115,18 +127,18 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
syncState: state
|
syncState: state,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomMemberTyping: function(ev, member) {
|
onRoomMemberTyping: function(ev, member) {
|
||||||
this.setState({
|
this.setState({
|
||||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
usersTyping: WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check whether current size is greater than 0, if yes call props.onVisible
|
// Check whether current size is greater than 0, if yes call props.onVisible
|
||||||
_checkSize: function () {
|
_checkSize: function() {
|
||||||
if (this.props.onVisible && this._getSize()) {
|
if (this.props.onVisible && this._getSize()) {
|
||||||
this.props.onVisible();
|
this.props.onVisible();
|
||||||
}
|
}
|
||||||
|
@ -140,7 +152,8 @@ 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.sentMessageAndIsAlone
|
||||||
) {
|
) {
|
||||||
return STATUS_BAR_EXPANDED;
|
return STATUS_BAR_EXPANDED;
|
||||||
} else if (this.props.unsentMessageError) {
|
} else if (this.props.unsentMessageError) {
|
||||||
|
@ -157,9 +170,9 @@ module.exports = React.createClass({
|
||||||
if (this.props.numUnreadMessages) {
|
if (this.props.numUnreadMessages) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_scrollDownIndicator"
|
<div className="mx_RoomStatusBar_scrollDownIndicator"
|
||||||
onClick={ this.props.onScrollToBottomClick }>
|
onClick={this.props.onScrollToBottomClick}>
|
||||||
<img src="img/newmessages.svg" width="24" height="24"
|
<img src="img/newmessages.svg" width="24" height="24"
|
||||||
alt=""/>
|
alt="" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -167,18 +180,18 @@ module.exports = React.createClass({
|
||||||
if (!this.props.atEndOfLiveTimeline) {
|
if (!this.props.atEndOfLiveTimeline) {
|
||||||
return (
|
return (
|
||||||
<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={ _t("Scroll to bottom of page") }
|
alt={_t("Scroll to bottom of page")}
|
||||||
title={ _t("Scroll to bottom of page") }/>
|
title={_t("Scroll to bottom of page")} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.hasActiveCall) {
|
if (this.props.hasActiveCall) {
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
return (
|
return (
|
||||||
<TintableSvg src="img/sound-indicator.svg" width="23" height="20"/>
|
<TintableSvg src="img/sound-indicator.svg" width="23" height="20" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +202,7 @@ module.exports = React.createClass({
|
||||||
if (wantPlaceholder) {
|
if (wantPlaceholder) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_typingIndicatorAvatars">
|
<div className="mx_RoomStatusBar_typingIndicatorAvatars">
|
||||||
{this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit)}
|
{ this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -221,8 +234,8 @@ module.exports = React.createClass({
|
||||||
if (othersCount > 0) {
|
if (othersCount > 0) {
|
||||||
avatars.push(
|
avatars.push(
|
||||||
<span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others">
|
<span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others">
|
||||||
+{othersCount}
|
+{ othersCount }
|
||||||
</span>
|
</span>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,12 +253,12 @@ module.exports = React.createClass({
|
||||||
if (this.state.syncState === "ERROR") {
|
if (this.state.syncState === "ERROR") {
|
||||||
return (
|
return (
|
||||||
<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">
|
||||||
{_t('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">
|
||||||
{_t('Sent messages will be stored until your connection has returned.')}
|
{ _t('Sent messages will be stored until your connection has returned.') }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -254,18 +267,18 @@ module.exports = React.createClass({
|
||||||
if (this.props.unsentMessageError) {
|
if (this.props.unsentMessageError) {
|
||||||
return (
|
return (
|
||||||
<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">
|
||||||
{ this.props.unsentMessageError }
|
{ this.props.unsentMessageError }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||||
{_tJsx("<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
|
{ _tJsx("<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
|
||||||
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
|
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
|
||||||
[
|
[
|
||||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={ this.props.onResendAllClick }>{sub}</a>,
|
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this.props.onResendAllClick}>{ sub }</a>,
|
||||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={ this.props.onCancelAllClick }>{sub}</a>,
|
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this.props.onCancelAllClick}>{ sub }</a>,
|
||||||
]
|
],
|
||||||
)}
|
) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -275,24 +288,24 @@ module.exports = React.createClass({
|
||||||
// set when you've scrolled up
|
// set when you've scrolled up
|
||||||
if (this.props.numUnreadMessages) {
|
if (this.props.numUnreadMessages) {
|
||||||
// MUST use var name "count" for pluralization to kick in
|
// MUST use var name "count" for pluralization to kick in
|
||||||
var unreadMsgs = _t("%(count)s new messages", {count: this.props.numUnreadMessages});
|
const unreadMsgs = _t("%(count)s new messages", {count: this.props.numUnreadMessages});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_unreadMessagesBar"
|
<div className="mx_RoomStatusBar_unreadMessagesBar"
|
||||||
onClick={ this.props.onScrollToBottomClick }>
|
onClick={this.props.onScrollToBottomClick}>
|
||||||
{unreadMsgs}
|
{ unreadMsgs }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const typingString = WhoIsTyping.whoIsTypingString(
|
const typingString = WhoIsTyping.whoIsTypingString(
|
||||||
this.state.usersTyping,
|
this.state.usersTyping,
|
||||||
this.props.whoIsTypingLimit
|
this.props.whoIsTypingLimit,
|
||||||
);
|
);
|
||||||
if (typingString) {
|
if (typingString) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_typingBar">
|
<div className="mx_RoomStatusBar_typingBar">
|
||||||
<EmojiText>{typingString}</EmojiText>
|
<EmojiText>{ typingString }</EmojiText>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -300,7 +313,22 @@ 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>{_t('Active call')}</b>
|
<b>{ _t('Active call') }</b>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you're alone in the room, and have sent a message, suggest to invite someone
|
||||||
|
if (this.props.sentMessageAndIsAlone) {
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomStatusBar_isAlone">
|
||||||
|
{ _tJsx("There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?",
|
||||||
|
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
|
||||||
|
[
|
||||||
|
(sub) => <a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
|
||||||
|
(sub) => <a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
|
||||||
|
],
|
||||||
|
) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -310,15 +338,15 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var content = this._getContent();
|
const content = this._getContent();
|
||||||
var indicator = this._getIndicator(this.state.usersTyping.length > 0);
|
const indicator = this._getIndicator(this.state.usersTyping.length > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar">
|
<div className="mx_RoomStatusBar">
|
||||||
<div className="mx_RoomStatusBar_indicator">
|
<div className="mx_RoomStatusBar_indicator">
|
||||||
{indicator}
|
{ indicator }
|
||||||
</div>
|
</div>
|
||||||
{content}
|
{ content }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
const React = require("react");
|
||||||
var ReactDOM = require("react-dom");
|
const ReactDOM = require("react-dom");
|
||||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
const GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
var KeyCode = require('../../KeyCode');
|
const KeyCode = require('../../KeyCode');
|
||||||
|
|
||||||
var DEBUG_SCROLL = false;
|
const DEBUG_SCROLL = false;
|
||||||
// var DEBUG_SCROLL = true;
|
// var DEBUG_SCROLL = true;
|
||||||
|
|
||||||
// The amount of extra scroll distance to allow prior to unfilling.
|
// The amount of extra scroll distance to allow prior to unfilling.
|
||||||
|
@ -148,6 +148,7 @@ module.exports = React.createClass({
|
||||||
onFillRequest: function(backwards) { return Promise.resolve(false); },
|
onFillRequest: function(backwards) { return Promise.resolve(false); },
|
||||||
onUnfillRequest: function(backwards, scrollToken) {},
|
onUnfillRequest: function(backwards, scrollToken) {},
|
||||||
onScroll: function() {},
|
onScroll: function() {},
|
||||||
|
onResize: function() {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -157,7 +158,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.checkFillState();
|
this.checkScroll();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function() {
|
||||||
|
@ -178,7 +179,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onScroll: function(ev) {
|
onScroll: function(ev) {
|
||||||
var sn = this._getScrollNode();
|
const sn = this._getScrollNode();
|
||||||
debuglog("Scroll event: offset now:", sn.scrollTop,
|
debuglog("Scroll event: offset now:", sn.scrollTop,
|
||||||
"_lastSetScroll:", this._lastSetScroll);
|
"_lastSetScroll:", this._lastSetScroll);
|
||||||
|
|
||||||
|
@ -238,7 +239,7 @@ module.exports = React.createClass({
|
||||||
// about whether the the content is scrolled down right now, irrespective of
|
// about whether the the content is scrolled down right now, irrespective of
|
||||||
// whether it will stay that way when the children update.
|
// whether it will stay that way when the children update.
|
||||||
isAtBottom: function() {
|
isAtBottom: function() {
|
||||||
var sn = this._getScrollNode();
|
const sn = this._getScrollNode();
|
||||||
|
|
||||||
// there seems to be some bug with flexbox/gemini/chrome/richvdh's
|
// there seems to be some bug with flexbox/gemini/chrome/richvdh's
|
||||||
// understanding of the box model, wherein the scrollNode ends up 2
|
// understanding of the box model, wherein the scrollNode ends up 2
|
||||||
|
@ -281,7 +282,7 @@ module.exports = React.createClass({
|
||||||
// |#########| |
|
// |#########| |
|
||||||
// `---------' -
|
// `---------' -
|
||||||
_getExcessHeight: function(backwards) {
|
_getExcessHeight: function(backwards) {
|
||||||
var sn = this._getScrollNode();
|
const sn = this._getScrollNode();
|
||||||
if (backwards) {
|
if (backwards) {
|
||||||
return sn.scrollTop - sn.clientHeight - UNPAGINATION_PADDING;
|
return sn.scrollTop - sn.clientHeight - UNPAGINATION_PADDING;
|
||||||
} else {
|
} else {
|
||||||
|
@ -295,7 +296,7 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sn = this._getScrollNode();
|
const sn = this._getScrollNode();
|
||||||
|
|
||||||
// if there is less than a screenful of messages above or below the
|
// if there is less than a screenful of messages above or below the
|
||||||
// viewport, try to get some more messages.
|
// viewport, try to get some more messages.
|
||||||
|
@ -377,7 +378,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// check if there is already a pending fill request. If not, set one off.
|
// check if there is already a pending fill request. If not, set one off.
|
||||||
_maybeFill: function(backwards) {
|
_maybeFill: function(backwards) {
|
||||||
var dir = backwards ? 'b' : 'f';
|
const dir = backwards ? 'b' : 'f';
|
||||||
if (this._pendingFillRequests[dir]) {
|
if (this._pendingFillRequests[dir]) {
|
||||||
debuglog("ScrollPanel: Already a "+dir+" fill in progress - not starting another");
|
debuglog("ScrollPanel: Already a "+dir+" fill in progress - not starting another");
|
||||||
return;
|
return;
|
||||||
|
@ -470,8 +471,8 @@ module.exports = React.createClass({
|
||||||
* mult: -1 to page up, +1 to page down
|
* mult: -1 to page up, +1 to page down
|
||||||
*/
|
*/
|
||||||
scrollRelative: function(mult) {
|
scrollRelative: function(mult) {
|
||||||
var scrollNode = this._getScrollNode();
|
const scrollNode = this._getScrollNode();
|
||||||
var delta = mult * scrollNode.clientHeight * 0.5;
|
const delta = mult * scrollNode.clientHeight * 0.5;
|
||||||
this._setScrollTop(scrollNode.scrollTop + delta);
|
this._setScrollTop(scrollNode.scrollTop + delta);
|
||||||
this._saveScrollState();
|
this._saveScrollState();
|
||||||
},
|
},
|
||||||
|
@ -535,7 +536,7 @@ module.exports = React.createClass({
|
||||||
this.scrollState = {
|
this.scrollState = {
|
||||||
stuckAtBottom: false,
|
stuckAtBottom: false,
|
||||||
trackedScrollToken: scrollToken,
|
trackedScrollToken: scrollToken,
|
||||||
pixelOffset: pixelOffset
|
pixelOffset: pixelOffset,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ... then make it so.
|
// ... then make it so.
|
||||||
|
@ -546,10 +547,10 @@ module.exports = React.createClass({
|
||||||
// given offset in the window. A helper for _restoreSavedScrollState.
|
// given offset in the window. A helper for _restoreSavedScrollState.
|
||||||
_scrollToToken: function(scrollToken, pixelOffset) {
|
_scrollToToken: function(scrollToken, pixelOffset) {
|
||||||
/* find the dom node with the right scrolltoken */
|
/* find the dom node with the right scrolltoken */
|
||||||
var node;
|
let node;
|
||||||
var messages = this.refs.itemlist.children;
|
const messages = this.refs.itemlist.children;
|
||||||
for (var i = messages.length-1; i >= 0; --i) {
|
for (let i = messages.length-1; i >= 0; --i) {
|
||||||
var m = messages[i];
|
const m = messages[i];
|
||||||
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
|
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
|
||||||
// There might only be one scroll token
|
// There might only be one scroll token
|
||||||
if (m.dataset.scrollTokens &&
|
if (m.dataset.scrollTokens &&
|
||||||
|
@ -564,10 +565,10 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var scrollNode = this._getScrollNode();
|
const scrollNode = this._getScrollNode();
|
||||||
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||||
var boundingRect = node.getBoundingClientRect();
|
const boundingRect = node.getBoundingClientRect();
|
||||||
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
const scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||||
|
|
||||||
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
|
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
|
||||||
pixelOffset + " (delta: "+scrollDelta+")");
|
pixelOffset + " (delta: "+scrollDelta+")");
|
||||||
|
@ -575,7 +576,6 @@ module.exports = React.createClass({
|
||||||
if(scrollDelta != 0) {
|
if(scrollDelta != 0) {
|
||||||
this._setScrollTop(scrollNode.scrollTop + scrollDelta);
|
this._setScrollTop(scrollNode.scrollTop + scrollDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_saveScrollState: function() {
|
_saveScrollState: function() {
|
||||||
|
@ -585,16 +585,16 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemlist = this.refs.itemlist;
|
const itemlist = this.refs.itemlist;
|
||||||
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||||
var messages = itemlist.children;
|
const messages = itemlist.children;
|
||||||
let newScrollState = null;
|
let newScrollState = null;
|
||||||
|
|
||||||
for (var i = messages.length-1; i >= 0; --i) {
|
for (let i = messages.length-1; i >= 0; --i) {
|
||||||
var node = messages[i];
|
const node = messages[i];
|
||||||
if (!node.dataset.scrollTokens) continue;
|
if (!node.dataset.scrollTokens) continue;
|
||||||
|
|
||||||
var boundingRect = node.getBoundingClientRect();
|
const boundingRect = node.getBoundingClientRect();
|
||||||
newScrollState = {
|
newScrollState = {
|
||||||
stuckAtBottom: false,
|
stuckAtBottom: false,
|
||||||
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
|
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
|
||||||
|
@ -619,8 +619,8 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_restoreSavedScrollState: function() {
|
_restoreSavedScrollState: function() {
|
||||||
var scrollState = this.scrollState;
|
const scrollState = this.scrollState;
|
||||||
var scrollNode = this._getScrollNode();
|
const scrollNode = this._getScrollNode();
|
||||||
|
|
||||||
if (scrollState.stuckAtBottom) {
|
if (scrollState.stuckAtBottom) {
|
||||||
this._setScrollTop(Number.MAX_VALUE);
|
this._setScrollTop(Number.MAX_VALUE);
|
||||||
|
@ -631,9 +631,9 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_setScrollTop: function(scrollTop) {
|
_setScrollTop: function(scrollTop) {
|
||||||
var scrollNode = this._getScrollNode();
|
const scrollNode = this._getScrollNode();
|
||||||
|
|
||||||
var prevScroll = scrollNode.scrollTop;
|
const prevScroll = scrollNode.scrollTop;
|
||||||
|
|
||||||
// FF ignores attempts to set scrollTop to very large numbers
|
// FF ignores attempts to set scrollTop to very large numbers
|
||||||
scrollNode.scrollTop = Math.min(scrollTop, scrollNode.scrollHeight);
|
scrollNode.scrollTop = Math.min(scrollTop, scrollNode.scrollHeight);
|
||||||
|
@ -676,7 +676,7 @@ module.exports = React.createClass({
|
||||||
className={this.props.className} style={this.props.style}>
|
className={this.props.className} style={this.props.style}>
|
||||||
<div className="mx_RoomView_messageListWrapper">
|
<div className="mx_RoomView_messageListWrapper">
|
||||||
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
|
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
|
||||||
{this.props.children}
|
{ this.props.children }
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbar>
|
||||||
|
|
|
@ -15,27 +15,27 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require('react');
|
const React = require('react');
|
||||||
var ReactDOM = require("react-dom");
|
const ReactDOM = require("react-dom");
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
var Matrix = require("matrix-js-sdk");
|
const Matrix = require("matrix-js-sdk");
|
||||||
var EventTimeline = Matrix.EventTimeline;
|
const EventTimeline = Matrix.EventTimeline;
|
||||||
|
|
||||||
var sdk = require('../../index');
|
const sdk = require('../../index');
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
var dis = require("../../dispatcher");
|
const dis = require("../../dispatcher");
|
||||||
var ObjectUtils = require('../../ObjectUtils');
|
const ObjectUtils = require('../../ObjectUtils');
|
||||||
var Modal = require("../../Modal");
|
const Modal = require("../../Modal");
|
||||||
var UserActivity = require("../../UserActivity");
|
const UserActivity = require("../../UserActivity");
|
||||||
var KeyCode = require('../../KeyCode');
|
const KeyCode = require('../../KeyCode');
|
||||||
import UserSettingsStore from '../../UserSettingsStore';
|
import UserSettingsStore from '../../UserSettingsStore';
|
||||||
|
|
||||||
var PAGINATE_SIZE = 20;
|
const PAGINATE_SIZE = 20;
|
||||||
var INITIAL_SIZE = 20;
|
const INITIAL_SIZE = 20;
|
||||||
|
|
||||||
var DEBUG = false;
|
const DEBUG = false;
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
// using bind means that we get to keep useful line numbers in the console
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
|
@ -59,6 +59,7 @@ var TimelinePanel = React.createClass({
|
||||||
// that room.
|
// that room.
|
||||||
timelineSet: React.PropTypes.object.isRequired,
|
timelineSet: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
showReadReceipts: React.PropTypes.bool,
|
||||||
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
||||||
manageReadReceipts: React.PropTypes.bool,
|
manageReadReceipts: React.PropTypes.bool,
|
||||||
manageReadMarkers: React.PropTypes.bool,
|
manageReadMarkers: React.PropTypes.bool,
|
||||||
|
@ -259,7 +260,7 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
|
||||||
var client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (client) {
|
if (client) {
|
||||||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
|
client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
|
||||||
|
@ -274,20 +275,20 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
onMessageListUnfillRequest: function(backwards, scrollToken) {
|
onMessageListUnfillRequest: function(backwards, scrollToken) {
|
||||||
// If backwards, unpaginate from the back (i.e. the start of the timeline)
|
// If backwards, unpaginate from the back (i.e. the start of the timeline)
|
||||||
let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||||
debuglog("TimelinePanel: unpaginating events in direction", dir);
|
debuglog("TimelinePanel: unpaginating events in direction", dir);
|
||||||
|
|
||||||
// All tiles are inserted by MessagePanel to have a scrollToken === eventId, and
|
// All tiles are inserted by MessagePanel to have a scrollToken === eventId, and
|
||||||
// this particular event should be the first or last to be unpaginated.
|
// this particular event should be the first or last to be unpaginated.
|
||||||
let eventId = scrollToken;
|
const eventId = scrollToken;
|
||||||
|
|
||||||
let marker = this.state.events.findIndex(
|
const marker = this.state.events.findIndex(
|
||||||
(ev) => {
|
(ev) => {
|
||||||
return ev.getId() === eventId;
|
return ev.getId() === eventId;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let count = backwards ? marker + 1 : this.state.events.length - marker;
|
const count = backwards ? marker + 1 : this.state.events.length - marker;
|
||||||
|
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir);
|
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir);
|
||||||
|
@ -304,9 +305,9 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// set off a pagination request.
|
// set off a pagination request.
|
||||||
onMessageListFillRequest: function(backwards) {
|
onMessageListFillRequest: function(backwards) {
|
||||||
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||||
var canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
|
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
|
||||||
var paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
|
const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
|
||||||
|
|
||||||
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");
|
||||||
|
@ -327,7 +328,7 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
|
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
|
||||||
|
|
||||||
var newState = {
|
const newState = {
|
||||||
[paginatingKey]: false,
|
[paginatingKey]: false,
|
||||||
[canPaginateKey]: r,
|
[canPaginateKey]: r,
|
||||||
events: this._getEvents(),
|
events: this._getEvents(),
|
||||||
|
@ -335,17 +336,24 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// moving the window in this direction may mean that we can now
|
// moving the window in this direction may mean that we can now
|
||||||
// paginate in the other where we previously could not.
|
// paginate in the other where we previously could not.
|
||||||
var otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
|
const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
|
||||||
var canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
|
const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
|
||||||
if (!this.state[canPaginateOtherWayKey] &&
|
if (!this.state[canPaginateOtherWayKey] &&
|
||||||
this._timelineWindow.canPaginate(otherDirection)) {
|
this._timelineWindow.canPaginate(otherDirection)) {
|
||||||
debuglog('TimelinePanel: can now', otherDirection, 'paginate again');
|
debuglog('TimelinePanel: can now', otherDirection, 'paginate again');
|
||||||
newState[canPaginateOtherWayKey] = true;
|
newState[canPaginateOtherWayKey] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(newState);
|
// Don't resolve until the setState has completed: we need to let
|
||||||
|
// the component update before we consider the pagination completed,
|
||||||
return r;
|
// otherwise we'll end up paginating in all the history the js-sdk
|
||||||
|
// has in memory because we never gave the component a chance to scroll
|
||||||
|
// itself into the right place
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.setState(newState, () => {
|
||||||
|
resolve(r);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -376,6 +384,9 @@ var TimelinePanel = React.createClass({
|
||||||
this.sendReadReceipt();
|
this.sendReadReceipt();
|
||||||
this.updateReadMarker();
|
this.updateReadMarker();
|
||||||
break;
|
break;
|
||||||
|
case 'ignore_state_changed':
|
||||||
|
this.forceUpdate();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -409,15 +420,15 @@ var TimelinePanel = React.createClass({
|
||||||
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => {
|
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => {
|
||||||
if (this.unmounted) { return; }
|
if (this.unmounted) { return; }
|
||||||
|
|
||||||
var events = this._timelineWindow.getEvents();
|
const events = this._timelineWindow.getEvents();
|
||||||
var lastEv = events[events.length-1];
|
const lastEv = events[events.length-1];
|
||||||
|
|
||||||
// if we're at the end of the live timeline, append the pending events
|
// if we're at the end of the live timeline, append the pending events
|
||||||
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||||
events.push(...this.props.timelineSet.room.getPendingEvents());
|
events.push(...this.props.timelineSet.room.getPendingEvents());
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedState = {events: events};
|
const updatedState = {events: events};
|
||||||
|
|
||||||
if (this.props.manageReadMarkers) {
|
if (this.props.manageReadMarkers) {
|
||||||
// when a new event arrives when the user is not watching the
|
// when a new event arrives when the user is not watching the
|
||||||
|
@ -428,8 +439,8 @@ var TimelinePanel = React.createClass({
|
||||||
// read-marker when a remote echo of an event we have just sent takes
|
// read-marker when a remote echo of an event we have just sent takes
|
||||||
// more than the timeout on userCurrentlyActive.
|
// more than the timeout on userCurrentlyActive.
|
||||||
//
|
//
|
||||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
var sender = ev.sender ? ev.sender.userId : null;
|
const sender = ev.sender ? ev.sender.userId : null;
|
||||||
var callback = null;
|
var callback = null;
|
||||||
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
|
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
|
||||||
updatedState.readMarkerVisible = true;
|
updatedState.readMarkerVisible = true;
|
||||||
|
@ -635,7 +646,7 @@ var TimelinePanel = React.createClass({
|
||||||
// and we'll get confused when their ID changes and we can't figure out
|
// and we'll get confused when their ID changes and we can't figure out
|
||||||
// where the RM is pointing to. The read marker will be invisible for
|
// where the RM is pointing to. The read marker will be invisible for
|
||||||
// now anyway, so this doesn't really matter.
|
// now anyway, so this doesn't really matter.
|
||||||
var lastDisplayedIndex = this._getLastDisplayedEventIndex({
|
const lastDisplayedIndex = this._getLastDisplayedEventIndex({
|
||||||
allowPartial: true,
|
allowPartial: true,
|
||||||
ignoreEchoes: true,
|
ignoreEchoes: true,
|
||||||
});
|
});
|
||||||
|
@ -644,7 +655,7 @@ var TimelinePanel = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastDisplayedEvent = this.state.events[lastDisplayedIndex];
|
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
|
||||||
this._setReadMarker(lastDisplayedEvent.getId(),
|
this._setReadMarker(lastDisplayedEvent.getId(),
|
||||||
lastDisplayedEvent.getTs());
|
lastDisplayedEvent.getTs());
|
||||||
|
|
||||||
|
@ -665,7 +676,7 @@ var TimelinePanel = React.createClass({
|
||||||
// we call _timelineWindow.getEvents() rather than using
|
// we call _timelineWindow.getEvents() rather than using
|
||||||
// this.state.events, because react batches the update to the latter, so it
|
// this.state.events, because react batches the update to the latter, so it
|
||||||
// may not have been updated yet.
|
// may not have been updated yet.
|
||||||
var events = this._timelineWindow.getEvents();
|
const events = this._timelineWindow.getEvents();
|
||||||
|
|
||||||
// first find where the current RM is
|
// first find where the current RM is
|
||||||
for (var i = 0; i < events.length; i++) {
|
for (var i = 0; i < events.length; i++) {
|
||||||
|
@ -678,7 +689,7 @@ var TimelinePanel = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
// now think about advancing it
|
// now think about advancing it
|
||||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
for (i++; i < events.length; i++) {
|
for (i++; i < events.length; i++) {
|
||||||
var ev = events[i];
|
var ev = events[i];
|
||||||
if (!ev.sender || ev.sender.userId != myUserId) {
|
if (!ev.sender || ev.sender.userId != myUserId) {
|
||||||
|
@ -723,7 +734,7 @@ var TimelinePanel = React.createClass({
|
||||||
//
|
//
|
||||||
// a quick way to figure out if we've loaded the relevant event is
|
// a quick way to figure out if we've loaded the relevant event is
|
||||||
// simply to check if the messagepanel knows where the read-marker is.
|
// simply to check if the messagepanel knows where the read-marker is.
|
||||||
var ret = this.refs.messagePanel.getReadMarkerPosition();
|
const ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||||
if (ret !== null) {
|
if (ret !== null) {
|
||||||
// The messagepanel knows where the RM is, so we must have loaded
|
// The messagepanel knows where the RM is, so we must have loaded
|
||||||
// the relevant event.
|
// the relevant event.
|
||||||
|
@ -744,13 +755,13 @@ var TimelinePanel = React.createClass({
|
||||||
forgetReadMarker: function() {
|
forgetReadMarker: function() {
|
||||||
if (!this.props.manageReadMarkers) return;
|
if (!this.props.manageReadMarkers) return;
|
||||||
|
|
||||||
var rmId = this._getCurrentReadReceipt();
|
const rmId = this._getCurrentReadReceipt();
|
||||||
|
|
||||||
// see if we know the timestamp for the rr event
|
// see if we know the timestamp for the rr event
|
||||||
var tl = this.props.timelineSet.getTimelineForEvent(rmId);
|
const tl = this.props.timelineSet.getTimelineForEvent(rmId);
|
||||||
var rmTs;
|
let rmTs;
|
||||||
if (tl) {
|
if (tl) {
|
||||||
var event = tl.getEvents().find((e) => { return e.getId() == rmId; });
|
const event = tl.getEvents().find((e) => { return e.getId() == rmId; });
|
||||||
if (event) {
|
if (event) {
|
||||||
rmTs = event.getTs();
|
rmTs = event.getTs();
|
||||||
}
|
}
|
||||||
|
@ -790,7 +801,7 @@ var TimelinePanel = React.createClass({
|
||||||
if (!this.props.manageReadMarkers) return null;
|
if (!this.props.manageReadMarkers) return null;
|
||||||
if (!this.refs.messagePanel) return null;
|
if (!this.refs.messagePanel) return null;
|
||||||
|
|
||||||
var ret = this.refs.messagePanel.getReadMarkerPosition();
|
const ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||||
if (ret !== null) {
|
if (ret !== null) {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -833,8 +844,7 @@ 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.shiftKey && !ev.altKey && !ev.metaKey &&
|
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
|
||||||
ev.keyCode == KeyCode.END)
|
ev.keyCode == KeyCode.END) {
|
||||||
{
|
|
||||||
this.jumpToLiveTimeline();
|
this.jumpToLiveTimeline();
|
||||||
} else {
|
} else {
|
||||||
this.refs.messagePanel.handleScrollKey(ev);
|
this.refs.messagePanel.handleScrollKey(ev);
|
||||||
|
@ -842,12 +852,12 @@ var TimelinePanel = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_initTimeline: function(props) {
|
_initTimeline: function(props) {
|
||||||
var initialEvent = props.eventId;
|
const initialEvent = props.eventId;
|
||||||
var pixelOffset = props.eventPixelOffset;
|
const pixelOffset = props.eventPixelOffset;
|
||||||
|
|
||||||
// if a pixelOffset is given, it is relative to the bottom of the
|
// if a pixelOffset is given, it is relative to the bottom of the
|
||||||
// container. If not, put the event in the middle of the container.
|
// container. If not, put the event in the middle of the container.
|
||||||
var offsetBase = 1;
|
let offsetBase = 1;
|
||||||
if (pixelOffset == null) {
|
if (pixelOffset == null) {
|
||||||
offsetBase = 0.5;
|
offsetBase = 0.5;
|
||||||
}
|
}
|
||||||
|
@ -876,7 +886,7 @@ var TimelinePanel = React.createClass({
|
||||||
MatrixClientPeg.get(), this.props.timelineSet,
|
MatrixClientPeg.get(), this.props.timelineSet,
|
||||||
{windowLimit: this.props.timelineCap});
|
{windowLimit: this.props.timelineCap});
|
||||||
|
|
||||||
var onLoaded = () => {
|
const onLoaded = () => {
|
||||||
this._reloadEvents();
|
this._reloadEvents();
|
||||||
|
|
||||||
// If we switched away from the room while there were pending
|
// If we switched away from the room while there were pending
|
||||||
|
@ -911,15 +921,15 @@ var TimelinePanel = React.createClass({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var onError = (error) => {
|
const onError = (error) => {
|
||||||
this.setState({timelineLoading: false});
|
this.setState({timelineLoading: false});
|
||||||
console.error(
|
console.error(
|
||||||
`Error loading timeline panel at ${eventId}: ${error}`,
|
`Error loading timeline panel at ${eventId}: ${error}`,
|
||||||
);
|
);
|
||||||
var msg = error.message ? error.message : JSON.stringify(error);
|
const msg = error.message ? error.message : JSON.stringify(error);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
|
||||||
var onFinished;
|
let onFinished;
|
||||||
|
|
||||||
// if we were given an event ID, then when the user closes the
|
// if we were given an event ID, then when the user closes the
|
||||||
// dialog, let's jump to the end of the timeline. If we weren't,
|
// dialog, let's jump to the end of the timeline. If we weren't,
|
||||||
|
@ -934,7 +944,7 @@ var TimelinePanel = React.createClass({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
var message = (error.errcode == 'M_FORBIDDEN')
|
const message = (error.errcode == 'M_FORBIDDEN')
|
||||||
? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.")
|
? _t("Tried to load a specific point in this room's timeline, but 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.");
|
: _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
|
||||||
Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, {
|
||||||
|
@ -944,7 +954,7 @@ var TimelinePanel = React.createClass({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
|
let prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
|
||||||
|
|
||||||
// if we already have the event in question, TimelineWindow.load
|
// if we already have the event in question, TimelineWindow.load
|
||||||
// returns a resolved promise.
|
// returns a resolved promise.
|
||||||
|
@ -985,7 +995,7 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// get the list of events from the timeline window and the pending event list
|
// get the list of events from the timeline window and the pending event list
|
||||||
_getEvents: function() {
|
_getEvents: function() {
|
||||||
var events = this._timelineWindow.getEvents();
|
const events = this._timelineWindow.getEvents();
|
||||||
|
|
||||||
// if we're at the end of the live timeline, append the pending events
|
// if we're at the end of the live timeline, append the pending events
|
||||||
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||||
|
@ -996,7 +1006,7 @@ var TimelinePanel = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_indexForEventId: function(evId) {
|
_indexForEventId: function(evId) {
|
||||||
for (var i = 0; i < this.state.events.length; ++i) {
|
for (let i = 0; i < this.state.events.length; ++i) {
|
||||||
if (evId == this.state.events[i].getId()) {
|
if (evId == this.state.events[i].getId()) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
@ -1006,18 +1016,18 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
_getLastDisplayedEventIndex: function(opts) {
|
_getLastDisplayedEventIndex: function(opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
var ignoreOwn = opts.ignoreOwn || false;
|
const ignoreOwn = opts.ignoreOwn || false;
|
||||||
var ignoreEchoes = opts.ignoreEchoes || false;
|
const ignoreEchoes = opts.ignoreEchoes || false;
|
||||||
var allowPartial = opts.allowPartial || false;
|
const allowPartial = opts.allowPartial || false;
|
||||||
|
|
||||||
var messagePanel = this.refs.messagePanel;
|
const messagePanel = this.refs.messagePanel;
|
||||||
if (messagePanel === undefined) return null;
|
if (messagePanel === undefined) return null;
|
||||||
|
|
||||||
var wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
const wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
||||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
|
|
||||||
for (var i = this.state.events.length-1; i >= 0; --i) {
|
for (let i = this.state.events.length-1; i >= 0; --i) {
|
||||||
var ev = this.state.events[i];
|
const ev = this.state.events[i];
|
||||||
|
|
||||||
if (ignoreOwn && ev.sender && ev.sender.userId == myUserId) {
|
if (ignoreOwn && ev.sender && ev.sender.userId == myUserId) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -1028,10 +1038,10 @@ var TimelinePanel = React.createClass({
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var node = messagePanel.getNodeForEventId(ev.getId());
|
const node = messagePanel.getNodeForEventId(ev.getId());
|
||||||
if (!node) continue;
|
if (!node) continue;
|
||||||
|
|
||||||
var boundingRect = node.getBoundingClientRect();
|
const boundingRect = node.getBoundingClientRect();
|
||||||
if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
|
if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
|
||||||
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
|
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
|
||||||
return i;
|
return i;
|
||||||
|
@ -1049,18 +1059,18 @@ var TimelinePanel = React.createClass({
|
||||||
* SDK.
|
* SDK.
|
||||||
*/
|
*/
|
||||||
_getCurrentReadReceipt: function(ignoreSynthesized) {
|
_getCurrentReadReceipt: function(ignoreSynthesized) {
|
||||||
var client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
// the client can be null on logout
|
// the client can be null on logout
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var myUserId = client.credentials.userId;
|
const myUserId = client.credentials.userId;
|
||||||
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
|
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
|
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
|
||||||
var roomId = this.props.timelineSet.room.roomId;
|
const roomId = this.props.timelineSet.room.roomId;
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -1085,8 +1095,8 @@ var TimelinePanel = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var MessagePanel = sdk.getComponent("structures.MessagePanel");
|
const MessagePanel = sdk.getComponent("structures.MessagePanel");
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
// just show a spinner while the timeline loads.
|
// just show a spinner while the timeline loads.
|
||||||
//
|
//
|
||||||
|
@ -1101,7 +1111,7 @@ var TimelinePanel = React.createClass({
|
||||||
// exist.
|
// exist.
|
||||||
if (this.state.timelineLoading) {
|
if (this.state.timelineLoading) {
|
||||||
return (
|
return (
|
||||||
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
|
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1109,7 +1119,7 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
|
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
|
||||||
return (
|
return (
|
||||||
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
|
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
||||||
<div className="mx_RoomView_empty">{ this.props.empty }</div>
|
<div className="mx_RoomView_empty">{ this.props.empty }</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1123,7 +1133,7 @@ var TimelinePanel = React.createClass({
|
||||||
// forwards, otherwise if somebody hits the bottom of the loaded
|
// forwards, otherwise if somebody hits the bottom of the loaded
|
||||||
// events when viewing historical messages, we get stuck in a loop
|
// events when viewing historical messages, we get stuck in a loop
|
||||||
// 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);
|
const stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||||
|
|
||||||
// If the state is PREPARED, we're still waiting for the js-sdk to sync with
|
// 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.
|
// the HS and fetch the latest events, so we are effectively forward paginating.
|
||||||
|
@ -1132,26 +1142,26 @@ var TimelinePanel = React.createClass({
|
||||||
);
|
);
|
||||||
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={ 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}
|
||||||
readMarkerVisible={ this.state.readMarkerVisible }
|
readMarkerVisible={this.state.readMarkerVisible}
|
||||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
suppressFirstDateSeparator={this.state.canBackPaginate}
|
||||||
showUrlPreview = { this.props.showUrlPreview }
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
manageReadReceipts = { this.props.manageReadReceipts }
|
showReadReceipts={this.props.showReadReceipts}
|
||||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
ourUserId={MatrixClientPeg.get().credentials.userId}
|
||||||
stickyBottom={ stickyBottom }
|
stickyBottom={stickyBottom}
|
||||||
onScroll={ this.onMessageListScroll }
|
onScroll={this.onMessageListScroll}
|
||||||
onFillRequest={ this.onMessageListFillRequest }
|
onFillRequest={this.onMessageListFillRequest}
|
||||||
onUnfillRequest={ this.onMessageListUnfillRequest }
|
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||||
opacity={ this.props.opacity }
|
opacity={this.props.opacity}
|
||||||
isTwelveHour={ this.state.isTwelveHour }
|
isTwelveHour={this.state.isTwelveHour}
|
||||||
alwaysShowTimestamps={ this.state.alwaysShowTimestamps }
|
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
|
||||||
className={ this.props.className }
|
className={this.props.className}
|
||||||
tileShape={ this.props.tileShape }
|
tileShape={this.props.tileShape}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require('react');
|
const React = require('react');
|
||||||
var ContentMessages = require('../../ContentMessages');
|
const ContentMessages = require('../../ContentMessages');
|
||||||
var dis = require('../../dispatcher');
|
const dis = require('../../dispatcher');
|
||||||
var filesize = require('filesize');
|
const filesize = require('filesize');
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
|
||||||
module.exports = React.createClass({displayName: 'UploadBar',
|
module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
room: React.PropTypes.object
|
room: React.PropTypes.object,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -46,7 +46,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var uploads = ContentMessages.getCurrentUploads();
|
const uploads = ContentMessages.getCurrentUploads();
|
||||||
|
|
||||||
// for testing UI... - also fix up the ContentMessages.getCurrentUploads().length
|
// for testing UI... - also fix up the ContentMessages.getCurrentUploads().length
|
||||||
// check in RoomView
|
// check in RoomView
|
||||||
|
@ -62,8 +62,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
var upload;
|
let upload;
|
||||||
for (var i = 0; i < uploads.length; ++i) {
|
for (let i = 0; i < uploads.length; ++i) {
|
||||||
if (uploads[i].roomId == this.props.room.roomId) {
|
if (uploads[i].roomId == this.props.room.roomId) {
|
||||||
upload = uploads[i];
|
upload = uploads[i];
|
||||||
break;
|
break;
|
||||||
|
@ -73,32 +73,32 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
var innerProgressStyle = {
|
const innerProgressStyle = {
|
||||||
width: ((upload.loaded / (upload.total || 1)) * 100) + '%'
|
width: ((upload.loaded / (upload.total || 1)) * 100) + '%',
|
||||||
};
|
};
|
||||||
var uploadedSize = filesize(upload.loaded);
|
let uploadedSize = filesize(upload.loaded);
|
||||||
var totalSize = filesize(upload.total);
|
const totalSize = filesize(upload.total);
|
||||||
if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) {
|
if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) {
|
||||||
uploadedSize = uploadedSize.replace(/ .*/, '');
|
uploadedSize = uploadedSize.replace(/ .*/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// MUST use var name 'count' for pluralization to kick in
|
// MUST use var name 'count' for pluralization to kick in
|
||||||
var uploadText = _t("Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)});
|
const uploadText = _t("Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_UploadBar">
|
<div className="mx_UploadBar">
|
||||||
<div className="mx_UploadBar_uploadProgressOuter">
|
<div className="mx_UploadBar_uploadProgressOuter">
|
||||||
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
|
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
|
||||||
</div>
|
</div>
|
||||||
<img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src="img/fileicon.png" width="17" height="22"/>
|
<img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src="img/fileicon.png" width="17" height="22" />
|
||||||
<img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src="img/cancel.svg" width="18" height="18"
|
<img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src="img/cancel.svg" width="18" height="18"
|
||||||
onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
|
onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
|
||||||
/>
|
/>
|
||||||
<div className="mx_UploadBar_uploadBytes">
|
<div className="mx_UploadBar_uploadBytes">
|
||||||
{ uploadedSize } / { totalSize }
|
{ uploadedSize } / { totalSize }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UploadBar_uploadFilename">{uploadText}</div>
|
<div className="mx_UploadBar_uploadFilename">{ uploadText }</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2017 New Vector 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.
|
||||||
|
@ -32,7 +33,7 @@ const AddThreepid = require('../../AddThreepid');
|
||||||
const SdkConfig = require('../../SdkConfig');
|
const SdkConfig = require('../../SdkConfig');
|
||||||
import Analytics from '../../Analytics';
|
import Analytics from '../../Analytics';
|
||||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t, _td } from '../../languageHandler';
|
||||||
import * as languageHandler from '../../languageHandler';
|
import * as languageHandler from '../../languageHandler';
|
||||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||||
|
|
||||||
|
@ -52,7 +53,7 @@ const gHVersionLabel = function(repo, token='') {
|
||||||
} else {
|
} else {
|
||||||
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
|
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
|
||||||
}
|
}
|
||||||
return <a target="_blank" rel="noopener" href={url}>{token}</a>;
|
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enumerate some simple 'flip a bit' UI settings (if any).
|
// Enumerate some simple 'flip a bit' UI settings (if any).
|
||||||
|
@ -63,51 +64,59 @@ const gHVersionLabel = function(repo, token='') {
|
||||||
const SETTINGS_LABELS = [
|
const SETTINGS_LABELS = [
|
||||||
{
|
{
|
||||||
id: 'autoplayGifsAndVideos',
|
id: 'autoplayGifsAndVideos',
|
||||||
label: 'Autoplay GIFs and videos',
|
label: _td('Autoplay GIFs and videos'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hideReadReceipts',
|
id: 'hideReadReceipts',
|
||||||
label: 'Hide read receipts',
|
label: _td('Hide read receipts'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dontSendTypingNotifications',
|
id: 'dontSendTypingNotifications',
|
||||||
label: "Don't send typing notifications",
|
label: _td("Don't send typing notifications"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alwaysShowTimestamps',
|
id: 'alwaysShowTimestamps',
|
||||||
label: 'Always show message timestamps',
|
label: _td('Always show message timestamps'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'showTwelveHourTimestamps',
|
id: 'showTwelveHourTimestamps',
|
||||||
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
|
label: _td('Show timestamps in 12 hour format (e.g. 2:30pm)'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hideJoinLeaves',
|
id: 'hideJoinLeaves',
|
||||||
label: 'Hide join/leave messages (invites/kicks/bans unaffected)',
|
label: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hideAvatarDisplaynameChanges',
|
id: 'hideAvatarDisplaynameChanges',
|
||||||
label: 'Hide avatar and display name changes',
|
label: _td('Hide avatar and display name changes'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'useCompactLayout',
|
id: 'useCompactLayout',
|
||||||
label: 'Use compact timeline layout',
|
label: _td('Use compact timeline layout'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hideRedactions',
|
id: 'hideRedactions',
|
||||||
label: 'Hide removed messages',
|
label: _td('Hide removed messages'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'enableSyntaxHighlightLanguageDetection',
|
id: 'enableSyntaxHighlightLanguageDetection',
|
||||||
label: 'Enable automatic language detection for syntax highlighting',
|
label: _td('Enable automatic language detection for syntax highlighting'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'MessageComposerInput.autoReplaceEmoji',
|
id: 'MessageComposerInput.autoReplaceEmoji',
|
||||||
label: 'Automatically replace plain text Emoji',
|
label: _td('Automatically replace plain text Emoji'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'MessageComposerInput.dontSuggestEmoji',
|
||||||
|
label: _td('Disable Emoji suggestions while typing'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'Pill.shouldHidePillAvatar',
|
id: 'Pill.shouldHidePillAvatar',
|
||||||
label: 'Hide avatars in user and room mentions',
|
label: _td('Hide avatars in user and room mentions'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'TextualBody.disableBigEmoji',
|
||||||
|
label: _td('Disable big emoji in chat'),
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
|
@ -120,7 +129,7 @@ const SETTINGS_LABELS = [
|
||||||
const ANALYTICS_SETTINGS_LABELS = [
|
const ANALYTICS_SETTINGS_LABELS = [
|
||||||
{
|
{
|
||||||
id: 'analyticsOptOut',
|
id: 'analyticsOptOut',
|
||||||
label: 'Opt out of analytics',
|
label: _td('Opt out of analytics'),
|
||||||
fn: function(checked) {
|
fn: function(checked) {
|
||||||
Analytics[checked ? 'disable' : 'enable']();
|
Analytics[checked ? 'disable' : 'enable']();
|
||||||
},
|
},
|
||||||
|
@ -130,7 +139,7 @@ const ANALYTICS_SETTINGS_LABELS = [
|
||||||
const WEBRTC_SETTINGS_LABELS = [
|
const WEBRTC_SETTINGS_LABELS = [
|
||||||
{
|
{
|
||||||
id: 'webRtcForceTURN',
|
id: 'webRtcForceTURN',
|
||||||
label: 'Disable Peer-to-Peer for 1:1 calls',
|
label: _td('Disable Peer-to-Peer for 1:1 calls'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -139,7 +148,7 @@ const WEBRTC_SETTINGS_LABELS = [
|
||||||
const CRYPTO_SETTINGS_LABELS = [
|
const CRYPTO_SETTINGS_LABELS = [
|
||||||
{
|
{
|
||||||
id: 'blacklistUnverifiedDevices',
|
id: 'blacklistUnverifiedDevices',
|
||||||
label: 'Never send encrypted messages to unverified devices from this device',
|
label: _td('Never send encrypted messages to unverified devices from this device'),
|
||||||
fn: function(checked) {
|
fn: function(checked) {
|
||||||
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
|
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
|
||||||
},
|
},
|
||||||
|
@ -162,16 +171,44 @@ const CRYPTO_SETTINGS_LABELS = [
|
||||||
const THEMES = [
|
const THEMES = [
|
||||||
{
|
{
|
||||||
id: 'theme',
|
id: 'theme',
|
||||||
label: 'Light theme',
|
label: _td('Light theme'),
|
||||||
value: 'light',
|
value: 'light',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'theme',
|
id: 'theme',
|
||||||
label: 'Dark theme',
|
label: _td('Dark theme'),
|
||||||
value: 'dark',
|
value: 'dark',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const IgnoredUser = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
userId: React.PropTypes.string.isRequired,
|
||||||
|
onUnignored: React.PropTypes.func.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
_onUnignoreClick: function() {
|
||||||
|
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||||
|
const index = ignoredUsers.indexOf(this.props.userId);
|
||||||
|
if (index !== -1) {
|
||||||
|
ignoredUsers.splice(index, 1);
|
||||||
|
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers)
|
||||||
|
.then(() => this.props.onUnignored(this.props.userId));
|
||||||
|
} else this.props.onUnignored(this.props.userId);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<AccessibleButton onClick={this._onUnignoreClick} className="mx_UserSettings_button mx_UserSettings_buttonSmall">
|
||||||
|
{ _t("Unignore") }
|
||||||
|
</AccessibleButton>
|
||||||
|
{ this.props.userId }
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'UserSettings',
|
displayName: 'UserSettings',
|
||||||
|
|
||||||
|
@ -180,9 +217,6 @@ module.exports = React.createClass({
|
||||||
// The brand string given when creating email pushers
|
// The brand string given when creating email pushers
|
||||||
brand: React.PropTypes.string,
|
brand: React.PropTypes.string,
|
||||||
|
|
||||||
// True to show the 'labs' section of experimental features
|
|
||||||
enableLabs: React.PropTypes.bool,
|
|
||||||
|
|
||||||
// The base URL to use in the referral link. Defaults to window.location.origin.
|
// The base URL to use in the referral link. Defaults to window.location.origin.
|
||||||
referralBaseUrl: React.PropTypes.string,
|
referralBaseUrl: React.PropTypes.string,
|
||||||
|
|
||||||
|
@ -194,7 +228,6 @@ module.exports = React.createClass({
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
onClose: function() {},
|
onClose: function() {},
|
||||||
enableLabs: true,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -207,6 +240,7 @@ module.exports = React.createClass({
|
||||||
vectorVersion: undefined,
|
vectorVersion: undefined,
|
||||||
rejectingInvites: false,
|
rejectingInvites: false,
|
||||||
mediaDevices: null,
|
mediaDevices: null,
|
||||||
|
ignoredUsers: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -228,6 +262,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
this._refreshMediaDevices();
|
this._refreshMediaDevices();
|
||||||
|
this._refreshIgnoredUsers();
|
||||||
|
|
||||||
// Bulk rejecting invites:
|
// Bulk rejecting invites:
|
||||||
// /sync won't have had time to return when UserSettings re-renders from state changes, so getRooms()
|
// /sync won't have had time to return when UserSettings re-renders from state changes, so getRooms()
|
||||||
|
@ -346,9 +381,22 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_refreshIgnoredUsers: function(userIdUnignored=null) {
|
||||||
|
const users = MatrixClientPeg.get().getIgnoredUsers();
|
||||||
|
if (userIdUnignored) {
|
||||||
|
const index = users.indexOf(userIdUnignored);
|
||||||
|
if (index !== -1) users.splice(index, 1);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
ignoredUsers: users,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
if (payload.action === "notifier_enabled") {
|
if (payload.action === "notifier_enabled") {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
|
} else if (payload.action === "ignore_state_changed") {
|
||||||
|
this._refreshIgnoredUsers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -379,6 +427,11 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onAvatarRemoveClick: function() {
|
||||||
|
MatrixClientPeg.get().setAvatarUrl(null);
|
||||||
|
this.setState({avatarUrl: null}); // the avatar update will complete async for us
|
||||||
|
},
|
||||||
|
|
||||||
onLogoutClicked: function(ev) {
|
onLogoutClicked: function(ev) {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, {
|
Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, {
|
||||||
|
@ -627,7 +680,7 @@ module.exports = React.createClass({
|
||||||
<div>
|
<div>
|
||||||
<h3>Referral</h3>
|
<h3>Referral</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
{_t("Refer a friend to Riot:")} <a href={href}>{href}</a>
|
{ _t("Refer a friend to Riot:") } <a href={href}>{ href }</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -646,7 +699,7 @@ module.exports = React.createClass({
|
||||||
_renderLanguageSetting: function() {
|
_renderLanguageSetting: function() {
|
||||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||||
return <div>
|
return <div>
|
||||||
<label htmlFor="languageSelector">{_t('Interface Language')}</label>
|
<label htmlFor="languageSelector">{ _t('Interface Language') }</label>
|
||||||
<LanguageDropdown ref="language" onOptionChange={this.onLanguageChange}
|
<LanguageDropdown ref="language" onOptionChange={this.onLanguageChange}
|
||||||
className="mx_UserSettings_language"
|
className="mx_UserSettings_language"
|
||||||
value={this.state.language}
|
value={this.state.language}
|
||||||
|
@ -669,7 +722,7 @@ module.exports = React.createClass({
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{_t('Autocomplete Delay (ms):')}</strong></td>
|
<td><strong>{ _t('Autocomplete Delay (ms):') }</strong></td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
@ -690,8 +743,8 @@ module.exports = React.createClass({
|
||||||
return <div className="mx_UserSettings_toggle">
|
return <div className="mx_UserSettings_toggle">
|
||||||
<input id="urlPreviewsDisabled"
|
<input id="urlPreviewsDisabled"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
|
defaultChecked={UserSettingsStore.getUrlPreviewsDisabled()}
|
||||||
onChange={ this._onPreviewsDisabledChanged }
|
onChange={this._onPreviewsDisabledChanged}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="urlPreviewsDisabled">
|
<label htmlFor="urlPreviewsDisabled">
|
||||||
{ _t("Disable inline URL previews by default") }
|
{ _t("Disable inline URL previews by default") }
|
||||||
|
@ -712,13 +765,13 @@ module.exports = React.createClass({
|
||||||
if (setting.fn) setting.fn(e.target.checked);
|
if (setting.fn) setting.fn(e.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className="mx_UserSettings_toggle" key={ setting.id }>
|
return <div className="mx_UserSettings_toggle" key={setting.id}>
|
||||||
<input id={ setting.id }
|
<input id={setting.id}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={ this._syncedSettings[setting.id] }
|
defaultChecked={this._syncedSettings[setting.id]}
|
||||||
onChange={ onChange }
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={ setting.id }>
|
<label htmlFor={setting.id}>
|
||||||
{ _t(setting.label) }
|
{ _t(setting.label) }
|
||||||
</label>
|
</label>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -729,6 +782,7 @@ module.exports = React.createClass({
|
||||||
// to rebind the onChange each time we render
|
// to rebind the onChange each time we render
|
||||||
const onChange = (e) => {
|
const onChange = (e) => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
|
this._syncedSettings[setting.id] = setting.value;
|
||||||
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
|
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
|
||||||
}
|
}
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -736,16 +790,16 @@ module.exports = React.createClass({
|
||||||
value: setting.value,
|
value: setting.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return <div className="mx_UserSettings_toggle" key={ setting.id + "_" + setting.value }>
|
return <div className="mx_UserSettings_toggle" key={setting.id + "_" + setting.value}>
|
||||||
<input id={ setting.id + "_" + setting.value }
|
<input id={setting.id + "_" + setting.value}
|
||||||
type="radio"
|
type="radio"
|
||||||
name={ setting.id }
|
name={setting.id}
|
||||||
value={ setting.value }
|
value={setting.value}
|
||||||
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
|
checked={this._syncedSettings[setting.id] === setting.value}
|
||||||
onChange={ onChange }
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={ setting.id + "_" + setting.value }>
|
<label htmlFor={setting.id + "_" + setting.value}>
|
||||||
{ setting.label }
|
{ _t(setting.label) }
|
||||||
</label>
|
</label>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
@ -781,10 +835,10 @@ module.exports = React.createClass({
|
||||||
<h3>{ _t("Cryptography") }</h3>
|
<h3>{ _t("Cryptography") }</h3>
|
||||||
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
|
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
|
||||||
<ul>
|
<ul>
|
||||||
<li><label>{_t("Device ID:")}</label>
|
<li><label>{ _t("Device ID:") }</label>
|
||||||
<span><code>{deviceId}</code></span></li>
|
<span><code>{ deviceId }</code></span></li>
|
||||||
<li><label>{_t("Device key:")}</label>
|
<li><label>{ _t("Device key:") }</label>
|
||||||
<span><code><b>{identityKey}</b></code></span></li>
|
<span><code><b>{ identityKey }</b></code></span></li>
|
||||||
</ul>
|
</ul>
|
||||||
{ importExportButtons }
|
{ importExportButtons }
|
||||||
</div>
|
</div>
|
||||||
|
@ -795,6 +849,26 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_renderIgnoredUsers: function() {
|
||||||
|
if (this.state.ignoredUsers.length > 0) {
|
||||||
|
const updateHandler = this._refreshIgnoredUsers;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>{ _t("Ignored Users") }</h3>
|
||||||
|
<div className="mx_UserSettings_section mx_UserSettings_ignoredUsersSection">
|
||||||
|
<ul>
|
||||||
|
{ this.state.ignoredUsers.map(function(userId) {
|
||||||
|
return (<IgnoredUser key={userId}
|
||||||
|
userId={userId}
|
||||||
|
onUnignored={updateHandler}></IgnoredUser>);
|
||||||
|
}) }
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else return (<div />);
|
||||||
|
},
|
||||||
|
|
||||||
_renderLocalSetting: function(setting) {
|
_renderLocalSetting: function(setting) {
|
||||||
// TODO: this ought to be a separate component so that we don't need
|
// TODO: this ought to be a separate component so that we don't need
|
||||||
// to rebind the onChange each time we render
|
// to rebind the onChange each time we render
|
||||||
|
@ -803,13 +877,13 @@ module.exports = React.createClass({
|
||||||
if (setting.fn) setting.fn(e.target.checked);
|
if (setting.fn) setting.fn(e.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className="mx_UserSettings_toggle" key={ setting.id }>
|
return <div className="mx_UserSettings_toggle" key={setting.id}>
|
||||||
<input id={ setting.id }
|
<input id={setting.id}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={ this._localSettings[setting.id] }
|
defaultChecked={this._localSettings[setting.id]}
|
||||||
onChange={ onChange }
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={ setting.id }>
|
<label htmlFor={setting.id}>
|
||||||
{ _t(setting.label) }
|
{ _t(setting.label) }
|
||||||
</label>
|
</label>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -819,8 +893,8 @@ module.exports = React.createClass({
|
||||||
const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>{_t("Devices")}</h3>
|
<h3>{ _t("Devices") }</h3>
|
||||||
<DevicesPanel className="mx_UserSettings_section"/>
|
<DevicesPanel className="mx_UserSettings_section" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -835,7 +909,7 @@ module.exports = React.createClass({
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<p>{ _t("Found a bug?") }</p>
|
<p>{ _t("Found a bug?") }</p>
|
||||||
<button className="mx_UserSettings_button danger"
|
<button className="mx_UserSettings_button danger"
|
||||||
onClick={this._onBugReportClicked}>{_t('Report it')}
|
onClick={this._onBugReportClicked}>{ _t('Report it') }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -843,46 +917,37 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderAnalyticsControl: function() {
|
_renderAnalyticsControl: function() {
|
||||||
if (!SdkConfig.get().piwik) return <div/>;
|
if (!SdkConfig.get().piwik) return <div />;
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<h3>{ _t('Analytics') }</h3>
|
<h3>{ _t('Analytics') }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
{_t('Riot collects anonymous analytics to allow us to improve the application.')}
|
{ _t('Riot collects anonymous analytics to allow us to improve the application.') }
|
||||||
{ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting )}
|
{ ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting ) }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderLabs: function() {
|
_renderLabs: function() {
|
||||||
// default to enabled if undefined
|
|
||||||
if (this.props.enableLabs === false) return null;
|
|
||||||
UserSettingsStore.doTranslations();
|
|
||||||
|
|
||||||
const features = [];
|
const features = [];
|
||||||
UserSettingsStore.LABS_FEATURES.forEach((feature) => {
|
UserSettingsStore.getLabsFeatures().forEach((featureId) => {
|
||||||
// This feature has an override and will be set to the default, so do not
|
|
||||||
// show it here.
|
|
||||||
if (feature.override) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// TODO: this ought to be a separate component so that we don't need
|
// TODO: this ought to be a separate component so that we don't need
|
||||||
// to rebind the onChange each time we render
|
// to rebind the onChange each time we render
|
||||||
const onChange = (e) => {
|
const onChange = (e) => {
|
||||||
UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked);
|
UserSettingsStore.setFeatureEnabled(featureId, e.target.checked);
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
features.push(
|
features.push(
|
||||||
<div key={feature.id} className="mx_UserSettings_toggle">
|
<div key={featureId} className="mx_UserSettings_toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={feature.id}
|
id={featureId}
|
||||||
name={feature.id}
|
name={featureId}
|
||||||
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
|
defaultChecked={UserSettingsStore.isFeatureEnabled(featureId)}
|
||||||
onChange={ onChange }
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={feature.id}>{feature.name}</label>
|
<label htmlFor={featureId}>{ UserSettingsStore.translatedNameForFeature(featureId) }</label>
|
||||||
</div>);
|
</div>);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -896,7 +961,7 @@ module.exports = React.createClass({
|
||||||
<h3>{ _t("Labs") }</h3>
|
<h3>{ _t("Labs") }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<p>{ _t("These are experimental features that may break in unexpected ways") }. { _t("Use with caution") }.</p>
|
<p>{ _t("These are experimental features that may break in unexpected ways") }. { _t("Use with caution") }.</p>
|
||||||
{features}
|
{ features }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -929,10 +994,10 @@ module.exports = React.createClass({
|
||||||
const platform = PlatformPeg.get();
|
const platform = PlatformPeg.get();
|
||||||
if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
|
if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
|
||||||
return <div>
|
return <div>
|
||||||
<h3>{_t('Updates')}</h3>
|
<h3>{ _t('Updates') }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<AccessibleButton className="mx_UserSettings_button" onClick={platform.startUpdateCheck}>
|
<AccessibleButton className="mx_UserSettings_button" onClick={platform.startUpdateCheck}>
|
||||||
{_t('Check for update')}
|
{ _t('Check for update') }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -958,7 +1023,7 @@ module.exports = React.createClass({
|
||||||
reject = (
|
reject = (
|
||||||
<AccessibleButton className="mx_UserSettings_button danger"
|
<AccessibleButton className="mx_UserSettings_button danger"
|
||||||
onClick={onClick}>
|
onClick={onClick}>
|
||||||
{_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})}
|
{ _t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length}) }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -966,7 +1031,7 @@ module.exports = React.createClass({
|
||||||
return <div>
|
return <div>
|
||||||
<h3>{ _t("Bulk Options") }</h3>
|
<h3>{ _t("Bulk Options") }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
{reject}
|
{ reject }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
@ -984,7 +1049,7 @@ module.exports = React.createClass({
|
||||||
defaultChecked={settings['auto-launch']}
|
defaultChecked={settings['auto-launch']}
|
||||||
onChange={this._onAutoLaunchChanged}
|
onChange={this._onAutoLaunchChanged}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="auto-launch">{_t('Start automatically after system login')}</label>
|
<label htmlFor="auto-launch">{ _t('Start automatically after system login') }</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -996,7 +1061,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_mapWebRtcDevicesToSpans: function(devices) {
|
_mapWebRtcDevicesToSpans: function(devices) {
|
||||||
return devices.map((device) => <span key={device.deviceId}>{device.label}</span>);
|
return devices.map((device) => <span key={device.deviceId}>{ device.label }</span>);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setAudioInput: function(deviceId) {
|
_setAudioInput: function(deviceId) {
|
||||||
|
@ -1032,15 +1097,15 @@ module.exports = React.createClass({
|
||||||
if (this.state.mediaDevices === false) {
|
if (this.state.mediaDevices === false) {
|
||||||
return (
|
return (
|
||||||
<p className="mx_UserSettings_link" onClick={this._requestMediaPermissions}>
|
<p className="mx_UserSettings_link" onClick={this._requestMediaPermissions}>
|
||||||
{_t('Missing Media Permissions, click here to request.')}
|
{ _t('Missing Media Permissions, click here to request.') }
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
} else if (!this.state.mediaDevices) return;
|
} else if (!this.state.mediaDevices) return;
|
||||||
|
|
||||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||||
|
|
||||||
let microphoneDropdown = <p>{_t('No Microphones detected')}</p>;
|
let microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
|
||||||
let webcamDropdown = <p>{_t('No Webcams detected')}</p>;
|
let webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
|
||||||
|
|
||||||
const defaultOption = {
|
const defaultOption = {
|
||||||
deviceId: '',
|
deviceId: '',
|
||||||
|
@ -1057,12 +1122,12 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
microphoneDropdown = <div>
|
microphoneDropdown = <div>
|
||||||
<h4>{_t('Microphone')}</h4>
|
<h4>{ _t('Microphone') }</h4>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className="mx_UserSettings_webRtcDevices_dropdown"
|
className="mx_UserSettings_webRtcDevices_dropdown"
|
||||||
value={this.state.activeAudioInput || defaultInput}
|
value={this.state.activeAudioInput || defaultInput}
|
||||||
onOptionChange={this._setAudioInput}>
|
onOptionChange={this._setAudioInput}>
|
||||||
{this._mapWebRtcDevicesToSpans(audioInputs)}
|
{ this._mapWebRtcDevicesToSpans(audioInputs) }
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -1077,25 +1142,25 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
webcamDropdown = <div>
|
webcamDropdown = <div>
|
||||||
<h4>{_t('Camera')}</h4>
|
<h4>{ _t('Camera') }</h4>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className="mx_UserSettings_webRtcDevices_dropdown"
|
className="mx_UserSettings_webRtcDevices_dropdown"
|
||||||
value={this.state.activeVideoInput || defaultInput}
|
value={this.state.activeVideoInput || defaultInput}
|
||||||
onOptionChange={this._setVideoInput}>
|
onOptionChange={this._setVideoInput}>
|
||||||
{this._mapWebRtcDevicesToSpans(videoInputs)}
|
{ this._mapWebRtcDevicesToSpans(videoInputs) }
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
{microphoneDropdown}
|
{ microphoneDropdown }
|
||||||
{webcamDropdown}
|
{ webcamDropdown }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderWebRtcSettings: function() {
|
_renderWebRtcSettings: function() {
|
||||||
return <div>
|
return <div>
|
||||||
<h3>{_t('VoIP')}</h3>
|
<h3>{ _t('VoIP') }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
{ WEBRTC_SETTINGS_LABELS.map(this._renderLocalSetting) }
|
{ WEBRTC_SETTINGS_LABELS.map(this._renderLocalSetting) }
|
||||||
{ this._renderWebRtcDeviceSettings() }
|
{ this._renderWebRtcDeviceSettings() }
|
||||||
|
@ -1161,7 +1226,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
||||||
<div className="mx_UserSettings_profileLabelCell">
|
<div className="mx_UserSettings_profileLabelCell">
|
||||||
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
|
<label htmlFor={id}>{ this.nameForMedium(val.medium) }</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_profileInputCell">
|
<div className="mx_UserSettings_profileInputCell">
|
||||||
<input type="text" key={val.address} id={id}
|
<input type="text" key={val.address} id={id}
|
||||||
|
@ -1169,7 +1234,7 @@ module.exports = React.createClass({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||||
<img src="img/cancel-small.svg" width="14" height="14" alt={ _t("Remove") }
|
<img src="img/cancel-small.svg" width="14" height="14" alt={_t("Remove")}
|
||||||
onClick={onRemoveClick} />
|
onClick={onRemoveClick} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1182,16 +1247,16 @@ module.exports = React.createClass({
|
||||||
addEmailSection = (
|
addEmailSection = (
|
||||||
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
|
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
|
||||||
<div className="mx_UserSettings_profileLabelCell">
|
<div className="mx_UserSettings_profileLabelCell">
|
||||||
<label>{_t('Email')}</label>
|
<label>{ _t('Email') }</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_profileInputCell">
|
<div className="mx_UserSettings_profileInputCell">
|
||||||
<EditableText
|
<EditableText
|
||||||
ref="add_email_input"
|
ref="add_email_input"
|
||||||
className="mx_UserSettings_editable"
|
className="mx_UserSettings_editable"
|
||||||
placeholderClassName="mx_UserSettings_threepidPlaceholder"
|
placeholderClassName="mx_UserSettings_threepidPlaceholder"
|
||||||
placeholder={ _t("Add email address") }
|
placeholder={_t("Add email address")}
|
||||||
blurToCancel={ false }
|
blurToCancel={false}
|
||||||
onValueChanged={ this._onAddEmailEditFinished } />
|
onValueChanged={this._onAddEmailEditFinished} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||||
<img src="img/plus.svg" width="14" height="14" alt={_t("Add")} onClick={this._addEmail} />
|
<img src="img/plus.svg" width="14" height="14" alt={_t("Add")} onClick={this._addEmail} />
|
||||||
|
@ -1239,8 +1304,8 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserSettings">
|
<div className="mx_UserSettings">
|
||||||
<SimpleRoomHeader
|
<SimpleRoomHeader
|
||||||
title={ _t("Settings") }
|
title={_t("Settings")}
|
||||||
onCancelClick={ this.props.onClose }
|
onCancelClick={this.props.onClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GeminiScrollbar className="mx_UserSettings_body"
|
<GeminiScrollbar className="mx_UserSettings_body"
|
||||||
|
@ -1258,21 +1323,25 @@ module.exports = React.createClass({
|
||||||
<ChangeDisplayName />
|
<ChangeDisplayName />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{threepidsSection}
|
{ threepidsSection }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx_UserSettings_avatarPicker">
|
<div className="mx_UserSettings_avatarPicker">
|
||||||
<div onClick={ this.onAvatarPickerClick }>
|
<div className="mx_UserSettings_avatarPicker_remove" onClick={this.onAvatarRemoveClick}>
|
||||||
|
<img src="img/cancel.svg" width="15" height="15"
|
||||||
|
alt={_t("Remove avatar")} title={_t("Remove avatar")} />
|
||||||
|
</div>
|
||||||
|
<div onClick={this.onAvatarPickerClick} className="mx_UserSettings_avatarPicker_imgContainer">
|
||||||
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
||||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
|
showUploadSection={false} className="mx_UserSettings_avatarPicker_img" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_avatarPicker_edit">
|
<div className="mx_UserSettings_avatarPicker_edit">
|
||||||
<label htmlFor="avatarInput" ref="file_label">
|
<label htmlFor="avatarInput" ref="file_label">
|
||||||
<img src="img/camera.svg" className="mx_filterFlipColor"
|
<img src="img/camera.svg" className="mx_filterFlipColor"
|
||||||
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
|
alt={_t("Upload avatar")} title={_t("Upload avatar")}
|
||||||
width="17" height="15" />
|
width="17" height="15" />
|
||||||
</label>
|
</label>
|
||||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
|
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1289,36 +1358,37 @@ module.exports = React.createClass({
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
{accountJsx}
|
{ accountJsx }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{this._renderReferral()}
|
{ this._renderReferral() }
|
||||||
|
|
||||||
{notificationArea}
|
{ notificationArea }
|
||||||
|
|
||||||
{this._renderUserInterfaceSettings()}
|
{ this._renderUserInterfaceSettings() }
|
||||||
{this._renderLabs()}
|
{ this._renderLabs() }
|
||||||
{this._renderWebRtcSettings()}
|
{ this._renderWebRtcSettings() }
|
||||||
{this._renderDevicesPanel()}
|
{ this._renderDevicesPanel() }
|
||||||
{this._renderCryptoInfo()}
|
{ this._renderCryptoInfo() }
|
||||||
{this._renderBulkOptions()}
|
{ this._renderIgnoredUsers() }
|
||||||
{this._renderBugReport()}
|
{ this._renderBulkOptions() }
|
||||||
|
{ this._renderBugReport() }
|
||||||
|
|
||||||
{PlatformPeg.get().isElectron() && this._renderElectronSettings()}
|
{ PlatformPeg.get().isElectron() && this._renderElectronSettings() }
|
||||||
|
|
||||||
{this._renderAnalyticsControl()}
|
{ this._renderAnalyticsControl() }
|
||||||
|
|
||||||
<h3>{ _t("Advanced") }</h3>
|
<h3>{ _t("Advanced") }</h3>
|
||||||
|
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<div className="mx_UserSettings_advanced">
|
<div className="mx_UserSettings_advanced">
|
||||||
{ _t("Logged in as:") } {this._me}
|
{ _t("Logged in as:") } { this._me }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_advanced">
|
<div className="mx_UserSettings_advanced">
|
||||||
{_t('Access Token:')}
|
{ _t('Access Token:') }
|
||||||
<span className="mx_UserSettings_advanced_spoiler"
|
<span className="mx_UserSettings_advanced_spoiler"
|
||||||
onClick={this._showSpoiler}
|
onClick={this._showSpoiler}
|
||||||
data-spoiler={ MatrixClientPeg.get().getAccessToken() }>
|
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
|
||||||
<{ _t("click to reveal") }>
|
<{ _t("click to reveal") }>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1329,23 +1399,23 @@ module.exports = React.createClass({
|
||||||
{ _t("Identity Server is") } { MatrixClientPeg.get().getIdentityServerUrl() }
|
{ _t("Identity Server is") } { MatrixClientPeg.get().getIdentityServerUrl() }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_advanced">
|
<div className="mx_UserSettings_advanced">
|
||||||
{_t('matrix-react-sdk version:')} {(REACT_SDK_VERSION !== '<local>')
|
{ _t('matrix-react-sdk version:') } { (REACT_SDK_VERSION !== '<local>')
|
||||||
? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
|
? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
|
||||||
: REACT_SDK_VERSION
|
: REACT_SDK_VERSION
|
||||||
}<br/>
|
}<br />
|
||||||
{_t('riot-web version:')} {(this.state.vectorVersion !== undefined)
|
{ _t('riot-web version:') } { (this.state.vectorVersion !== undefined)
|
||||||
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
||||||
: 'unknown'
|
: 'unknown'
|
||||||
}<br/>
|
}<br />
|
||||||
{ _t("olm version:") } {olmVersionString}<br/>
|
{ _t("olm version:") } { olmVersionString }<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{this._renderCheckUpdate()}
|
{ this._renderCheckUpdate() }
|
||||||
|
|
||||||
{this._renderClearCache()}
|
{ this._renderClearCache() }
|
||||||
|
|
||||||
{this._renderDeactivateAccount()}
|
{ this._renderDeactivateAccount() }
|
||||||
|
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbar>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 New Vector 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,13 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
const React = require('react');
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
var sdk = require('../../../index');
|
const sdk = require('../../../index');
|
||||||
var Modal = require("../../../Modal");
|
const Modal = require("../../../Modal");
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
|
||||||
var PasswordReset = require("../../../PasswordReset");
|
const PasswordReset = require("../../../PasswordReset");
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'ForgotPassword',
|
displayName: 'ForgotPassword',
|
||||||
|
@ -34,30 +35,30 @@ module.exports = React.createClass({
|
||||||
customIsUrl: React.PropTypes.string,
|
customIsUrl: React.PropTypes.string,
|
||||||
onLoginClick: React.PropTypes.func,
|
onLoginClick: React.PropTypes.func,
|
||||||
onRegisterClick: React.PropTypes.func,
|
onRegisterClick: React.PropTypes.func,
|
||||||
onComplete: React.PropTypes.func.isRequired
|
onComplete: React.PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||||
progress: null
|
progress: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
submitPasswordReset: function(hsUrl, identityUrl, email, password) {
|
submitPasswordReset: function(hsUrl, identityUrl, email, password) {
|
||||||
this.setState({
|
this.setState({
|
||||||
progress: "sending_email"
|
progress: "sending_email",
|
||||||
});
|
});
|
||||||
this.reset = new PasswordReset(hsUrl, identityUrl);
|
this.reset = new PasswordReset(hsUrl, identityUrl);
|
||||||
this.reset.resetPassword(email, password).done(() => {
|
this.reset.resetPassword(email, password).done(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
progress: "sent_email"
|
progress: "sent_email",
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
|
this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
|
||||||
this.setState({
|
this.setState({
|
||||||
progress: null
|
progress: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -80,15 +81,12 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (!this.state.email) {
|
if (!this.state.email) {
|
||||||
this.showErrorDialog(_t('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(_t('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(_t('New passwords must match each other.'));
|
this.showErrorDialog(_t('New passwords must match each other.'));
|
||||||
}
|
} else {
|
||||||
else {
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
||||||
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
|
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
|
||||||
title: _t('Warning!'),
|
title: _t('Warning!'),
|
||||||
description:
|
description:
|
||||||
|
@ -98,7 +96,7 @@ module.exports = React.createClass({
|
||||||
'end-to-end encryption keys on all devices, ' +
|
'end-to-end encryption keys on all devices, ' +
|
||||||
'making encrypted chat history unreadable, ' +
|
'making encrypted chat history unreadable, ' +
|
||||||
'unless you first export your room keys and re-import ' +
|
'unless you first export your room keys and re-import ' +
|
||||||
'them afterwards. In future this will be improved.'
|
'them afterwards. In future this will be improved.',
|
||||||
) }
|
) }
|
||||||
</div>,
|
</div>,
|
||||||
button: _t('Continue'),
|
button: _t('Continue'),
|
||||||
|
@ -106,13 +104,13 @@ module.exports = React.createClass({
|
||||||
<button className="mx_Dialog_primary"
|
<button className="mx_Dialog_primary"
|
||||||
onClick={this._onExportE2eKeysClicked}>
|
onClick={this._onExportE2eKeysClicked}>
|
||||||
{ _t('Export E2E room keys') }
|
{ _t('Export E2E room keys') }
|
||||||
</button>
|
</button>,
|
||||||
],
|
],
|
||||||
onFinished: (confirmed) => {
|
onFinished: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.submitPasswordReset(
|
this.submitPasswordReset(
|
||||||
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
|
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
|
||||||
this.state.email, this.state.password
|
this.state.email, this.state.password,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -132,24 +130,23 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onInputChanged: function(stateKey, ev) {
|
onInputChanged: function(stateKey, ev) {
|
||||||
this.setState({
|
this.setState({
|
||||||
[stateKey]: ev.target.value
|
[stateKey]: ev.target.value,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onHsUrlChanged: function(newHsUrl) {
|
onServerConfigChange: function(config) {
|
||||||
this.setState({
|
const newState = {};
|
||||||
enteredHomeserverUrl: newHsUrl
|
if (config.hsUrl !== undefined) {
|
||||||
});
|
newState.enteredHomeserverUrl = config.hsUrl;
|
||||||
},
|
}
|
||||||
|
if (config.isUrl !== undefined) {
|
||||||
onIsUrlChanged: function(newIsUrl) {
|
newState.enteredIdentityServerUrl = config.isUrl;
|
||||||
this.setState({
|
}
|
||||||
enteredIdentityServerUrl: newIsUrl
|
this.setState(newState);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
showErrorDialog: function(body, title) {
|
showErrorDialog: function(body, title) {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
|
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
|
||||||
title: title,
|
title: title,
|
||||||
description: body,
|
description: body,
|
||||||
|
@ -157,37 +154,34 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
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 ServerConfig = sdk.getComponent("login.ServerConfig");
|
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||||
var Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
var resetPasswordJsx;
|
let resetPasswordJsx;
|
||||||
|
|
||||||
if (this.state.progress === "sending_email") {
|
if (this.state.progress === "sending_email") {
|
||||||
resetPasswordJsx = <Spinner />;
|
resetPasswordJsx = <Spinner />;
|
||||||
}
|
} else if (this.state.progress === "sent_email") {
|
||||||
else if (this.state.progress === "sent_email") {
|
|
||||||
resetPasswordJsx = (
|
resetPasswordJsx = (
|
||||||
<div>
|
<div>
|
||||||
{ _t('An email has been sent to') } {this.state.email}. { _t('Once you've followed the link it contains, click below') }.
|
{ _t('An email has been sent to') } { this.state.email }. { _t("Once you've followed 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={ _t('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>{ _t('Your password has been reset') }.</p>
|
<p>{ _t('Your password has been reset') }.</p>
|
||||||
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
|
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
|
||||||
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||||
value={ _t('Return to login screen') } />
|
value={_t('Return to login screen')} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
resetPasswordJsx = (
|
resetPasswordJsx = (
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_Login_prompt">
|
<div className="mx_Login_prompt">
|
||||||
|
@ -199,21 +193,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={ _t('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={ _t('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={ _t('Confirm your new password') } />
|
placeholder={_t('Confirm your new password')} />
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_submit" type="submit" value={ _t('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}
|
||||||
|
@ -221,13 +215,12 @@ module.exports = React.createClass({
|
||||||
defaultIsUrl={this.props.defaultIsUrl}
|
defaultIsUrl={this.props.defaultIsUrl}
|
||||||
customHsUrl={this.props.customHsUrl}
|
customHsUrl={this.props.customHsUrl}
|
||||||
customIsUrl={this.props.customIsUrl}
|
customIsUrl={this.props.customIsUrl}
|
||||||
onHsUrlChanged={this.onHsUrlChanged}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
onIsUrlChanged={this.onIsUrlChanged}
|
delayTimeMs={0} />
|
||||||
delayTimeMs={0}/>
|
|
||||||
<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="#">
|
||||||
{_t('Return to login screen')}
|
{ _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="#">
|
||||||
{ _t('Create an account') }
|
{ _t('Create an account') }
|
||||||
|
@ -243,9 +236,9 @@ module.exports = React.createClass({
|
||||||
<div className="mx_Login">
|
<div className="mx_Login">
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
<LoginHeader />
|
<LoginHeader />
|
||||||
{resetPasswordJsx}
|
{ resetPasswordJsx }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -134,7 +134,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_onLoginAsGuestClick: function() {
|
_onLoginAsGuestClick: function() {
|
||||||
var self = this;
|
const self = this;
|
||||||
self.setState({
|
self.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
|
@ -156,7 +156,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
}).finally(function() {
|
}).finally(function() {
|
||||||
self.setState({
|
self.setState({
|
||||||
busy: false
|
busy: false,
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
},
|
},
|
||||||
|
@ -183,8 +183,8 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onServerConfigChange: function(config) {
|
onServerConfigChange: function(config) {
|
||||||
var self = this;
|
const self = this;
|
||||||
let newState = {
|
const newState = {
|
||||||
errorText: null, // reset err messages
|
errorText: null, // reset err messages
|
||||||
};
|
};
|
||||||
if (config.hsUrl !== undefined) {
|
if (config.hsUrl !== undefined) {
|
||||||
|
@ -199,13 +199,13 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_initLoginLogic: function(hsUrl, isUrl) {
|
_initLoginLogic: function(hsUrl, isUrl) {
|
||||||
var self = this;
|
const self = this;
|
||||||
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
|
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
|
||||||
isUrl = isUrl || this.state.enteredIdentityServerUrl;
|
isUrl = isUrl || this.state.enteredIdentityServerUrl;
|
||||||
|
|
||||||
var fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
const fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||||
|
|
||||||
var loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||||
});
|
});
|
||||||
this._loginLogic = loginLogic;
|
this._loginLogic = loginLogic;
|
||||||
|
@ -259,15 +259,15 @@ module.exports = React.createClass({
|
||||||
{ _tJsx("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>enable unsafe scripts</a>.",
|
"Either use HTTPS or <a>enable unsafe scripts</a>.",
|
||||||
/<a>(.*?)<\/a>/,
|
/<a>(.*?)<\/a>/,
|
||||||
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; }
|
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; },
|
||||||
)}
|
) }
|
||||||
</span>;
|
</span>;
|
||||||
} else {
|
} else {
|
||||||
errorText = <span>
|
errorText = <span>
|
||||||
{ _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.",
|
{ _tJsx("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
|
||||||
/<a>(.*?)<\/a>/,
|
/<a>(.*?)<\/a>/,
|
||||||
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; }
|
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; },
|
||||||
)}
|
) }
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -290,6 +290,7 @@ module.exports = React.createClass({
|
||||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||||
loginIncorrect={this.state.loginIncorrect}
|
loginIncorrect={this.state.loginIncorrect}
|
||||||
|
hsUrl={this.state.enteredHomeserverUrl}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'm.login.cas':
|
case 'm.login.cas':
|
||||||
|
@ -303,7 +304,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ _t('Sorry, this homeserver is using a login which is not recognised ')}({step})
|
{ _t('Sorry, this homeserver is using a login which is not recognised ') }({ step })
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -333,19 +334,19 @@ module.exports = React.createClass({
|
||||||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||||
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||||
|
|
||||||
var loginAsGuestJsx;
|
let 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="#">
|
||||||
{ _t('Login as guest')}
|
{ _t('Login as guest') }
|
||||||
</a>;
|
</a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var returnToAppJsx;
|
let returnToAppJsx;
|
||||||
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="#">
|
||||||
{ _t('Return to app')}
|
{ _t('Return to app') }
|
||||||
</a>;
|
</a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,7 +355,7 @@ module.exports = React.createClass({
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
<LoginHeader />
|
<LoginHeader />
|
||||||
<div>
|
<div>
|
||||||
<h2>{ _t('Sign in')}
|
<h2>{ _t('Sign in') }
|
||||||
{ loader }
|
{ loader }
|
||||||
</h2>
|
</h2>
|
||||||
{ this.componentForStep(this.state.currentFlow) }
|
{ this.componentForStep(this.state.currentFlow) }
|
||||||
|
@ -365,12 +366,12 @@ module.exports = React.createClass({
|
||||||
defaultHsUrl={this.props.defaultHsUrl}
|
defaultHsUrl={this.props.defaultHsUrl}
|
||||||
defaultIsUrl={this.props.defaultIsUrl}
|
defaultIsUrl={this.props.defaultIsUrl}
|
||||||
onServerConfigChange={this.onServerConfigChange}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
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="#">
|
||||||
{ _t('Create an account')}
|
{ _t('Create an account') }
|
||||||
</a>
|
</a>
|
||||||
{ loginAsGuestJsx }
|
{ loginAsGuestJsx }
|
||||||
{ returnToAppJsx }
|
{ returnToAppJsx }
|
||||||
|
@ -380,5 +381,5 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,14 +25,14 @@ module.exports = React.createClass({
|
||||||
displayName: 'PostRegistration',
|
displayName: 'PostRegistration',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onComplete: React.PropTypes.func.isRequired
|
onComplete: React.PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
errorString: null,
|
errorString: null,
|
||||||
busy: false
|
busy: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -40,26 +40,26 @@ module.exports = React.createClass({
|
||||||
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
|
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
|
||||||
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects
|
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects
|
||||||
// the URL to be passed to you (because it's also used for room avatars).
|
// the URL to be passed to you (because it's also used for room avatars).
|
||||||
var cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
this.setState({busy: true});
|
this.setState({busy: true});
|
||||||
var self = this;
|
const self = this;
|
||||||
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
|
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
|
||||||
self.setState({
|
self.setState({
|
||||||
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
|
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
|
||||||
busy: false
|
busy: false,
|
||||||
});
|
});
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
self.setState({
|
self.setState({
|
||||||
errorString: _t("Failed to fetch avatar URL"),
|
errorString: _t("Failed to fetch avatar URL"),
|
||||||
busy: false
|
busy: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
||||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||||
var LoginHeader = sdk.getComponent('login.LoginHeader');
|
const LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||||
return (
|
return (
|
||||||
<div className="mx_Login">
|
<div className="mx_Login">
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
|
@ -71,10 +71,10 @@ module.exports = React.createClass({
|
||||||
<ChangeAvatar
|
<ChangeAvatar
|
||||||
initialAvatarUrl={this.state.avatarUrl} />
|
initialAvatarUrl={this.state.avatarUrl} />
|
||||||
<button onClick={this.props.onComplete}>{ _t('Continue') }</button>
|
<button onClick={this.props.onComplete}>{ _t('Continue') }</button>
|
||||||
{this.state.errorString}
|
{ this.state.errorString }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -57,7 +57,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// registration shouldn't know or care how login is done.
|
// registration shouldn't know or care how login is done.
|
||||||
onLoginClick: React.PropTypes.func.isRequired,
|
onLoginClick: React.PropTypes.func.isRequired,
|
||||||
onCancelClick: React.PropTypes.func
|
onCancelClick: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -121,7 +121,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onServerConfigChange: function(config) {
|
onServerConfigChange: function(config) {
|
||||||
let newState = {};
|
const newState = {};
|
||||||
if (config.hsUrl !== undefined) {
|
if (config.hsUrl !== undefined) {
|
||||||
newState.hsUrl = config.hsUrl;
|
newState.hsUrl = config.hsUrl;
|
||||||
}
|
}
|
||||||
|
@ -195,7 +195,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
this._rtsClient.getTeam(teamToken).then((team) => {
|
this._rtsClient.getTeam(teamToken).then((team) => {
|
||||||
console.log(
|
console.log(
|
||||||
`User successfully registered with team ${team.name}`
|
`User successfully registered with team ${team.name}`,
|
||||||
);
|
);
|
||||||
if (!team.rooms) {
|
if (!team.rooms) {
|
||||||
return;
|
return;
|
||||||
|
@ -223,7 +223,7 @@ module.exports = React.createClass({
|
||||||
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((cli) => {
|
}).then((cli) => {
|
||||||
return this._setupPushers(cli);
|
return this._setupPushers(cli);
|
||||||
|
@ -253,7 +253,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onFormValidationFailed: function(errCode) {
|
onFormValidationFailed: function(errCode) {
|
||||||
var errMsg;
|
let errMsg;
|
||||||
switch (errCode) {
|
switch (errCode) {
|
||||||
case "RegistrationForm.ERR_PASSWORD_MISSING":
|
case "RegistrationForm.ERR_PASSWORD_MISSING":
|
||||||
errMsg = _t('Missing password.');
|
errMsg = _t('Missing password.');
|
||||||
|
@ -282,7 +282,7 @@ module.exports = React.createClass({
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: errMsg
|
errorText: errMsg,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -316,7 +316,7 @@ module.exports = React.createClass({
|
||||||
emailAddress: this.state.formVals.email,
|
emailAddress: this.state.formVals.email,
|
||||||
phoneCountry: this.state.formVals.phoneCountry,
|
phoneCountry: this.state.formVals.phoneCountry,
|
||||||
phoneNumber: this.state.formVals.phoneNumber,
|
phoneNumber: this.state.formVals.phoneNumber,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
@ -346,7 +346,7 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
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>;
|
||||||
}
|
}
|
||||||
registerBody = (
|
registerBody = (
|
||||||
<div>
|
<div>
|
||||||
|
@ -362,7 +362,7 @@ module.exports = React.createClass({
|
||||||
onRegisterClick={this.onFormSubmit}
|
onRegisterClick={this.onFormSubmit}
|
||||||
onTeamSelected={this.onTeamSelected}
|
onTeamSelected={this.onTeamSelected}
|
||||||
/>
|
/>
|
||||||
{errorSection}
|
{ errorSection }
|
||||||
<ServerConfig ref="serverConfig"
|
<ServerConfig ref="serverConfig"
|
||||||
withToggleButton={true}
|
withToggleButton={true}
|
||||||
customHsUrl={this.props.customHsUrl}
|
customHsUrl={this.props.customHsUrl}
|
||||||
|
@ -380,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="#">
|
||||||
{_t('Return to app')}
|
{ _t('Return to app') }
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -393,15 +393,15 @@ module.exports = React.createClass({
|
||||||
this.state.teamSelected.domain + "/icon.png" :
|
this.state.teamSelected.domain + "/icon.png" :
|
||||||
null}
|
null}
|
||||||
/>
|
/>
|
||||||
<h2>{_t('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="#">
|
||||||
{_t('I already have an account')}
|
{ _t('I already have an account') }
|
||||||
</a>
|
</a>
|
||||||
{returnToAppJsx}
|
{ returnToAppJsx }
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
import React from 'react';
|
||||||
|
import AvatarLogic from '../../../Avatar';
|
||||||
var React = require('react');
|
|
||||||
var AvatarLogic = require("../../../Avatar");
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
|
@ -34,7 +32,7 @@ module.exports = React.createClass({
|
||||||
height: React.PropTypes.number,
|
height: React.PropTypes.number,
|
||||||
// XXX resizeMethod not actually used.
|
// 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
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -42,7 +40,7 @@ module.exports = React.createClass({
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
defaultToInitialLetter: true
|
defaultToInitialLetter: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -52,15 +50,14 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
// work out if we need to call setState (if the image URLs array has changed)
|
// work out if we need to call setState (if the image URLs array has changed)
|
||||||
var newState = this._getState(nextProps);
|
const newState = this._getState(nextProps);
|
||||||
var newImageUrls = newState.imageUrls;
|
const newImageUrls = newState.imageUrls;
|
||||||
var oldImageUrls = this.state.imageUrls;
|
const oldImageUrls = this.state.imageUrls;
|
||||||
if (newImageUrls.length !== oldImageUrls.length) {
|
if (newImageUrls.length !== oldImageUrls.length) {
|
||||||
this.setState(newState); // detected a new entry
|
this.setState(newState); // detected a new entry
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// check each one to see if they are the same
|
// check each one to see if they are the same
|
||||||
for (var i = 0; i < newImageUrls.length; i++) {
|
for (let i = 0; i < newImageUrls.length; i++) {
|
||||||
if (oldImageUrls[i] !== newImageUrls[i]) {
|
if (oldImageUrls[i] !== newImageUrls[i]) {
|
||||||
this.setState(newState); // detected a diff
|
this.setState(newState); // detected a diff
|
||||||
break;
|
break;
|
||||||
|
@ -73,31 +70,31 @@ module.exports = React.createClass({
|
||||||
// work out the full set of urls to try to load. This is formed like so:
|
// work out the full set of urls to try to load. This is formed like so:
|
||||||
// imageUrls: [ props.url, props.urls, default image ]
|
// imageUrls: [ props.url, props.urls, default image ]
|
||||||
|
|
||||||
var urls = props.urls || [];
|
const urls = props.urls || [];
|
||||||
if (props.url) {
|
if (props.url) {
|
||||||
urls.unshift(props.url); // put in urls[0]
|
urls.unshift(props.url); // put in urls[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultImageUrl = null;
|
let defaultImageUrl = null;
|
||||||
if (props.defaultToInitialLetter) {
|
if (props.defaultToInitialLetter) {
|
||||||
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
|
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
|
||||||
props.idName || props.name
|
props.idName || props.name,
|
||||||
);
|
);
|
||||||
urls.push(defaultImageUrl); // lowest priority
|
urls.push(defaultImageUrl); // lowest priority
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
imageUrls: urls,
|
imageUrls: urls,
|
||||||
defaultImageUrl: defaultImageUrl,
|
defaultImageUrl: defaultImageUrl,
|
||||||
urlsIndex: 0
|
urlsIndex: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
onError: function(ev) {
|
onError: function(ev) {
|
||||||
var nextIndex = this.state.urlsIndex + 1;
|
const nextIndex = this.state.urlsIndex + 1;
|
||||||
if (nextIndex < this.state.imageUrls.length) {
|
if (nextIndex < this.state.imageUrls.length) {
|
||||||
// try the next one
|
// try the next one
|
||||||
this.setState({
|
this.setState({
|
||||||
urlsIndex: nextIndex
|
urlsIndex: nextIndex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -111,32 +108,32 @@ module.exports = React.createClass({
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
var idx = 0;
|
let idx = 0;
|
||||||
var initial = name[0];
|
const initial = name[0];
|
||||||
if ((initial === '@' || initial === '#') && name[1]) {
|
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
|
||||||
idx++;
|
idx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// string.codePointAt(0) would do this, but that isn't supported by
|
// string.codePointAt(0) would do this, but that isn't supported by
|
||||||
// some browsers (notably PhantomJS).
|
// some browsers (notably PhantomJS).
|
||||||
var chars = 1;
|
let chars = 1;
|
||||||
var first = name.charCodeAt(idx);
|
const first = name.charCodeAt(idx);
|
||||||
|
|
||||||
// check if it’s the start of a surrogate pair
|
// check if it’s the start of a surrogate pair
|
||||||
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
|
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
|
||||||
var second = name.charCodeAt(idx+1);
|
const second = name.charCodeAt(idx+1);
|
||||||
if (second >= 0xDC00 && second <= 0xDFFF) {
|
if (second >= 0xDC00 && second <= 0xDFFF) {
|
||||||
chars++;
|
chars++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstChar = name.substring(idx, idx+chars);
|
const firstChar = name.substring(idx, idx+chars);
|
||||||
return firstChar.toUpperCase();
|
return firstChar.toUpperCase();
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
var imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
name, idName, title, url, urls, width, height, resizeMethod,
|
name, idName, title, url, urls, width, height, resizeMethod,
|
||||||
|
@ -152,7 +149,7 @@ module.exports = React.createClass({
|
||||||
width: width + "px",
|
width: width + "px",
|
||||||
lineHeight: height + "px" }}
|
lineHeight: height + "px" }}
|
||||||
>
|
>
|
||||||
{initialLetter}
|
{ initialLetter }
|
||||||
</EmojiText>
|
</EmojiText>
|
||||||
);
|
);
|
||||||
const imgNode = (
|
const imgNode = (
|
||||||
|
@ -165,15 +162,15 @@ module.exports = React.createClass({
|
||||||
<AccessibleButton element='span' className="mx_BaseAvatar"
|
<AccessibleButton element='span' className="mx_BaseAvatar"
|
||||||
onClick={onClick} {...otherProps}
|
onClick={onClick} {...otherProps}
|
||||||
>
|
>
|
||||||
{textNode}
|
{ textNode }
|
||||||
{imgNode}
|
{ imgNode }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="mx_BaseAvatar" {...otherProps}>
|
<span className="mx_BaseAvatar" {...otherProps}>
|
||||||
{textNode}
|
{ textNode }
|
||||||
{imgNode}
|
{ imgNode }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -198,5 +195,5 @@ module.exports = React.createClass({
|
||||||
{...otherProps} />
|
{...otherProps} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,9 +16,9 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
const React = require('react');
|
||||||
var Avatar = require('../../../Avatar');
|
const Avatar = require('../../../Avatar');
|
||||||
var sdk = require("../../../index");
|
const sdk = require("../../../index");
|
||||||
const dispatcher = require("../../../dispatcher");
|
const dispatcher = require("../../../dispatcher");
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -63,14 +63,14 @@ module.exports = React.createClass({
|
||||||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||||
props.width,
|
props.width,
|
||||||
props.height,
|
props.height,
|
||||||
props.resizeMethod)
|
props.resizeMethod),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
|
|
||||||
var {member, onClick, viewUserOnClick, ...otherProps} = this.props;
|
let {member, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||||
|
|
||||||
if (viewUserOnClick) {
|
if (viewUserOnClick) {
|
||||||
onClick = () => {
|
onClick = () => {
|
||||||
|
@ -83,7 +83,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
||||||
idName={member.userId} url={this.state.imageUrl} onClick={onClick}/>
|
idName={member.userId} url={this.state.imageUrl} onClick={onClick} />
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,11 +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.
|
||||||
*/
|
*/
|
||||||
var React = require('react');
|
import React from "react";
|
||||||
var ContentRepo = require("matrix-js-sdk").ContentRepo;
|
import {ContentRepo} from "matrix-js-sdk";
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
var Avatar = require('../../../Avatar');
|
import sdk from "../../../index";
|
||||||
var sdk = require("../../../index");
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomAvatar',
|
displayName: 'RoomAvatar',
|
||||||
|
@ -30,7 +29,7 @@ module.exports = React.createClass({
|
||||||
oobData: React.PropTypes.object,
|
oobData: React.PropTypes.object,
|
||||||
width: React.PropTypes.number,
|
width: React.PropTypes.number,
|
||||||
height: React.PropTypes.number,
|
height: React.PropTypes.number,
|
||||||
resizeMethod: React.PropTypes.string
|
resizeMethod: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -44,13 +43,13 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
urls: this.getImageUrls(this.props)
|
urls: this.getImageUrls(this.props),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
componentWillReceiveProps: function(newProps) {
|
||||||
this.setState({
|
this.setState({
|
||||||
urls: this.getImageUrls(newProps)
|
urls: this.getImageUrls(newProps),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -61,11 +60,10 @@ module.exports = React.createClass({
|
||||||
props.oobData.avatarUrl,
|
props.oobData.avatarUrl,
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod
|
props.resizeMethod,
|
||||||
), // highest priority
|
), // highest priority
|
||||||
this.getRoomAvatarUrl(props),
|
this.getRoomAvatarUrl(props),
|
||||||
this.getOneToOneAvatar(props),
|
this.getOneToOneAvatar(props), // lowest priority
|
||||||
this.getFallbackAvatar(props) // lowest priority
|
|
||||||
].filter(function(url) {
|
].filter(function(url) {
|
||||||
return (url != null && url != "");
|
return (url != null && url != "");
|
||||||
});
|
});
|
||||||
|
@ -79,17 +77,17 @@ module.exports = React.createClass({
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
getOneToOneAvatar: function(props) {
|
getOneToOneAvatar: function(props) {
|
||||||
if (!props.room) return null;
|
if (!props.room) return null;
|
||||||
|
|
||||||
var mlist = props.room.currentState.members;
|
const mlist = props.room.currentState.members;
|
||||||
var userIds = [];
|
const userIds = [];
|
||||||
// for .. in optimisation to return early if there are >2 keys
|
// for .. in optimisation to return early if there are >2 keys
|
||||||
for (var uid in mlist) {
|
for (const uid in mlist) {
|
||||||
if (mlist.hasOwnProperty(uid)) {
|
if (mlist.hasOwnProperty(uid)) {
|
||||||
userIds.push(uid);
|
userIds.push(uid);
|
||||||
}
|
}
|
||||||
|
@ -99,7 +97,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userIds.length == 2) {
|
if (userIds.length == 2) {
|
||||||
var theOtherGuy = null;
|
let theOtherGuy = null;
|
||||||
if (mlist[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
if (mlist[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
||||||
theOtherGuy = mlist[userIds[1]];
|
theOtherGuy = mlist[userIds[1]];
|
||||||
} else {
|
} else {
|
||||||
|
@ -110,7 +108,7 @@ module.exports = React.createClass({
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
} else if (userIds.length == 1) {
|
} else if (userIds.length == 1) {
|
||||||
return mlist[userIds[0]].getAvatarUrl(
|
return mlist[userIds[0]].getAvatarUrl(
|
||||||
|
@ -118,37 +116,24 @@ module.exports = React.createClass({
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getFallbackAvatar: function(props) {
|
|
||||||
let roomId = null;
|
|
||||||
if (props.oobData && props.oobData.roomId) {
|
|
||||||
roomId = this.props.oobData.roomId;
|
|
||||||
} else if (props.room) {
|
|
||||||
roomId = props.room.roomId;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Avatar.defaultAvatarUrlForString(roomId);
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
|
|
||||||
var {room, oobData, ...otherProps} = this.props;
|
const {room, oobData, ...otherProps} = this.props;
|
||||||
|
|
||||||
var roomName = room ? room.name : oobData.name;
|
const roomName = room ? room.name : oobData.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseAvatar {...otherProps} name={roomName}
|
<BaseAvatar {...otherProps} name={roomName}
|
||||||
idName={room ? room.roomId : null}
|
idName={room ? room.roomId : null}
|
||||||
urls={this.state.urls} />
|
urls={this.state.urls} />
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -36,7 +36,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<button className="mx_CreateRoomButton" onClick={this.onClick}>{_t("Create Room")}</button>
|
<button className="mx_CreateRoomButton" onClick={this.onClick}>{ _t("Create Room") }</button>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,10 +16,10 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
const React = require('react');
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
var Presets = {
|
const Presets = {
|
||||||
PrivateChat: "private_chat",
|
PrivateChat: "private_chat",
|
||||||
PublicChat: "public_chat",
|
PublicChat: "public_chat",
|
||||||
Custom: "custom",
|
Custom: "custom",
|
||||||
|
@ -29,7 +29,7 @@ module.exports = React.createClass({
|
||||||
displayName: 'CreateRoomPresets',
|
displayName: 'CreateRoomPresets',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onChange: React.PropTypes.func,
|
onChange: React.PropTypes.func,
|
||||||
preset: React.PropTypes.string
|
preset: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
Presets: Presets,
|
Presets: Presets,
|
||||||
|
@ -47,10 +47,10 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
|
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
|
||||||
<option value={this.Presets.PrivateChat}>{_t("Private Chat")}</option>
|
<option value={this.Presets.PrivateChat}>{ _t("Private Chat") }</option>
|
||||||
<option value={this.Presets.PublicChat}>{_t("Public Chat")}</option>
|
<option value={this.Presets.PublicChat}>{ _t("Public Chat") }</option>
|
||||||
<option value={this.Presets.Custom}>{_t("Custom")}</option>
|
<option value={this.Presets.Custom}>{ _t("Custom") }</option>
|
||||||
</select>
|
</select>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require('react');
|
const React = require('react');
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -35,10 +35,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getAliasLocalpart: function() {
|
getAliasLocalpart: function() {
|
||||||
var room_alias = this.props.alias;
|
let room_alias = this.props.alias;
|
||||||
|
|
||||||
if (room_alias && this.props.homeserver) {
|
if (room_alias && this.props.homeserver) {
|
||||||
var suffix = ":" + this.props.homeserver;
|
const suffix = ":" + this.props.homeserver;
|
||||||
if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) {
|
if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) {
|
||||||
room_alias = room_alias.slice(1, -suffix.length);
|
room_alias = room_alias.slice(1, -suffix.length);
|
||||||
}
|
}
|
||||||
|
@ -52,22 +52,22 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onFocus: function(ev) {
|
onFocus: function(ev) {
|
||||||
var target = ev.target;
|
const target = ev.target;
|
||||||
var curr_val = ev.target.value;
|
const curr_val = ev.target.value;
|
||||||
|
|
||||||
if (this.props.homeserver) {
|
if (this.props.homeserver) {
|
||||||
if (curr_val == "") {
|
if (curr_val == "") {
|
||||||
var self = this;
|
const self = this;
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
target.value = "#:" + self.props.homeserver;
|
target.value = "#:" + self.props.homeserver;
|
||||||
target.setSelectionRange(1, 1);
|
target.setSelectionRange(1, 1);
|
||||||
}, 0);
|
}, 0);
|
||||||
} else {
|
} else {
|
||||||
var suffix = ":" + this.props.homeserver;
|
const suffix = ":" + this.props.homeserver;
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
target.setSelectionRange(
|
target.setSelectionRange(
|
||||||
curr_val.startsWith("#") ? 1 : 0,
|
curr_val.startsWith("#") ? 1 : 0,
|
||||||
curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length
|
curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length,
|
||||||
);
|
);
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onBlur: function(ev) {
|
onBlur: function(ev) {
|
||||||
var curr_val = ev.target.value;
|
const curr_val = ev.target.value;
|
||||||
|
|
||||||
if (this.props.homeserver) {
|
if (this.props.homeserver) {
|
||||||
if (curr_val == "#:" + this.props.homeserver) {
|
if (curr_val == "#:" + this.props.homeserver) {
|
||||||
|
@ -84,8 +84,8 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (curr_val != "") {
|
if (curr_val != "") {
|
||||||
var new_val = ev.target.value;
|
let new_val = ev.target.value;
|
||||||
var suffix = ":" + this.props.homeserver;
|
const suffix = ":" + this.props.homeserver;
|
||||||
if (!curr_val.startsWith("#")) new_val = "#" + new_val;
|
if (!curr_val.startsWith("#")) new_val = "#" + new_val;
|
||||||
if (!curr_val.endsWith(suffix)) new_val = new_val + suffix;
|
if (!curr_val.endsWith(suffix)) new_val = new_val + suffix;
|
||||||
ev.target.value = new_val;
|
ev.target.value = new_val;
|
||||||
|
@ -97,7 +97,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
|
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
|
||||||
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
|
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
|
||||||
value={this.props.alias}/>
|
value={this.props.alias} />
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,12 +23,13 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||||
|
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||||
|
|
||||||
const TRUNCATE_QUERY_LIST = 40;
|
const TRUNCATE_QUERY_LIST = 40;
|
||||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: "UserPickerDialog",
|
displayName: "AddressPickerDialog",
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
|
@ -40,6 +41,12 @@ module.exports = React.createClass({
|
||||||
focus: PropTypes.bool,
|
focus: PropTypes.bool,
|
||||||
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)),
|
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)),
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
groupId: PropTypes.string,
|
||||||
|
// The type of entity to search for. Default: 'user'.
|
||||||
|
pickerType: PropTypes.oneOf(['user', 'room']),
|
||||||
|
// Whether the current user should be included in the addresses returned. Only
|
||||||
|
// applicable when pickerType is `user`. Default: false.
|
||||||
|
includeSelf: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -47,6 +54,8 @@ module.exports = React.createClass({
|
||||||
value: "",
|
value: "",
|
||||||
focus: true,
|
focus: true,
|
||||||
validAddressTypes: addressTypes,
|
validAddressTypes: addressTypes,
|
||||||
|
pickerType: 'user',
|
||||||
|
includeSelf: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -140,10 +149,22 @@ module.exports = React.createClass({
|
||||||
// Only do search if there is something to search
|
// Only do search if there is something to search
|
||||||
if (query.length > 0 && query != '@' && query.length >= 2) {
|
if (query.length > 0 && query != '@' && query.length >= 2) {
|
||||||
this.queryChangedDebouncer = setTimeout(() => {
|
this.queryChangedDebouncer = setTimeout(() => {
|
||||||
if (this.state.serverSupportsUserDirectory) {
|
if (this.props.pickerType === 'user') {
|
||||||
this._doUserDirectorySearch(query);
|
if (this.props.groupId) {
|
||||||
|
this._doNaiveGroupSearch(query);
|
||||||
|
} else if (this.state.serverSupportsUserDirectory) {
|
||||||
|
this._doUserDirectorySearch(query);
|
||||||
|
} else {
|
||||||
|
this._doLocalSearch(query);
|
||||||
|
}
|
||||||
|
} else if (this.props.pickerType === 'room') {
|
||||||
|
if (this.props.groupId) {
|
||||||
|
this._doNaiveGroupRoomSearch(query);
|
||||||
|
} else {
|
||||||
|
this._doRoomSearch(query);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this._doLocalSearch(query);
|
console.error('Unknown pickerType', this.props.pickerType);
|
||||||
}
|
}
|
||||||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||||
} else {
|
} else {
|
||||||
|
@ -185,6 +206,94 @@ module.exports = React.createClass({
|
||||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_doNaiveGroupSearch: function(query) {
|
||||||
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
|
this.setState({
|
||||||
|
busy: true,
|
||||||
|
query,
|
||||||
|
searchError: null,
|
||||||
|
});
|
||||||
|
MatrixClientPeg.get().getGroupUsers(this.props.groupId).then((resp) => {
|
||||||
|
const results = [];
|
||||||
|
resp.chunk.forEach((u) => {
|
||||||
|
const userIdMatch = u.user_id.toLowerCase().includes(lowerCaseQuery);
|
||||||
|
const displayNameMatch = (u.displayname || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
if (!(userIdMatch || displayNameMatch)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
results.push({
|
||||||
|
user_id: u.user_id,
|
||||||
|
avatar_url: u.avatar_url,
|
||||||
|
display_name: u.displayname,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._processResults(results, query);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error('Error whilst searching group rooms: ', err);
|
||||||
|
this.setState({
|
||||||
|
searchError: err.errcode ? err.message : _t('Something went wrong!'),
|
||||||
|
});
|
||||||
|
}).done(() => {
|
||||||
|
this.setState({
|
||||||
|
busy: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_doNaiveGroupRoomSearch: function(query) {
|
||||||
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
|
const groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), this.props.groupId);
|
||||||
|
const results = [];
|
||||||
|
groupStore.getGroupRooms().forEach((r) => {
|
||||||
|
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
if (!(nameMatch || topicMatch || aliasMatch)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
results.push({
|
||||||
|
room_id: r.room_id,
|
||||||
|
avatar_url: r.avatar_url,
|
||||||
|
name: r.name || r.canonical_alias,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._processResults(results, query);
|
||||||
|
this.setState({
|
||||||
|
busy: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_doRoomSearch: function(query) {
|
||||||
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
|
const rooms = MatrixClientPeg.get().getRooms();
|
||||||
|
const results = [];
|
||||||
|
rooms.forEach((room) => {
|
||||||
|
const nameEvent = room.currentState.getStateEvents('m.room.name', '');
|
||||||
|
const topicEvent = room.currentState.getStateEvents('m.room.topic', '');
|
||||||
|
const name = nameEvent ? nameEvent.getContent().name : '';
|
||||||
|
const canonicalAlias = room.getCanonicalAlias();
|
||||||
|
const topic = topicEvent ? topicEvent.getContent().topic : '';
|
||||||
|
|
||||||
|
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
const aliasMatch = (canonicalAlias || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
const topicMatch = (topic || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
if (!(nameMatch || topicMatch || aliasMatch)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
|
||||||
|
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
|
||||||
|
results.push({
|
||||||
|
room_id: room.roomId,
|
||||||
|
avatar_url: avatarUrl,
|
||||||
|
name: name || canonicalAlias,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._processResults(results, query);
|
||||||
|
this.setState({
|
||||||
|
busy: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_doUserDirectorySearch: function(query) {
|
_doUserDirectorySearch: function(query) {
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
|
@ -245,17 +354,30 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_processResults: function(results, query) {
|
_processResults: function(results, query) {
|
||||||
const queryList = [];
|
const queryList = [];
|
||||||
results.forEach((user) => {
|
results.forEach((result) => {
|
||||||
if (user.user_id === MatrixClientPeg.get().credentials.userId) {
|
if (result.room_id) {
|
||||||
|
queryList.push({
|
||||||
|
addressType: 'mx-room-id',
|
||||||
|
address: result.room_id,
|
||||||
|
displayName: result.name,
|
||||||
|
avatarMxc: result.avatar_url,
|
||||||
|
isKnown: true,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!this.props.includeSelf &&
|
||||||
|
result.user_id === MatrixClientPeg.get().credentials.userId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Return objects, structure of which is defined
|
// Return objects, structure of which is defined
|
||||||
// by UserAddressType
|
// by UserAddressType
|
||||||
queryList.push({
|
queryList.push({
|
||||||
addressType: 'mx',
|
addressType: 'mx-user-id',
|
||||||
address: user.user_id,
|
address: result.user_id,
|
||||||
displayName: user.display_name,
|
displayName: result.display_name,
|
||||||
avatarMxc: user.avatar_url,
|
avatarMxc: result.avatar_url,
|
||||||
isKnown: true,
|
isKnown: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -291,16 +413,23 @@ module.exports = React.createClass({
|
||||||
address: addressText,
|
address: addressText,
|
||||||
isKnown: false,
|
isKnown: false,
|
||||||
};
|
};
|
||||||
if (addrType == null) {
|
if (!this.props.validAddressTypes.includes(addrType)) {
|
||||||
this.setState({ error: true });
|
this.setState({ error: true });
|
||||||
return null;
|
return null;
|
||||||
} else if (addrType == 'mx') {
|
} else if (addrType == 'mx-user-id') {
|
||||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||||
if (user) {
|
if (user) {
|
||||||
addrObj.displayName = user.displayName;
|
addrObj.displayName = user.displayName;
|
||||||
addrObj.avatarMxc = user.avatarUrl;
|
addrObj.avatarMxc = user.avatarUrl;
|
||||||
addrObj.isKnown = true;
|
addrObj.isKnown = true;
|
||||||
}
|
}
|
||||||
|
} else if (addrType == 'mx-room-id') {
|
||||||
|
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||||
|
if (room) {
|
||||||
|
addrObj.displayName = room.name;
|
||||||
|
addrObj.avatarMxc = room.avatarUrl;
|
||||||
|
addrObj.isKnown = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userList = this.state.userList.slice();
|
const userList = this.state.userList.slice();
|
||||||
|
@ -360,7 +489,12 @@ module.exports = React.createClass({
|
||||||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||||
for (let i = 0; i < this.state.userList.length; i++) {
|
for (let i = 0; i < this.state.userList.length; i++) {
|
||||||
query.push(
|
query.push(
|
||||||
<AddressTile key={i} address={this.state.userList[i]} canDismiss={true} onDismissed={ this.onDismissed(i) } />,
|
<AddressTile
|
||||||
|
key={i}
|
||||||
|
address={this.state.userList[i]}
|
||||||
|
canDismiss={true}
|
||||||
|
onDismissed={this.onDismissed(i)}
|
||||||
|
showAddress={this.props.pickerType === 'user'} />,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -382,23 +516,37 @@ module.exports = React.createClass({
|
||||||
let error;
|
let error;
|
||||||
let addressSelector;
|
let addressSelector;
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
|
let tryUsing = '';
|
||||||
|
const validTypeDescriptions = this.props.validAddressTypes.map((t) => {
|
||||||
|
return {
|
||||||
|
'mx-user-id': _t("Matrix ID"),
|
||||||
|
'mx-room-id': _t("Matrix Room ID"),
|
||||||
|
'email': _t("email address"),
|
||||||
|
}[t];
|
||||||
|
});
|
||||||
|
tryUsing = _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||||
|
validTypesList: validTypeDescriptions.join(", "),
|
||||||
|
});
|
||||||
error = <div className="mx_ChatInviteDialog_error">
|
error = <div className="mx_ChatInviteDialog_error">
|
||||||
{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}
|
{ _t("You have entered an invalid address.") }
|
||||||
|
<br />
|
||||||
|
{ tryUsing }
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.state.searchError) {
|
} else if (this.state.searchError) {
|
||||||
error = <div className="mx_ChatInviteDialog_error">{this.state.searchError}</div>;
|
error = <div className="mx_ChatInviteDialog_error">{ this.state.searchError }</div>;
|
||||||
} else if (
|
} else if (
|
||||||
this.state.query.length > 0 &&
|
this.state.query.length > 0 &&
|
||||||
this.state.queryList.length === 0 &&
|
this.state.queryList.length === 0 &&
|
||||||
!this.state.busy
|
!this.state.busy
|
||||||
) {
|
) {
|
||||||
error = <div className="mx_ChatInviteDialog_error">{_t("No results")}</div>;
|
error = <div className="mx_ChatInviteDialog_error">{ _t("No results") }</div>;
|
||||||
} else {
|
} else {
|
||||||
addressSelector = (
|
addressSelector = (
|
||||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||||
addressList={ this.state.queryList }
|
addressList={this.state.queryList}
|
||||||
onSelected={ this.onSelected }
|
showAddress={this.props.pickerType === 'user'}
|
||||||
truncateAt={ TRUNCATE_QUERY_LIST }
|
onSelected={this.onSelected}
|
||||||
|
truncateAt={TRUNCATE_QUERY_LIST}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -406,7 +554,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
|
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
|
||||||
<div className="mx_Dialog_title">
|
<div className="mx_Dialog_title">
|
||||||
{this.props.title}
|
{ this.props.title }
|
||||||
</div>
|
</div>
|
||||||
<AccessibleButton className="mx_ChatInviteDialog_cancel"
|
<AccessibleButton className="mx_ChatInviteDialog_cancel"
|
||||||
onClick={this.onCancel} >
|
onClick={this.onCancel} >
|
||||||
|
@ -422,7 +570,7 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
|
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
|
||||||
{this.props.button}
|
{ this.props.button }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -155,7 +155,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
width={48} height={48}
|
width={48} height={48}
|
||||||
/>
|
/>
|
||||||
<div className="mx_ChatCreateOrReuseDialog_profile_name">
|
<div className="mx_ChatCreateOrReuseDialog_profile_name">
|
||||||
{this.state.profile.displayName || this.props.userId}
|
{ this.state.profile.displayName || this.props.userId }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -177,7 +177,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
||||||
onFinished={ this.props.onFinished.bind(false) }
|
onFinished={this.props.onFinished.bind(false)}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
{ content }
|
{ content }
|
||||||
|
|
|
@ -52,20 +52,20 @@ export default React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={ this.onOk }
|
onEnterPressed={this.onOk}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{_t("Are you sure you wish to remove (delete) this event? " +
|
{ _t("Are you sure you wish to remove (delete) this event? " +
|
||||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
"Note that if you delete a room name or topic change, it could undo the change.") }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className={confirmButtonClass} onClick={this.onOk}>
|
<button className={confirmButtonClass} onClick={this.onOk}>
|
||||||
{_t("Remove")}
|
{ _t("Remove") }
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button onClick={this.onCancel}>
|
<button onClick={this.onCancel}>
|
||||||
{_t("Cancel")}
|
{ _t("Cancel") }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { GroupMemberType } from '../../../groups';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A dialog for confirming an operation on another user.
|
* A dialog for confirming an operation on another user.
|
||||||
|
@ -30,7 +31,10 @@ import classnames from 'classnames';
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'ConfirmUserActionDialog',
|
displayName: 'ConfirmUserActionDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
member: React.PropTypes.object.isRequired, // matrix-js-sdk member object
|
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||||
|
member: React.PropTypes.object,
|
||||||
|
// group member object. Supply either this or 'member'
|
||||||
|
groupMember: GroupMemberType,
|
||||||
action: React.PropTypes.string.isRequired, // eg. 'Ban'
|
action: React.PropTypes.string.isRequired, // eg. 'Ban'
|
||||||
|
|
||||||
// Whether to display a text field for a reason
|
// Whether to display a text field for a reason
|
||||||
|
@ -69,6 +73,7 @@ export default React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||||
|
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||||
|
|
||||||
const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
|
const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
|
||||||
const confirmButtonClass = classnames({
|
const confirmButtonClass = classnames({
|
||||||
|
@ -83,7 +88,7 @@ export default React.createClass({
|
||||||
<form onSubmit={this.onOk}>
|
<form onSubmit={this.onOk}>
|
||||||
<input className="mx_ConfirmUserActionDialog_reasonField"
|
<input className="mx_ConfirmUserActionDialog_reasonField"
|
||||||
ref={this._collectReasonField}
|
ref={this._collectReasonField}
|
||||||
placeholder={ _t("Reason") }
|
placeholder={_t("Reason")}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
@ -91,24 +96,38 @@ export default React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let avatar;
|
||||||
|
let name;
|
||||||
|
let userId;
|
||||||
|
if (this.props.member) {
|
||||||
|
avatar = <MemberAvatar member={this.props.member} width={48} height={48} />;
|
||||||
|
name = this.props.member.name;
|
||||||
|
userId = this.props.member.userId;
|
||||||
|
} else {
|
||||||
|
// we don't get this info from the API yet
|
||||||
|
avatar = <BaseAvatar name={this.props.groupMember.userId} width={48} height={48} />;
|
||||||
|
name = this.props.groupMember.userId;
|
||||||
|
userId = this.props.groupMember.userId;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={ this.onOk }
|
onEnterPressed={this.onOk}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||||
<MemberAvatar member={this.props.member} width={48} height={48} />
|
{ avatar }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div>
|
<div className="mx_ConfirmUserActionDialog_name">{ name }</div>
|
||||||
<div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div>
|
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
||||||
</div>
|
</div>
|
||||||
{reasonBox}
|
{ reasonBox }
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className={confirmButtonClass}
|
<button className={confirmButtonClass}
|
||||||
onClick={this.onOk} autoFocus={!this.props.askReason}
|
onClick={this.onOk} autoFocus={!this.props.askReason}
|
||||||
>
|
>
|
||||||
{this.props.action}
|
{ this.props.action }
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button onClick={this.onCancel}>
|
<button onClick={this.onCancel}>
|
||||||
|
|
|
@ -62,15 +62,15 @@ export default React.createClass({
|
||||||
let error = null;
|
let error = null;
|
||||||
if (parsedGroupId === null) {
|
if (parsedGroupId === null) {
|
||||||
error = _t(
|
error = _t(
|
||||||
"Group IDs must be of the form +localpart:%(domain)s",
|
"Community IDs must be of the form +localpart:%(domain)s",
|
||||||
{domain: MatrixClientPeg.get().getDomain()},
|
{domain: MatrixClientPeg.get().getDomain()},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const domain = parsedGroupId[1];
|
const domain = parsedGroupId[1];
|
||||||
if (domain !== MatrixClientPeg.get().getDomain()) {
|
if (domain !== MatrixClientPeg.get().getDomain()) {
|
||||||
error = _t(
|
error = _t(
|
||||||
"It is currently only possible to create groups on your own home server: "+
|
"It is currently only possible to create communities on your own home server: "+
|
||||||
"use a group ID ending with %(domain)s",
|
"use a community ID ending with %(domain)s",
|
||||||
{domain: MatrixClientPeg.get().getDomain()},
|
{domain: MatrixClientPeg.get().getDomain()},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -142,21 +142,21 @@ export default React.createClass({
|
||||||
// rather than displaying what the server gives us, but synapse doesn't give
|
// rather than displaying what the server gives us, but synapse doesn't give
|
||||||
// any yet.
|
// any yet.
|
||||||
createErrorNode = <div className="error">
|
createErrorNode = <div className="error">
|
||||||
<div>{_t('Room creation failed')}</div>
|
<div>{ _t('Room creation failed') }</div>
|
||||||
<div>{this.state.createError.message}</div>
|
<div>{ this.state.createError.message }</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={this._onFormSubmit}
|
onEnterPressed={this._onFormSubmit}
|
||||||
title={_t('Create Group')}
|
title={_t('Create Community')}
|
||||||
>
|
>
|
||||||
<form onSubmit={this._onFormSubmit}>
|
<form onSubmit={this._onFormSubmit}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_CreateGroupDialog_inputRow">
|
<div className="mx_CreateGroupDialog_inputRow">
|
||||||
<div className="mx_CreateGroupDialog_label">
|
<div className="mx_CreateGroupDialog_label">
|
||||||
<label htmlFor="groupname">{_t('Group Name')}</label>
|
<label htmlFor="groupname">{ _t('Community Name') }</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input id="groupname" className="mx_CreateGroupDialog_input"
|
<input id="groupname" className="mx_CreateGroupDialog_input"
|
||||||
|
@ -169,7 +169,7 @@ export default React.createClass({
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_CreateGroupDialog_inputRow">
|
<div className="mx_CreateGroupDialog_inputRow">
|
||||||
<div className="mx_CreateGroupDialog_label">
|
<div className="mx_CreateGroupDialog_label">
|
||||||
<label htmlFor="groupid">{_t('Group ID')}</label>
|
<label htmlFor="groupid">{ _t('Community ID') }</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input id="groupid" className="mx_CreateGroupDialog_input"
|
<input id="groupid" className="mx_CreateGroupDialog_input"
|
||||||
|
@ -182,9 +182,9 @@ export default React.createClass({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="error">
|
<div className="error">
|
||||||
{this.state.groupIdError}
|
{ this.state.groupIdError }
|
||||||
</div>
|
</div>
|
||||||
{createErrorNode}
|
{ createErrorNode }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this._onCancel}>
|
<button onClick={this._onCancel}>
|
||||||
|
|
81
src/components/views/dialogs/CreateRoomDialog.js
Normal file
81
src/components/views/dialogs/CreateRoomDialog.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
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 React from 'react';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'CreateRoomDialog',
|
||||||
|
propTypes: {
|
||||||
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
const config = SdkConfig.get();
|
||||||
|
// Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true)
|
||||||
|
this.defaultNoFederate = config.default_federate === false;
|
||||||
|
},
|
||||||
|
|
||||||
|
onOk: function() {
|
||||||
|
this.props.onFinished(true, this.refs.textinput.value, this.refs.checkbox.checked);
|
||||||
|
},
|
||||||
|
|
||||||
|
onCancel: function() {
|
||||||
|
this.props.onFinished(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
return (
|
||||||
|
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||||
|
onEnterPressed={this.onOk}
|
||||||
|
title={_t('Create Room')}
|
||||||
|
>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
<div className="mx_CreateRoomDialog_label">
|
||||||
|
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<details className="mx_CreateRoomDialog_details">
|
||||||
|
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
|
||||||
|
<label htmlFor="checkbox">
|
||||||
|
{ _t('Block users on other matrix homeservers from joining this room') }
|
||||||
|
<br />
|
||||||
|
({ _t('This setting cannot be changed later!') })
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button onClick={this.onCancel}>
|
||||||
|
{ _t('Cancel') }
|
||||||
|
</button>
|
||||||
|
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||||
|
{ _t('Create Room') }
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -83,7 +83,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
let error = null;
|
let error = null;
|
||||||
if (this.state.errStr) {
|
if (this.state.errStr) {
|
||||||
error = <div className="error">
|
error = <div className="error">
|
||||||
{this.state.errStr}
|
{ this.state.errStr }
|
||||||
</div>;
|
</div>;
|
||||||
passwordBoxClass = 'error';
|
passwordBoxClass = 'error';
|
||||||
}
|
}
|
||||||
|
@ -94,30 +94,30 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
let cancelButton = null;
|
let cancelButton = null;
|
||||||
if (!this.state.busy) {
|
if (!this.state.busy) {
|
||||||
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
|
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
|
||||||
{_t("Cancel")}
|
{ _t("Cancel") }
|
||||||
</button>;
|
</button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_DeactivateAccountDialog">
|
<div className="mx_DeactivateAccountDialog">
|
||||||
<div className="mx_Dialog_title danger">
|
<div className="mx_Dialog_title danger">
|
||||||
{_t("Deactivate Account")}
|
{ _t("Deactivate Account") }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<p>{_t("This will make your account permanently unusable. You will not be able to re-register the same user ID.")}</p>
|
<p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p>
|
||||||
|
|
||||||
<p>{_t("This action is irreversible.")}</p>
|
<p>{ _t("This action is irreversible.") }</p>
|
||||||
|
|
||||||
<p>{_t("To continue, please enter your password.")}</p>
|
<p>{ _t("To continue, please enter your password.") }</p>
|
||||||
|
|
||||||
<p>{_t("Password")}:</p>
|
<p>{ _t("Password") }:</p>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
onChange={this._onPasswordFieldChange}
|
onChange={this._onPasswordFieldChange}
|
||||||
ref={(e) => {this._passwordField = e;}}
|
ref={(e) => {this._passwordField = e;}}
|
||||||
className={passwordBoxClass}
|
className={passwordBoxClass}
|
||||||
/>
|
/>
|
||||||
{error}
|
{ error }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button
|
<button
|
||||||
|
@ -125,10 +125,10 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
onClick={this._onOk}
|
onClick={this._onOk}
|
||||||
disabled={!okEnabled}
|
disabled={!okEnabled}
|
||||||
>
|
>
|
||||||
{okLabel}
|
{ okLabel }
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{cancelButton}
|
{ cancelButton }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,25 +28,25 @@ export default function DeviceVerifyDialog(props) {
|
||||||
const body = (
|
const body = (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
{_t("To verify that this device can be trusted, please contact its " +
|
{ _t("To verify that this device can be trusted, please contact its " +
|
||||||
"owner using some other means (e.g. in person or a phone call) " +
|
"owner using some other means (e.g. in person or a phone call) " +
|
||||||
"and ask them whether the key they see in their User Settings " +
|
"and ask them whether the key they see in their User Settings " +
|
||||||
"for this device matches the key below:")}
|
"for this device matches the key below:") }
|
||||||
</p>
|
</p>
|
||||||
<div className="mx_UserSettings_cryptoSection">
|
<div className="mx_UserSettings_cryptoSection">
|
||||||
<ul>
|
<ul>
|
||||||
<li><label>{_t("Device name")}:</label> <span>{ props.device.getDisplayName() }</span></li>
|
<li><label>{ _t("Device name") }:</label> <span>{ props.device.getDisplayName() }</span></li>
|
||||||
<li><label>{_t("Device ID")}:</label> <span><code>{ props.device.deviceId}</code></span></li>
|
<li><label>{ _t("Device ID") }:</label> <span><code>{ props.device.deviceId }</code></span></li>
|
||||||
<li><label>{_t("Device key")}:</label> <span><code><b>{ key }</b></code></span></li>
|
<li><label>{ _t("Device key") }:</label> <span><code><b>{ key }</b></code></span></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
{_t("If it matches, press the verify button below. " +
|
{ _t("If it matches, press the verify button below. " +
|
||||||
"If it doesn't, then someone else is intercepting this device " +
|
"If it doesn't, then someone else is intercepting this device " +
|
||||||
"and you probably want to press the blacklist button instead.")}
|
"and you probably want to press the blacklist button instead.") }
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{_t("In future this verification process will be more sophisticated.")}
|
{ _t("In future this verification process will be more sophisticated.") }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -63,11 +63,11 @@ export default React.createClass({
|
||||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||||
title={this.props.title || _t('Error')}>
|
title={this.props.title || _t('Error')}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{this.props.description || _t('An error has occurred.')}
|
{ this.props.description || _t('An error has occurred.') }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
|
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
|
||||||
{this.props.button || _t('OK')}
|
{ this.props.button || _t('OK') }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -48,7 +48,7 @@ export default React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
authError: null,
|
authError: null,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
_onAuthFinished: function(success, result) {
|
_onAuthFinished: function(success, result) {
|
||||||
|
@ -73,12 +73,12 @@ export default React.createClass({
|
||||||
if (this.state.authError) {
|
if (this.state.authError) {
|
||||||
content = (
|
content = (
|
||||||
<div>
|
<div>
|
||||||
<div>{this.state.authError.message || this.state.authError.toString()}</div>
|
<div>{ this.state.authError.message || this.state.authError.toString() }</div>
|
||||||
<br />
|
<br />
|
||||||
<AccessibleButton onClick={this._onDismissClick}
|
<AccessibleButton onClick={this._onDismissClick}
|
||||||
className="mx_UserSettings_button"
|
className="mx_UserSettings_button"
|
||||||
>
|
>
|
||||||
{_t("Dismiss")}
|
{ _t("Dismiss") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -100,7 +100,7 @@ export default React.createClass({
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
|
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
|
||||||
>
|
>
|
||||||
{content}
|
{ content }
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,7 +18,7 @@ import Modal from '../../../Modal';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog which asks the user whether they want to share their keys with
|
* Dialog which asks the user whether they want to share their keys with
|
||||||
|
@ -116,27 +116,27 @@ export default React.createClass({
|
||||||
|
|
||||||
let text;
|
let text;
|
||||||
if (this.state.wasNewDevice) {
|
if (this.state.wasNewDevice) {
|
||||||
text = "You added a new device '%(displayName)s', which is"
|
text = _td("You added a new device '%(displayName)s', which is"
|
||||||
+ " requesting encryption keys.";
|
+ " requesting encryption keys.");
|
||||||
} else {
|
} else {
|
||||||
text = "Your unverified device '%(displayName)s' is requesting"
|
text = _td("Your unverified device '%(displayName)s' is requesting"
|
||||||
+ " encryption keys.";
|
+ " encryption keys.");
|
||||||
}
|
}
|
||||||
text = _t(text, {displayName: displayName});
|
text = _t(text, {displayName: displayName});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{text}</p>
|
<p>{ text }</p>
|
||||||
|
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this._onVerifyClicked}>
|
<button onClick={this._onVerifyClicked}>
|
||||||
{_t('Start verification')}
|
{ _t('Start verification') }
|
||||||
</button>
|
</button>
|
||||||
<button onClick={this._onShareClicked}>
|
<button onClick={this._onShareClicked}>
|
||||||
{_t('Share without verifying')}
|
{ _t('Share without verifying') }
|
||||||
</button>
|
</button>
|
||||||
<button onClick={this._onIgnoreClicked}>
|
<button onClick={this._onIgnoreClicked}>
|
||||||
{_t('Ignore request')}
|
{ _t('Ignore request') }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -154,7 +154,7 @@ export default React.createClass({
|
||||||
} else {
|
} else {
|
||||||
content = (
|
content = (
|
||||||
<div>
|
<div>
|
||||||
<p>{_t('Loading device info...')}</p>
|
<p>{ _t('Loading device info...') }</p>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -165,7 +165,7 @@ export default React.createClass({
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t('Encryption key request')}
|
title={_t('Encryption key request')}
|
||||||
>
|
>
|
||||||
{content}
|
{ content }
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 New Vector 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,6 +18,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'QuestionDialog',
|
displayName: 'QuestionDialog',
|
||||||
|
@ -25,6 +27,7 @@ export default React.createClass({
|
||||||
description: React.PropTypes.node,
|
description: React.PropTypes.node,
|
||||||
extraButtons: React.PropTypes.node,
|
extraButtons: React.PropTypes.node,
|
||||||
button: React.PropTypes.string,
|
button: React.PropTypes.string,
|
||||||
|
danger: React.PropTypes.bool,
|
||||||
focus: React.PropTypes.bool,
|
focus: React.PropTypes.bool,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
@ -36,6 +39,7 @@ export default React.createClass({
|
||||||
extraButtons: null,
|
extraButtons: null,
|
||||||
focus: true,
|
focus: true,
|
||||||
hasCancelButton: true,
|
hasCancelButton: true,
|
||||||
|
danger: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -51,23 +55,27 @@ export default React.createClass({
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const cancelButton = this.props.hasCancelButton ? (
|
const cancelButton = this.props.hasCancelButton ? (
|
||||||
<button onClick={this.onCancel}>
|
<button onClick={this.onCancel}>
|
||||||
{_t("Cancel")}
|
{ _t("Cancel") }
|
||||||
</button>
|
</button>
|
||||||
) : null;
|
) : null;
|
||||||
|
const buttonClasses = classnames({
|
||||||
|
mx_Dialog_primary: true,
|
||||||
|
danger: this.props.danger,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={ this.onOk }
|
onEnterPressed={this.onOk}
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{this.props.description}
|
{ this.props.description }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
|
<button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}>
|
||||||
{this.props.button || _t('OK')}
|
{ this.props.button || _t('OK') }
|
||||||
</button>
|
</button>
|
||||||
{this.props.extraButtons}
|
{ this.props.extraButtons }
|
||||||
{cancelButton}
|
{ cancelButton }
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
|
|
|
@ -45,10 +45,10 @@ export default React.createClass({
|
||||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||||
bugreport = (
|
bugreport = (
|
||||||
<p>
|
<p>
|
||||||
{_tJsx(
|
{ _tJsx(
|
||||||
"Otherwise, <a>click here</a> to send a bug report.",
|
"Otherwise, <a>click here</a> to send a bug report.",
|
||||||
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{sub}</a>,
|
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a>,
|
||||||
)}
|
) }
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -57,19 +57,19 @@ export default React.createClass({
|
||||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||||
title={_t('Unable to restore session')}>
|
title={_t('Unable to restore session')}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<p>{_t("We encountered an error trying to restore your previous session. If " +
|
<p>{ _t("We encountered an error trying to restore your previous session. If " +
|
||||||
"you continue, you will need to log in again, and encrypted chat " +
|
"you continue, you will need to log in again, and encrypted chat " +
|
||||||
"history will be unreadable.")}</p>
|
"history will be unreadable.") }</p>
|
||||||
|
|
||||||
<p>{_t("If you have previously used a more recent version of Riot, your session " +
|
<p>{ _t("If you have previously used a more recent version of Riot, your session " +
|
||||||
"may be incompatible with this version. Close this window and return " +
|
"may be incompatible with this version. Close this window and return " +
|
||||||
"to the more recent version.")}</p>
|
"to the more recent version.") }</p>
|
||||||
|
|
||||||
{bugreport}
|
{ bugreport }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
|
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
|
||||||
{_t("Continue anyway")}
|
{ _t("Continue anyway") }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -130,10 +130,10 @@ export default React.createClass({
|
||||||
|
|
||||||
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
||||||
className="mx_SetEmailDialog_email_input"
|
className="mx_SetEmailDialog_email_input"
|
||||||
placeholder={ _t("Email address") }
|
placeholder={_t("Email address")}
|
||||||
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
|
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
|
||||||
blurToCancel={ false }
|
blurToCancel={false}
|
||||||
onValueChanged={ this.onEmailAddressChanged } />;
|
onValueChanged={this.onEmailAddressChanged} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_SetEmailDialog"
|
<BaseDialog className="mx_SetEmailDialog"
|
||||||
|
|
|
@ -226,7 +226,7 @@ export default React.createClass({
|
||||||
let usernameIndicator = null;
|
let usernameIndicator = null;
|
||||||
let usernameBusyIndicator = null;
|
let usernameBusyIndicator = null;
|
||||||
if (this.state.usernameBusy) {
|
if (this.state.usernameBusy) {
|
||||||
usernameBusyIndicator = <Spinner w="24" h="24"/>;
|
usernameBusyIndicator = <Spinner w="24" h="24" />;
|
||||||
} else {
|
} else {
|
||||||
const usernameAvailable = this.state.username &&
|
const usernameAvailable = this.state.username &&
|
||||||
this.state.usernameCheckSupport && !this.state.usernameError;
|
this.state.usernameCheckSupport && !this.state.usernameError;
|
||||||
|
@ -275,17 +275,17 @@ export default React.createClass({
|
||||||
/<a>(.*?)<\/a>/,
|
/<a>(.*?)<\/a>/,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
(sub) => <span>{this.props.homeserverUrl}</span>,
|
(sub) => <span>{ this.props.homeserverUrl }</span>,
|
||||||
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{sub}</a>,
|
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
||||||
],
|
],
|
||||||
)}
|
) }
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{ _tJsx(
|
{ _tJsx(
|
||||||
'If you already have a Matrix account you can <a>log in</a> instead.',
|
'If you already have a Matrix account you can <a>log in</a> instead.',
|
||||||
/<a>(.*?)<\/a>/,
|
/<a>(.*?)<\/a>/,
|
||||||
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{sub}</a>],
|
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a>],
|
||||||
)}
|
) }
|
||||||
</p>
|
</p>
|
||||||
{ auth }
|
{ auth }
|
||||||
{ authErrorIndicator }
|
{ authErrorIndicator }
|
||||||
|
|
|
@ -65,10 +65,10 @@ export default React.createClass({
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_TextInputDialog_label">
|
<div className="mx_TextInputDialog_label">
|
||||||
<label htmlFor="textinput"> {this.props.description} </label>
|
<label htmlFor="textinput"> { this.props.description } </label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" onKeyDown={this.onKeyDown}/>
|
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
|
@ -76,7 +76,7 @@ export default React.createClass({
|
||||||
{ _t("Cancel") }
|
{ _t("Cancel") }
|
||||||
</button>
|
</button>
|
||||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||||
{this.props.button}
|
{ this.props.button }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -28,9 +28,9 @@ function DeviceListEntry(props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<DeviceVerifyButtons device={ device } userId={ userId } />
|
<DeviceVerifyButtons device={device} userId={userId} />
|
||||||
{ device.deviceId }
|
{ device.deviceId }
|
||||||
<br/>
|
<br />
|
||||||
{ device.getDisplayName() }
|
{ device.getDisplayName() }
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -48,13 +48,13 @@ function UserUnknownDeviceList(props) {
|
||||||
const {userId, userDevices} = props;
|
const {userId, userDevices} = props;
|
||||||
|
|
||||||
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
|
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
|
||||||
<DeviceListEntry key={ deviceId } userId={ userId }
|
<DeviceListEntry key={deviceId} userId={userId}
|
||||||
device={ userDevices[deviceId] } />,
|
device={userDevices[deviceId]} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="mx_UnknownDeviceDialog_deviceList">
|
<ul className="mx_UnknownDeviceDialog_deviceList">
|
||||||
{deviceListEntries}
|
{ deviceListEntries }
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -71,13 +71,13 @@ function UnknownDeviceList(props) {
|
||||||
const {devices} = props;
|
const {devices} = props;
|
||||||
|
|
||||||
const userListEntries = Object.keys(devices).map((userId) =>
|
const userListEntries = Object.keys(devices).map((userId) =>
|
||||||
<li key={ userId }>
|
<li key={userId}>
|
||||||
<p>{ userId }:</p>
|
<p>{ userId }:</p>
|
||||||
<UserUnknownDeviceList userId={ userId } userDevices={ devices[userId] } />
|
<UserUnknownDeviceList userId={userId} userDevices={devices[userId]} />
|
||||||
</li>,
|
</li>,
|
||||||
);
|
);
|
||||||
|
|
||||||
return <ul>{userListEntries}</ul>;
|
return <ul>{ userListEntries }</ul>;
|
||||||
}
|
}
|
||||||
|
|
||||||
UnknownDeviceList.propTypes = {
|
UnknownDeviceList.propTypes = {
|
||||||
|
@ -120,17 +120,17 @@ export default React.createClass({
|
||||||
if (blacklistUnverified) {
|
if (blacklistUnverified) {
|
||||||
warning = (
|
warning = (
|
||||||
<h4>
|
<h4>
|
||||||
{_t("You are currently blacklisting unverified devices; to send " +
|
{ _t("You are currently blacklisting unverified devices; to send " +
|
||||||
"messages to these devices you must verify them.")}
|
"messages to these devices you must verify them.") }
|
||||||
</h4>
|
</h4>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
warning = (
|
warning = (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
{_t("We recommend you go through the verification process " +
|
{ _t("We recommend you go through the verification process " +
|
||||||
"for each device to confirm they belong to their legitimate owner, " +
|
"for each device to confirm they belong to their legitimate owner, " +
|
||||||
"but you can resend the message without verifying if you prefer.")}
|
"but you can resend the message without verifying if you prefer.") }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -149,22 +149,22 @@ export default React.createClass({
|
||||||
>
|
>
|
||||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||||
<h4>
|
<h4>
|
||||||
{_t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name})}
|
{ _t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name}) }
|
||||||
</h4>
|
</h4>
|
||||||
{ warning }
|
{ warning }
|
||||||
{_t("Unknown devices")}:
|
{ _t("Unknown devices") }:
|
||||||
|
|
||||||
<UnknownDeviceList devices={this.props.devices} />
|
<UnknownDeviceList devices={this.props.devices} />
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbar>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className="mx_Dialog_primary" autoFocus={ true }
|
<button className="mx_Dialog_primary" autoFocus={true}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
Resend.resendUnsentEvents(this.props.room);
|
Resend.resendUnsentEvents(this.props.room);
|
||||||
}}>
|
}}>
|
||||||
{_t("Send anyway")}
|
{ _t("Send anyway") }
|
||||||
</button>
|
</button>
|
||||||
<button className="mx_Dialog_primary" autoFocus={ true }
|
<button className="mx_Dialog_primary" autoFocus={true}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/riot-web/issues/3148
|
// https://github.com/vector-im/riot-web/issues/3148
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default function AccessibleButton(props) {
|
||||||
};
|
};
|
||||||
restProps.tabIndex = restProps.tabIndex || "0";
|
restProps.tabIndex = restProps.tabIndex || "0";
|
||||||
restProps.role = "button";
|
restProps.role = "button";
|
||||||
restProps.className = (restProps.className ? restProps.className + " " : "") +
|
restProps.className = (restProps.className ? restProps.className + " " : "") +
|
||||||
"mx_AccessibleButton";
|
"mx_AccessibleButton";
|
||||||
return React.createElement(element, restProps, children);
|
return React.createElement(element, restProps, children);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
|
||||||
import AccessibleButton from './AccessibleButton';
|
import AccessibleButton from './AccessibleButton';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import Analytics from '../../../Analytics';
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'RoleButton',
|
displayName: 'RoleButton',
|
||||||
|
@ -47,6 +48,7 @@ export default React.createClass({
|
||||||
|
|
||||||
_onClick: function(ev) {
|
_onClick: function(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
Analytics.trackEvent('Action Button', 'click', this.props.action);
|
||||||
dis.dispatch({action: this.props.action});
|
dis.dispatch({action: this.props.action});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -77,8 +79,8 @@ export default React.createClass({
|
||||||
onMouseLeave={this._onMouseLeave}
|
onMouseLeave={this._onMouseLeave}
|
||||||
>
|
>
|
||||||
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
|
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
|
||||||
{tooltip}
|
{ tooltip }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -30,6 +30,8 @@ export default React.createClass({
|
||||||
|
|
||||||
// List of the addresses to display
|
// List of the addresses to display
|
||||||
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
|
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
|
||||||
|
// Whether to show the address on the address tiles
|
||||||
|
showAddress: React.PropTypes.bool,
|
||||||
truncateAt: React.PropTypes.number.isRequired,
|
truncateAt: React.PropTypes.number.isRequired,
|
||||||
selected: React.PropTypes.number,
|
selected: React.PropTypes.number,
|
||||||
|
|
||||||
|
@ -46,8 +48,8 @@ export default React.createClass({
|
||||||
|
|
||||||
componentWillReceiveProps: function(props) {
|
componentWillReceiveProps: function(props) {
|
||||||
// Make sure the selected item isn't outside the list bounds
|
// Make sure the selected item isn't outside the list bounds
|
||||||
var selected = this.state.selected;
|
const selected = this.state.selected;
|
||||||
var maxSelected = this._maxSelected(props.addressList);
|
const maxSelected = this._maxSelected(props.addressList);
|
||||||
if (selected > maxSelected) {
|
if (selected > maxSelected) {
|
||||||
this.setState({ selected: maxSelected });
|
this.setState({ selected: maxSelected });
|
||||||
}
|
}
|
||||||
|
@ -57,7 +59,7 @@ export default React.createClass({
|
||||||
// As the user scrolls with the arrow keys keep the selected item
|
// As the user scrolls with the arrow keys keep the selected item
|
||||||
// at the top of the window.
|
// at the top of the window.
|
||||||
if (this.scrollElement && this.props.addressList.length > 0 && !this.state.hover) {
|
if (this.scrollElement && this.props.addressList.length > 0 && !this.state.hover) {
|
||||||
var elementHeight = this.addressListElement.getBoundingClientRect().height;
|
const elementHeight = this.addressListElement.getBoundingClientRect().height;
|
||||||
this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight;
|
this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -75,7 +77,7 @@ export default React.createClass({
|
||||||
if (this.state.selected > 0) {
|
if (this.state.selected > 0) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selected: this.state.selected - 1,
|
selected: this.state.selected - 1,
|
||||||
hover : false,
|
hover: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -84,7 +86,7 @@ export default React.createClass({
|
||||||
if (this.state.selected < this._maxSelected(this.props.addressList)) {
|
if (this.state.selected < this._maxSelected(this.props.addressList)) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selected: this.state.selected + 1,
|
selected: this.state.selected + 1,
|
||||||
hover : false,
|
hover: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -105,7 +107,7 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseLeave: function() {
|
onMouseLeave: function() {
|
||||||
this.setState({ hover : false });
|
this.setState({ hover: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
selectAddress: function(index) {
|
selectAddress: function(index) {
|
||||||
|
@ -117,15 +119,15 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
createAddressListTiles: function() {
|
createAddressListTiles: function() {
|
||||||
var self = this;
|
const self = this;
|
||||||
var AddressTile = sdk.getComponent("elements.AddressTile");
|
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||||
var maxSelected = this._maxSelected(this.props.addressList);
|
const maxSelected = this._maxSelected(this.props.addressList);
|
||||||
var addressList = [];
|
const addressList = [];
|
||||||
|
|
||||||
// Only create the address elements if there are address
|
// Only create the address elements if there are address
|
||||||
if (this.props.addressList.length > 0) {
|
if (this.props.addressList.length > 0) {
|
||||||
for (var i = 0; i <= maxSelected; i++) {
|
for (let i = 0; i <= maxSelected; i++) {
|
||||||
var classes = classNames({
|
const classes = classNames({
|
||||||
"mx_AddressSelector_addressListElement": true,
|
"mx_AddressSelector_addressListElement": true,
|
||||||
"mx_AddressSelector_selected": this.state.selected === i,
|
"mx_AddressSelector_selected": this.state.selected === i,
|
||||||
});
|
});
|
||||||
|
@ -142,8 +144,14 @@ export default React.createClass({
|
||||||
key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address}
|
key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address}
|
||||||
ref={(ref) => { this.addressListElement = ref; }}
|
ref={(ref) => { this.addressListElement = ref; }}
|
||||||
>
|
>
|
||||||
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
<AddressTile
|
||||||
</div>
|
address={this.props.addressList[i]}
|
||||||
|
showAddress={this.props.showAddress}
|
||||||
|
justified={true}
|
||||||
|
networkName="vector"
|
||||||
|
networkUrl="img/search-icon-vector.svg"
|
||||||
|
/>
|
||||||
|
</div>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,13 +159,13 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_maxSelected: function(list) {
|
_maxSelected: function(list) {
|
||||||
var listSize = list.length === 0 ? 0 : list.length - 1;
|
const listSize = list.length === 0 ? 0 : list.length - 1;
|
||||||
var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
|
const maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
|
||||||
return maxSelected;
|
return maxSelected;
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var classes = classNames({
|
const classes = classNames({
|
||||||
"mx_AddressSelector": true,
|
"mx_AddressSelector": true,
|
||||||
"mx_AddressSelector_empty": this.props.addressList.length === 0,
|
"mx_AddressSelector_empty": this.props.addressList.length === 0,
|
||||||
});
|
});
|
||||||
|
@ -168,5 +176,5 @@ export default React.createClass({
|
||||||
{ this.createAddressListTiles() }
|
{ this.createAddressListTiles() }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -45,11 +45,12 @@ export default React.createClass({
|
||||||
const address = this.props.address;
|
const address = this.props.address;
|
||||||
const name = address.displayName || address.address;
|
const name = address.displayName || address.address;
|
||||||
|
|
||||||
let imgUrls = [];
|
const imgUrls = [];
|
||||||
|
const isMatrixAddress = ['mx-user-id', 'mx-room-id'].includes(address.addressType);
|
||||||
|
|
||||||
if (address.addressType === "mx" && address.avatarMxc) {
|
if (isMatrixAddress && address.avatarMxc) {
|
||||||
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
|
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
|
||||||
address.avatarMxc, 25, 25, 'crop'
|
address.avatarMxc, 25, 25, 'crop',
|
||||||
));
|
));
|
||||||
} else if (address.addressType === 'email') {
|
} else if (address.addressType === 'email') {
|
||||||
imgUrls.push('img/icon-email-user.svg');
|
imgUrls.push('img/icon-email-user.svg');
|
||||||
|
@ -77,7 +78,7 @@ export default React.createClass({
|
||||||
|
|
||||||
let info;
|
let info;
|
||||||
let error = false;
|
let error = false;
|
||||||
if (address.addressType === "mx" && address.isKnown) {
|
if (isMatrixAddress && address.isKnown) {
|
||||||
const idClasses = classNames({
|
const idClasses = classNames({
|
||||||
"mx_AddressTile_id": true,
|
"mx_AddressTile_id": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
|
@ -86,10 +87,13 @@ export default React.createClass({
|
||||||
info = (
|
info = (
|
||||||
<div className="mx_AddressTile_mx">
|
<div className="mx_AddressTile_mx">
|
||||||
<div className={nameClasses}>{ name }</div>
|
<div className={nameClasses}>{ name }</div>
|
||||||
<div className={idClasses}>{ address.address }</div>
|
{ this.props.showAddress ?
|
||||||
|
<div className={idClasses}>{ address.address }</div> :
|
||||||
|
<div />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (address.addressType === "mx") {
|
} else if (isMatrixAddress) {
|
||||||
const unknownMxClasses = classNames({
|
const unknownMxClasses = classNames({
|
||||||
"mx_AddressTile_unknownMx": true,
|
"mx_AddressTile_unknownMx": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
|
@ -106,24 +110,24 @@ export default React.createClass({
|
||||||
|
|
||||||
let nameNode = null;
|
let nameNode = null;
|
||||||
if (address.displayName) {
|
if (address.displayName) {
|
||||||
nameNode = <div className={nameClasses}>{ address.displayName }</div>
|
nameNode = <div className={nameClasses}>{ address.displayName }</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
info = (
|
info = (
|
||||||
<div className="mx_AddressTile_mx">
|
<div className="mx_AddressTile_mx">
|
||||||
<div className={emailClasses}>{ address.address }</div>
|
<div className={emailClasses}>{ address.address }</div>
|
||||||
{nameNode}
|
{ nameNode }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
error = true;
|
error = true;
|
||||||
var unknownClasses = classNames({
|
const unknownClasses = classNames({
|
||||||
"mx_AddressTile_unknown": true,
|
"mx_AddressTile_unknown": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
});
|
});
|
||||||
|
|
||||||
info = (
|
info = (
|
||||||
<div className={unknownClasses}>{_t("Unknown Address")}</div>
|
<div className={unknownClasses}>{ _t("Unknown Address") }</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,5 +154,5 @@ export default React.createClass({
|
||||||
{ dismiss }
|
{ dismiss }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -50,16 +50,16 @@ export default class AppPermission extends React.Component {
|
||||||
let e2eWarningText;
|
let e2eWarningText;
|
||||||
if (this.props.isRoomEncrypted) {
|
if (this.props.isRoomEncrypted) {
|
||||||
e2eWarningText =
|
e2eWarningText =
|
||||||
<span className='mx_AppPermissionWarningTextLabel'>{_t('NOTE: Apps are not end-to-end encrypted')}</span>;
|
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className='mx_AppPermissionWarning'>
|
<div className='mx_AppPermissionWarning'>
|
||||||
<div className='mx_AppPermissionWarningImage'>
|
<div className='mx_AppPermissionWarningImage'>
|
||||||
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
<img src='img/warning.svg' alt={_t('Warning!')} />
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_AppPermissionWarningText'>
|
<div className='mx_AppPermissionWarningText'>
|
||||||
<span className='mx_AppPermissionWarningTextLabel'>{_t('Do you want to load widget from URL:')}</span> <span className='mx_AppPermissionWarningTextURL'>{this.state.curlBase}</span>
|
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
|
||||||
{e2eWarningText}
|
{ e2eWarningText }
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
className='mx_AppPermissionButton'
|
className='mx_AppPermissionButton'
|
||||||
|
|
|
@ -19,10 +19,11 @@ limitations under the License.
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
import PlatformPeg from '../../../PlatformPeg';
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import AppPermission from './AppPermission';
|
import AppPermission from './AppPermission';
|
||||||
import AppWarning from './AppWarning';
|
import AppWarning from './AppWarning';
|
||||||
|
@ -72,8 +73,17 @@ export default React.createClass({
|
||||||
|
|
||||||
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
|
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
|
||||||
isScalarUrl: function() {
|
isScalarUrl: function() {
|
||||||
const scalarUrl = SdkConfig.get().integrations_rest_url;
|
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
|
||||||
return scalarUrl && this.props.url.startsWith(scalarUrl);
|
if (!scalarUrls || scalarUrls.length == 0) {
|
||||||
|
scalarUrls = [SdkConfig.get().integrations_rest_url];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < scalarUrls.length; i++) {
|
||||||
|
if (this.props.url.startsWith(scalarUrls[i])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
isMixedContent: function() {
|
isMixedContent: function() {
|
||||||
|
@ -118,6 +128,30 @@ export default React.createClass({
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
window.addEventListener('message', this._onMessage, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('message', this._onMessage);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onMessage(event) {
|
||||||
|
if (this.props.type !== 'jitsi') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!event.origin) {
|
||||||
|
event.origin = event.originalEvent.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.widgetUrl.startsWith(event.origin)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.widgetAction === 'jitsi_iframe_loaded') {
|
||||||
|
const iframe = this.refs.appFrame.contentWindow
|
||||||
|
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
|
||||||
|
PlatformPeg.get().setupScreenSharingForIframe(iframe);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_canUserModify: function() {
|
_canUserModify: function() {
|
||||||
|
@ -161,9 +195,9 @@ export default React.createClass({
|
||||||
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
||||||
_deleteWidgetLabel() {
|
_deleteWidgetLabel() {
|
||||||
if (this._canUserModify()) {
|
if (this._canUserModify()) {
|
||||||
return 'Delete widget';
|
return _td('Delete widget');
|
||||||
}
|
}
|
||||||
return 'Revoke widget access';
|
return _td('Revoke widget access');
|
||||||
},
|
},
|
||||||
|
|
||||||
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
||||||
|
@ -227,7 +261,7 @@ export default React.createClass({
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div className='mx_AppTileBody mx_AppLoading'>
|
<div className='mx_AppTileBody mx_AppLoading'>
|
||||||
<MessageSpinner msg='Loading...'/>
|
<MessageSpinner msg='Loading...' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.state.hasPermissionToLoad == true) {
|
} else if (this.state.hasPermissionToLoad == true) {
|
||||||
|
@ -278,19 +312,19 @@ export default React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||||
{this.formatAppTileName()}
|
{ this.formatAppTileName() }
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
{/* Edit widget */}
|
{ /* Edit widget */ }
|
||||||
{showEditButton && <img
|
{ showEditButton && <img
|
||||||
src="img/edit.svg"
|
src="img/edit.svg"
|
||||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||||
width="8" height="8"
|
width="8" height="8"
|
||||||
alt={_t('Edit')}
|
alt={_t('Edit')}
|
||||||
title={_t('Edit')}
|
title={_t('Edit')}
|
||||||
onClick={this._onEditClick}
|
onClick={this._onEditClick}
|
||||||
/>}
|
/> }
|
||||||
|
|
||||||
{/* Delete widget */}
|
{ /* Delete widget */ }
|
||||||
<img src={deleteIcon}
|
<img src={deleteIcon}
|
||||||
className={deleteClasses}
|
className={deleteClasses}
|
||||||
width="8" height="8"
|
width="8" height="8"
|
||||||
|
@ -300,7 +334,7 @@ export default React.createClass({
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{appTileBody}
|
{ appTileBody }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,10 +6,10 @@ const AppWarning = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className='mx_AppPermissionWarning'>
|
<div className='mx_AppPermissionWarning'>
|
||||||
<div className='mx_AppPermissionWarningImage'>
|
<div className='mx_AppPermissionWarningImage'>
|
||||||
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
<img src='img/warning.svg' alt={_t('Warning!')} />
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_AppPermissionWarningText'>
|
<div className='mx_AppPermissionWarningText'>
|
||||||
<span className='mx_AppPermissionWarningTextLabel'>{props.errorMsg}</span>
|
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,7 +24,7 @@ const CreateRoomButton = function(props) {
|
||||||
return (
|
return (
|
||||||
<ActionButton action="view_create_room"
|
<ActionButton action="view_create_room"
|
||||||
mouseOverAction={props.callout ? "callout_create_room" : null}
|
mouseOverAction={props.callout ? "callout_create_room" : null}
|
||||||
label={ _t("Create new room") }
|
label={_t("Create new room")}
|
||||||
iconPath="img/icons-create-room.svg"
|
iconPath="img/icons-create-room.svg"
|
||||||
size={props.size}
|
size={props.size}
|
||||||
tooltip={props.tooltip}
|
tooltip={props.tooltip}
|
||||||
|
|
|
@ -30,7 +30,7 @@ export default React.createClass({
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
device: this.props.device
|
device: this.props.device,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -60,37 +60,37 @@ export default React.createClass({
|
||||||
|
|
||||||
onUnverifyClick: function() {
|
onUnverifyClick: function() {
|
||||||
MatrixClientPeg.get().setDeviceVerified(
|
MatrixClientPeg.get().setDeviceVerified(
|
||||||
this.props.userId, this.state.device.deviceId, false
|
this.props.userId, this.state.device.deviceId, false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
onBlacklistClick: function() {
|
onBlacklistClick: function() {
|
||||||
MatrixClientPeg.get().setDeviceBlocked(
|
MatrixClientPeg.get().setDeviceBlocked(
|
||||||
this.props.userId, this.state.device.deviceId, true
|
this.props.userId, this.state.device.deviceId, true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnblacklistClick: function() {
|
onUnblacklistClick: function() {
|
||||||
MatrixClientPeg.get().setDeviceBlocked(
|
MatrixClientPeg.get().setDeviceBlocked(
|
||||||
this.props.userId, this.state.device.deviceId, false
|
this.props.userId, this.state.device.deviceId, false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var blacklistButton = null, verifyButton = null;
|
let blacklistButton = null, verifyButton = null;
|
||||||
|
|
||||||
if (this.state.device.isBlocked()) {
|
if (this.state.device.isBlocked()) {
|
||||||
blacklistButton = (
|
blacklistButton = (
|
||||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
|
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
|
||||||
onClick={this.onUnblacklistClick}>
|
onClick={this.onUnblacklistClick}>
|
||||||
{_t("Unblacklist")}
|
{ _t("Unblacklist") }
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
blacklistButton = (
|
blacklistButton = (
|
||||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
|
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
|
||||||
onClick={this.onBlacklistClick}>
|
onClick={this.onBlacklistClick}>
|
||||||
{_t("Blacklist")}
|
{ _t("Blacklist") }
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -99,14 +99,14 @@ export default React.createClass({
|
||||||
verifyButton = (
|
verifyButton = (
|
||||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
|
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
|
||||||
onClick={this.onUnverifyClick}>
|
onClick={this.onUnverifyClick}>
|
||||||
{_t("Unverify")}
|
{ _t("Unverify") }
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
verifyButton = (
|
verifyButton = (
|
||||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
|
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
|
||||||
onClick={this.onVerifyClick}>
|
onClick={this.onVerifyClick}>
|
||||||
{_t("Verify...")}
|
{ _t("Verify...") }
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ export default class DirectorySearchBox extends React.Component {
|
||||||
onChange={this._onChange} onKeyUp={this._onKeyUp}
|
onChange={this._onChange} onKeyUp={this._onKeyUp}
|
||||||
placeholder={this.props.placeholder} autoFocus
|
placeholder={this.props.placeholder} autoFocus
|
||||||
/>
|
/>
|
||||||
{join_button}
|
{ join_button }
|
||||||
<span className="mx_DirectorySearchBox_clear_wrapper">
|
<span className="mx_DirectorySearchBox_clear_wrapper">
|
||||||
<span className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} />
|
<span className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} />
|
||||||
</span>
|
</span>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue