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

This commit is contained in:
Richard Lewis 2017-10-19 16:26:22 +01:00
commit a49eabda4c
253 changed files with 13210 additions and 9039 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
View 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"
);

View file

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

View file

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

View file

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

View file

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

View file

@ -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
View 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(", "),
});
});
}

View file

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

View file

@ -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);
} }

View file

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

View file

@ -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);
} }

View file

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

View file

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

View file

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

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var 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);

View file

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

View file

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

View file

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

View file

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

View file

@ -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);
} }

View file

@ -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();
} }

View file

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

View file

@ -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 '';
} },
}; };

View file

@ -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");
} },
}; };

View file

@ -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;
} },
}; };

View file

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

View file

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

View file

@ -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>
); );
}, },

View file

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

View file

@ -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});
} }
} },
}; };

View file

@ -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>
); );
} },
}); });

View file

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

View file

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

View file

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

View file

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

View file

@ -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>
); );
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>
); );
} }
} },
}); });

View file

@ -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>
); );
} }

View file

@ -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>
); );
} }

View file

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

View file

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

View file

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

View file

@ -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>
); );
}, },

View file

@ -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>;
}, },

View file

@ -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>
); );
} }

View file

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

View file

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

View file

@ -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}
/> />
); );
}, },

View file

@ -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>
); );
} },
}); });

View file

@ -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()}>
&lt;{ _t("click to reveal") }&gt; &lt;{ _t("click to reveal") }&gt;
</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>

View file

@ -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&#39;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>
); );
} },
}); });

View file

@ -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>
); );
} },
}); });

View file

@ -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>
); );
} },
}); });

View file

@ -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>
); );
} },
}); });

View file

@ -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 its the start of a surrogate pair // check if its 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} />
); );
} }
} },
}); });

View file

@ -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} />
); );
} },
}); });

View file

@ -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} />
); );
} },
}); });

View file

@ -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>
); );
} },
}); });

View file

@ -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>
); );
} },
}); });

View file

@ -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} />
); );
} },
}); });

View file

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

View file

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

View file

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

View file

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

View file

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

View 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>
);
},
});

View file

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

View file

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

View file

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

View file

@ -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>
); );
}, },

View file

@ -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>
); );
}, },

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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);
} }

View file

@ -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>
); );
} },
}); });

View file

@ -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>
); );
} },
}); });

View file

@ -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>
); );
} },
}); });

View file

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

View file

@ -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>
); );
}, },

View file

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

View file

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

View file

@ -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>
); );
} }

View file

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