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.
src/async-components/views/dialogs/EncryptedEventDialog.js
src/autocomplete/AutocompleteProvider.js
src/autocomplete/Autocompleter.js
src/autocomplete/Components.js
src/autocomplete/DuckDuckGoProvider.js
src/autocomplete/EmojiProvider.js
src/autocomplete/RoomProvider.js
src/autocomplete/UserProvider.js
src/CallHandler.js
src/component-index.js
src/components/structures/ContextualMenu.js
src/components/structures/CreateRoom.js
src/components/structures/FilePanel.js
src/components/structures/InteractiveAuth.js
src/components/structures/LoggedInView.js
src/components/structures/login/ForgotPassword.js
src/components/structures/login/Login.js
src/components/structures/login/PostRegistration.js
src/components/structures/login/Registration.js
src/components/structures/MessagePanel.js
src/components/structures/NotificationPanel.js
@ -28,53 +22,32 @@ src/components/structures/TimelinePanel.js
src/components/structures/UploadBar.js
src/components/views/avatars/BaseAvatar.js
src/components/views/avatars/MemberAvatar.js
src/components/views/avatars/RoomAvatar.js
src/components/views/create_room/CreateRoomButton.js
src/components/views/create_room/Presets.js
src/components/views/create_room/RoomAlias.js
src/components/views/dialogs/ChatCreateOrReuseDialog.js
src/components/views/dialogs/DeactivateAccountDialog.js
src/components/views/dialogs/InteractiveAuthDialog.js
src/components/views/dialogs/SetMxIdDialog.js
src/components/views/dialogs/UnknownDeviceDialog.js
src/components/views/elements/AccessibleButton.js
src/components/views/elements/ActionButton.js
src/components/views/elements/AddressSelector.js
src/components/views/elements/AddressTile.js
src/components/views/elements/CreateRoomButton.js
src/components/views/elements/DeviceVerifyButtons.js
src/components/views/elements/DirectorySearchBox.js
src/components/views/elements/Dropdown.js
src/components/views/elements/EditableText.js
src/components/views/elements/EditableTextContainer.js
src/components/views/elements/HomeButton.js
src/components/views/elements/LanguageDropdown.js
src/components/views/elements/MemberEventListSummary.js
src/components/views/elements/PowerSelector.js
src/components/views/elements/ProgressBar.js
src/components/views/elements/RoomDirectoryButton.js
src/components/views/elements/SettingsButton.js
src/components/views/elements/StartChatButton.js
src/components/views/elements/TintableSvg.js
src/components/views/elements/TruncatedList.js
src/components/views/elements/UserSelector.js
src/components/views/login/CaptchaForm.js
src/components/views/login/CasLogin.js
src/components/views/login/CountryDropdown.js
src/components/views/login/CustomServerDialog.js
src/components/views/login/InteractiveAuthEntryComponents.js
src/components/views/login/LoginHeader.js
src/components/views/login/PasswordLogin.js
src/components/views/login/RegistrationForm.js
src/components/views/login/ServerConfig.js
src/components/views/messages/MAudioBody.js
src/components/views/messages/MessageEvent.js
src/components/views/messages/MFileBody.js
src/components/views/messages/MImageBody.js
src/components/views/messages/MVideoBody.js
src/components/views/messages/RoomAvatarEvent.js
src/components/views/messages/TextualBody.js
src/components/views/messages/TextualEvent.js
src/components/views/room_settings/AliasSettings.js
src/components/views/room_settings/ColorSettings.js
src/components/views/room_settings/UrlPreviewSettings.js
@ -89,18 +62,13 @@ src/components/views/rooms/MemberList.js
src/components/views/rooms/MemberTile.js
src/components/views/rooms/MessageComposer.js
src/components/views/rooms/MessageComposerInput.js
src/components/views/rooms/MessageComposerInputOld.js
src/components/views/rooms/PresenceLabel.js
src/components/views/rooms/ReadReceiptMarker.js
src/components/views/rooms/RoomList.js
src/components/views/rooms/RoomNameEditor.js
src/components/views/rooms/RoomPreviewBar.js
src/components/views/rooms/RoomSettings.js
src/components/views/rooms/RoomTile.js
src/components/views/rooms/RoomTopicEditor.js
src/components/views/rooms/SearchableEntityList.js
src/components/views/rooms/SearchResultTile.js
src/components/views/rooms/TabCompleteBar.js
src/components/views/rooms/TopUnreadMessagesBar.js
src/components/views/rooms/UserTile.js
src/components/views/settings/AddPhoneNumber.js
@ -108,8 +76,6 @@ src/components/views/settings/ChangeAvatar.js
src/components/views/settings/ChangeDisplayName.js
src/components/views/settings/ChangePassword.js
src/components/views/settings/DevicesPanel.js
src/components/views/settings/DevicesPanelEntry.js
src/components/views/settings/EnableNotificationsButton.js
src/ContentMessages.js
src/HtmlUtils.js
src/ImageUtils.js
@ -127,10 +93,6 @@ src/RichText.js
src/Roles.js
src/Rooms.js
src/ScalarAuthClient.js
src/ScalarMessaging.js
src/TabComplete.js
src/TabCompleteEntries.js
src/TextForEvent.js
src/Tinter.js
src/UiEffects.js
src/Unread.js
@ -142,18 +104,14 @@ src/utils/Receipt.js
src/Velociraptor.js
src/VelocityBounce.js
src/WhoIsTyping.js
src/wrappers/WithMatrixClient.js
test/all-tests.js
src/wrappers/withMatrixClient.js
test/components/structures/login/Registration-test.js
test/components/structures/MessagePanel-test.js
test/components/structures/ScrollPanel-test.js
test/components/structures/TimelinePanel-test.js
test/components/stub-component.js
test/components/views/dialogs/InteractiveAuthDialog-test.js
test/components/views/elements/MemberEventListSummary-test.js
test/components/views/login/RegistrationForm-test.js
test/components/views/rooms/MessageComposerInput-test.js
test/mock-clock.js
test/skinned-sdk.js
test/stores/RoomViewStore-test.js
test/test-utils.js

View file

@ -40,6 +40,19 @@ module.exports = {
}],
"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/require-parameter-type": ["warn", {
"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)
=====================================================================================================
[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
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
(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
# re-run the linter, excluding any files known to have errors or warnings.
./node_modules/.bin/eslint --max-warnings 0 \
--ignore-path .eslintignore.errorfiles \
src test
npm run lintwithexclusions
# delete the old tarball, if it exists
rm -f matrix-react-sdk-*.tgz

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "0.10.2",
"version": "0.10.7",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -28,19 +28,22 @@
"test"
],
"bin": {
"reskindex": "scripts/reskindex.js"
"reskindex": "scripts/reskindex.js",
"matrix-gen-i18n": "scripts/gen-i18n.js"
},
"scripts": {
"reskindex": "node scripts/reskindex.js -h header",
"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:watch": "babel src -w -d lib --source-maps --copy-files",
"emoji-data-strip": "node scripts/emoji-data-strip.js",
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
"lint": "eslint src/",
"lintall": "eslint src/ test/",
"lintwithexclusions": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
"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-multi": "karma start"
},
@ -66,7 +69,7 @@
"isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.3",
"lodash": "^4.13.1",
"matrix-js-sdk": "0.8.2",
"matrix-js-sdk": "0.8.5",
"optimist": "^0.6.1",
"prop-types": "^15.5.8",
"react": "^15.4.0",
@ -99,8 +102,10 @@
"eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^4.0.1",
"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",
"flow-parser": "^0.57.3",
"json-loader": "^0.5.3",
"karma": "^1.7.0",
"karma-chrome-launcher": "^0.2.3",
@ -120,6 +125,7 @@
"rimraf": "^2.4.3",
"sinon": "^1.17.3",
"source-map-loader": "^0.1.5",
"walk": "^2.3.9",
"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
# run the linter, but exclude any files known to have errors or warnings.
./node_modules/.bin/eslint --max-warnings 0 \
--ignore-path .eslintignore.errorfiles \
src test
npm run lintwithexclusions

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; }
setupScreenSharingForIframe() {
}
/**
* Restarts the application, without neccessarily reloading
* any application code

View file

@ -63,23 +63,22 @@ import dis from './dispatcher';
global.mxCalls = {
//room_id: MatrixCall
};
var calls = global.mxCalls;
var ConferenceHandler = null;
const calls = global.mxCalls;
let ConferenceHandler = null;
var audioPromises = {};
const audioPromises = {};
function play(audioId) {
// TODO: Attach an invisible element for this instead
// which listens?
var audio = document.getElementById(audioId);
const audio = document.getElementById(audioId);
if (audio) {
if (audioPromises[audioId]) {
audioPromises[audioId] = audioPromises[audioId].then(()=>{
audio.load();
return audio.play();
});
}
else {
} else {
audioPromises[audioId] = audio.play();
}
}
@ -88,12 +87,11 @@ function play(audioId) {
function pause(audioId) {
// TODO: Attach an invisible element for this instead
// which listens?
var audio = document.getElementById(audioId);
const audio = document.getElementById(audioId);
if (audio) {
if (audioPromises[audioId]) {
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();
audioPromises[audioId] = audio.pause();
}
@ -125,38 +123,32 @@ function _setCallListeners(call) {
if (newState === "ringing") {
_setCallState(call, call.roomId, "ringing");
pause("ringbackAudio");
}
else if (newState === "invite_sent") {
} else if (newState === "invite_sent") {
_setCallState(call, call.roomId, "ringback");
play("ringbackAudio");
}
else if (newState === "ended" && oldState === "connected") {
} else if (newState === "ended" && oldState === "connected") {
_setCallState(undefined, call.roomId, "ended");
pause("ringbackAudio");
play("callendAudio");
}
else if (newState === "ended" && oldState === "invite_sent" &&
} else if (newState === "ended" && oldState === "invite_sent" &&
(call.hangupParty === "remote" ||
(call.hangupParty === "local" && call.hangupReason === "invite_timeout")
)) {
_setCallState(call, call.roomId, "busy");
pause("ringbackAudio");
play("busyAudio");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
title: _t('Call Timeout'),
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");
pause("ringbackAudio");
}
else if (oldState === "ringing") {
} else if (oldState === "ringing") {
_setCallState(call, call.roomId, "stop_ringing");
pause("ringbackAudio");
}
else if (newState === "connected") {
} else if (newState === "connected") {
_setCallState(call, call.roomId, "connected");
pause("ringbackAudio");
}
@ -165,14 +157,13 @@ function _setCallListeners(call) {
function _setCallState(call, roomId, status) {
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;
if (status === "ringing") {
play("ringAudio");
}
else if (call && call.call_state === "ringing") {
} else if (call && call.call_state === "ringing") {
pause("ringAudio");
}
@ -192,14 +183,12 @@ function _onAction(payload) {
_setCallState(newCall, newCall.roomId, "ringback");
if (payload.type === 'voice') {
newCall.placeVoiceCall();
}
else if (payload.type === 'video') {
} else if (payload.type === 'video') {
newCall.placeVideoCall(
payload.remote_element,
payload.local_element
payload.local_element,
);
}
else if (payload.type === 'screensharing') {
} else if (payload.type === 'screensharing') {
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
if (screenCapErrorString) {
_setCallState(undefined, newCall.roomId, "ended");
@ -213,10 +202,9 @@ function _onAction(payload) {
}
newCall.placeScreenSharingCall(
payload.remote_element,
payload.local_element
payload.local_element,
);
}
else {
} else {
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.'),
});
return;
}
else if (members.length === 2) {
} else if (members.length === 2) {
console.log("Place %s call in %s", payload.type, payload.room_id);
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, {
forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false),
});
placeCall(call);
}
else { // > 2
} else { // > 2
dis.dispatch({
action: "place_conference_call",
room_id: payload.room_id,
type: payload.type,
remote_element: payload.remote_element,
local_element: payload.local_element
local_element: payload.local_element,
});
}
break;
@ -280,15 +266,13 @@ function _onAction(payload) {
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
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");
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
title: _t('VoIP is unsupported'),
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
// server which combines the audio from all the participants together
// 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, {
description: _t('Conference calls are not supported in encrypted rooms'),
});
}
else {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
} else {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
title: _t('Warning!'),
description: _t('Conference calling is in development and may not be reliable.'),
onFinished: confirm=>{
onFinished: (confirm)=>{
if (confirm) {
ConferenceHandler.createNewMatrixCall(
MatrixClientPeg.get(), payload.room_id
MatrixClientPeg.get(), payload.room_id,
).done(function(call) {
placeCall(call);
}, function(err) {
@ -357,7 +340,7 @@ function _onAction(payload) {
_setCallState(calls[payload.room_id], payload.room_id, "connected");
dis.dispatch({
action: "view_room",
room_id: payload.room_id
room_id: payload.room_id,
});
break;
}
@ -368,9 +351,9 @@ if (!global.mxCallHandler) {
dis.register(_onAction);
}
var callHandler = {
const callHandler = {
getCallForRoom: function(roomId) {
var call = module.exports.getCall(roomId);
let call = module.exports.getCall(roomId);
if (call) return call;
if (ConferenceHandler) {
@ -386,8 +369,8 @@ var callHandler = {
},
getAnyActiveCall: function() {
var roomsWithCalls = Object.keys(calls);
for (var i = 0; i < roomsWithCalls.length; i++) {
const roomsWithCalls = Object.keys(calls);
for (let i = 0; i < roomsWithCalls.length; i++) {
if (calls[roomsWithCalls[i]] &&
calls[roomsWithCalls[i]].call_state !== "ended") {
return calls[roomsWithCalls[i]];
@ -402,7 +385,7 @@ var callHandler = {
getConferenceHandler: function() {
return ConferenceHandler;
}
},
};
// Only things in here which actually need to be global are the
// calls list (done separately) and making sure we only register

View file

@ -17,14 +17,14 @@ limitations under the License.
'use strict';
import Promise from 'bluebird';
var extend = require('./extend');
var dis = require('./dispatcher');
var MatrixClientPeg = require('./MatrixClientPeg');
var sdk = require('./index');
const extend = require('./extend');
const dis = require('./dispatcher');
const MatrixClientPeg = require('./MatrixClientPeg');
const sdk = require('./index');
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
require("blueimp-canvas-to-blob");
@ -54,8 +54,8 @@ const MAX_HEIGHT = 600;
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
const deferred = Promise.defer();
var targetWidth = inputWidth;
var targetHeight = inputHeight;
let targetWidth = inputWidth;
let targetHeight = inputHeight;
if (targetHeight > MAX_HEIGHT) {
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
targetHeight = MAX_HEIGHT;
@ -81,7 +81,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
w: inputWidth,
h: inputHeight,
},
thumbnail: thumbnail
thumbnail: thumbnail,
});
}, mimeType);
@ -129,12 +129,12 @@ function loadImageElement(imageFile) {
* @return {Promise} A promise that resolves with the attachment info.
*/
function infoForImageFile(matrixClient, roomId, imageFile) {
var thumbnailType = "image/png";
let thumbnailType = "image/png";
if (imageFile.type == "image/jpeg") {
thumbnailType = "image/jpeg";
}
var imageInfo;
let imageInfo;
return loadImageElement(imageFile).then(function(img) {
return createThumbnail(img, img.width, img.height, thumbnailType);
}).then(function(result) {
@ -191,7 +191,7 @@ function loadVideoElement(videoFile) {
function infoForVideoFile(matrixClient, roomId, videoFile) {
const thumbnailType = "image/jpeg";
var videoInfo;
let videoInfo;
return loadVideoElement(videoFile).then(function(video) {
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
}).then(function(result) {
@ -286,7 +286,7 @@ class ContentMessages {
body: file.name || 'Attachment',
info: {
size: file.size,
}
},
};
// 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();
if (file.type.indexOf('image/') == 0) {
content.msgtype = 'm.image';
infoForImageFile(matrixClient, roomId, file).then(imageInfo=>{
infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{
extend(content.info, imageInfo);
def.resolve();
}, error=>{
}, (error)=>{
console.error(error);
content.msgtype = 'm.file';
def.resolve();
@ -310,10 +310,10 @@ class ContentMessages {
def.resolve();
} else if (file.type.indexOf('video/') == 0) {
content.msgtype = 'm.video';
infoForVideoFile(matrixClient, roomId, file).then(videoInfo=>{
infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{
extend(content.info, videoInfo);
def.resolve();
}, error=>{
}, (error)=>{
content.msgtype = 'm.file';
def.resolve();
});
@ -331,7 +331,7 @@ class ContentMessages {
this.inprogress.push(upload);
dis.dispatch({action: 'upload_started'});
var error;
let error;
function onProgress(ev) {
upload.total = ev.total;
@ -355,11 +355,11 @@ class ContentMessages {
}, function(err) {
error = err;
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) {
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, {
title: _t('Upload Failed'),
description: desc,
@ -367,8 +367,8 @@ class ContentMessages {
}
}).finally(() => {
const inprogressKeys = Object.keys(this.inprogress);
for (var i = 0; i < this.inprogress.length; ++i) {
var k = inprogressKeys[i];
for (let i = 0; i < this.inprogress.length; ++i) {
const k = inprogressKeys[i];
if (this.inprogress[k].promise === upload.promise) {
this.inprogress.splice(k, 1);
break;
@ -376,8 +376,7 @@ class ContentMessages {
}
if (error) {
dis.dispatch({action: 'upload_failed', upload: upload});
}
else {
} else {
dis.dispatch({action: 'upload_finished', upload: upload});
}
});
@ -389,9 +388,9 @@ class ContentMessages {
cancelUpload(promise) {
const inprogressKeys = Object.keys(this.inprogress);
var upload;
for (var i = 0; i < this.inprogress.length; ++i) {
var k = inprogressKeys[i];
let upload;
for (let i = 0; i < this.inprogress.length; ++i) {
const k = inprogressKeys[i];
if (this.inprogress[k].promise === promise) {
upload = this.inprogress[k];
break;

View file

@ -65,7 +65,7 @@ module.exports = {
const days = getDaysArray();
const months = getMonthsArray();
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) {
// TODO: use standard date localize function provided in counterpart
return _t('%(weekDayName)s %(time)s', {
@ -78,7 +78,7 @@ module.exports = {
weekDayName: days[date.getDay()],
monthName: months[date.getMonth()],
day: date.getDate(),
time: this.formatTime(date),
time: this.formatTime(date, showTwelveHour),
});
}
return this.formatFullDate(date, showTwelveHour);
@ -92,7 +92,7 @@ module.exports = {
monthName: months[date.getMonth()],
day: date.getDate(),
fullYear: date.getFullYear(),
time: showTwelveHour ? twelveHourTime(date) : this.formatTime(date),
time: this.formatTime(date, showTwelveHour),
});
},

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 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.
@ -16,10 +17,10 @@ limitations under the License.
'use strict';
var React = require('react');
var sanitizeHtml = require('sanitize-html');
var highlight = require('highlight.js');
var linkifyMatrix = require('./linkify-matrix');
const React = require('react');
const sanitizeHtml = require('sanitize-html');
const highlight = require('highlight.js');
const linkifyMatrix = require('./linkify-matrix');
import escape from 'lodash/escape';
import emojione from 'emojione';
import classNames from 'classnames';
@ -31,13 +32,33 @@ emojione.imagePathPNG = 'emojione/png/';
// Use SVGs for emojis
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 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
* because we want to include emoji shortnames in title text
*/
export function unicodeToImage(str) {
function unicodeToImage(str) {
let replaceWith, unicode, alt, short, fname;
const mappedUnicode = emojione.mapUnicodeToShort();
@ -45,8 +66,7 @@ export function unicodeToImage(str) {
if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
// if the unicodeChar doesnt exist just return the entire match
return unicodeChar;
}
else {
} else {
// get the unicode codepoint from the actual char
unicode = emojione.jsEscapeMap[unicodeChar];
@ -136,7 +156,7 @@ const sanitizeHtmlParams = {
allowedTags: [
'font', // custom to matrix for IRC-style font coloring
'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',
'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
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
// URL schemes we permit
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'],
allowProtocolRelative: false,
@ -162,21 +182,19 @@ const sanitizeHtmlParams = {
if (attribs.href) {
attribs.target = '_blank'; // by default
var m;
let m;
// FIXME: horrible duplication with linkify-matrix
m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN);
if (m) {
attribs.href = m[1];
delete attribs.target;
}
else {
} else {
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
if (m) {
var entity = m[1];
const entity = m[1];
if (entity[0] === '@') {
attribs.href = '#/user/' + entity;
}
else if (entity[0] === '#' || entity[0] === '!') {
} else if (entity[0] === '#' || entity[0] === '!') {
attribs.href = '#/room/' + entity;
}
delete attribs.target;
@ -184,7 +202,7 @@ const sanitizeHtmlParams = {
}
}
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
return { tagName: tagName, attribs : attribs };
return { tagName: tagName, attribs: attribs };
},
'img': function(tagName, attribs) {
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
@ -203,7 +221,7 @@ const sanitizeHtmlParams = {
'code': function(tagName, attribs) {
if (typeof attribs.class !== 'undefined') {
// Filter out all classes other than ones starting with language- for syntax highlighting.
let classes = attribs.class.split(/\s+/).filter(function(cl) {
const classes = attribs.class.split(/\s+/).filter(function(cl) {
return cl.startsWith('language-');
});
attribs.class = classes.join(' ');
@ -266,11 +284,11 @@ class BaseHighlighter {
* TextHighlighter).
*/
applyHighlights(safeSnippet, safeHighlights) {
var lastOffset = 0;
var offset;
var nodes = [];
let lastOffset = 0;
let offset;
let nodes = [];
var safeHighlight = safeHighlights[0];
const safeHighlight = safeHighlights[0];
while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
// handle preamble
if (offset > lastOffset) {
@ -280,7 +298,7 @@ class BaseHighlighter {
// do highlight. use the original string rather than safeHighlight
// to preserve the original casing.
var endOffset = offset + safeHighlight.length;
const endOffset = offset + safeHighlight.length;
nodes.push(this._processSnippet(safeSnippet.substring(offset, endOffset), true));
lastOffset = endOffset;
@ -298,8 +316,7 @@ class BaseHighlighter {
if (safeHighlights[1]) {
// recurse into this range to check for the next set of highlight matches
return this.applyHighlights(safeSnippet, safeHighlights.slice(1));
}
else {
} else {
// no more highlights to be found, just return the unhighlighted string
return [this._processSnippet(safeSnippet, false)];
}
@ -320,7 +337,7 @@ class HtmlHighlighter extends BaseHighlighter {
return snippet;
}
var span = "<span class=\""+this.highlightClass+"\">"
let span = "<span class=\""+this.highlightClass+"\">"
+ snippet + "</span>";
if (this.highlightLink) {
@ -345,15 +362,15 @@ class TextHighlighter extends BaseHighlighter {
* returns a React node
*/
_processSnippet(snippet, highlight) {
var key = this._key++;
const key = this._key++;
var node =
<span key={key} className={highlight ? this.highlightClass : null }>
let node =
<span key={key} className={highlight ? this.highlightClass : null}>
{ snippet }
</span>;
if (highlight && this.highlightLink) {
node = <a key={key} href={this.highlightLink}>{node}</a>;
node = <a key={key} href={this.highlightLink}>{ node }</a>;
}
return node;
@ -368,22 +385,23 @@ class TextHighlighter extends BaseHighlighter {
* highlights: optional list of words to highlight, ordered by longest word first
*
* 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) {
opts = opts || {};
export function bodyToHtml(content, highlights, 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 body = isHtml ? content.formatted_body : escape(content.body);
let bodyHasEmoji = false;
var safeBody;
let safeBody;
// 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
// 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
try {
if (highlights && highlights.length > 0) {
var highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
var safeHighlights = highlights.map(function(highlight) {
const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
const safeHighlights = highlights.map(function(highlight) {
return sanitizeHtml(highlight, sanitizeHtmlParams);
});
// 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 = unicodeToImage(safeBody);
safeBody = addCodeCopyButton(safeBody);
}
finally {
bodyHasEmoji = containsEmoji(body);
if (bodyHasEmoji) safeBody = unicodeToImage(safeBody);
} finally {
delete sanitizeHtmlParams.textFilter;
}
let emojiBody = false;
if (!opts.disableBigEmoji && bodyHasEmoji) {
EMOJI_REGEX.lastIndex = 0;
let contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
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({
'mx_EventTile_body': true,
@ -412,23 +432,6 @@ export function bodyToHtml(content, highlights, opts) {
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) {
return {
__html: unicodeToImage(escape(text)),

View file

@ -42,13 +42,12 @@ module.exports = {
// no scaling needs to be applied
return fullHeight;
}
var widthMulti = thumbWidth / fullWidth;
var heightMulti = thumbHeight / fullHeight;
const widthMulti = thumbWidth / fullWidth;
const heightMulti = thumbHeight / fullHeight;
if (widthMulti < heightMulti) {
// width is the dominant dimension so scaling will be fixed on that
return Math.floor(widthMulti * fullHeight);
}
else {
} else {
// height is the dominant dimension so scaling will be fixed on that
return Math.floor(heightMulti * fullHeight);
}

View file

@ -59,8 +59,8 @@ export default class Login {
}
getFlows() {
var self = this;
var client = this._createTemporaryClient();
const self = this;
const client = this._createTemporaryClient();
return client.loginFlows().then(function(result) {
self._flows = result.flows;
self._currentFlowIndex = 0;
@ -77,12 +77,12 @@ export default class Login {
getCurrentFlowStep() {
// technically the flow can have multiple steps, but no one does this
// for login so we can ignore it.
var flowStep = this._flows[this._currentFlowIndex];
const flowStep = this._flows[this._currentFlowIndex];
return flowStep ? flowStep.type : null;
}
loginAsGuest() {
var client = this._createTemporaryClient();
const client = this._createTemporaryClient();
return client.registerGuest({
body: {
initial_device_display_name: this._defaultDeviceDisplayName,
@ -94,7 +94,7 @@ export default class Login {
accessToken: creds.access_token,
homeserverUrl: this._hsUrl,
identityServerUrl: this._isUrl,
guest: true
guest: true,
};
}, (error) => {
throw error;
@ -149,12 +149,12 @@ export default class Login {
identityServerUrl: self._isUrl,
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token
accessToken: data.access_token,
});
}, function(error) {
if (error.httpStatus === 403) {
if (self._fallbackHsUrl) {
var fbClient = Matrix.createClient({
const fbClient = Matrix.createClient({
baseUrl: self._fallbackHsUrl,
idBaseUrl: this._isUrl,
});
@ -165,7 +165,7 @@ export default class Login {
identityServerUrl: self._isUrl,
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token
accessToken: data.access_token,
});
}, function(fallback_error) {
// throw the original error

View file

@ -17,7 +17,7 @@ limitations under the License.
import commonmark from 'commonmark';
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
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.
*/
function is_multi_line(node) {
var par = node;
let par = node;
while (par.parent) {
par = par.parent;
}
@ -143,7 +143,7 @@ export default class Markdown {
if (isMultiLine) this.cr();
html_if_tag_allowed.call(this, node);
if (isMultiLine) this.cr();
}
};
return renderer.render(this.parsed);
}
@ -178,7 +178,7 @@ export default class Markdown {
renderer.html_block = function(node) {
this.lit(node.literal);
if (is_multi_line(node) && node.next) this.lit('\n\n');
}
};
return renderer.render(this.parsed);
}

View file

@ -95,7 +95,7 @@ class MatrixClientPeg {
opts.pendingEventOrdering = "detached";
try {
let promise = this.matrixClient.store.startup();
const promise = this.matrixClient.store.startup();
console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
await promise;
} catch(err) {
@ -136,7 +136,7 @@ class MatrixClientPeg {
}
_createClient(creds: MatrixClientCreds) {
var opts = {
const opts = {
baseUrl: creds.homeserverUrl,
idBaseUrl: creds.identityServerUrl,
accessToken: creds.accessToken,
@ -153,8 +153,8 @@ class MatrixClientPeg {
this.matrixClient.setGuest(Boolean(creds.guest));
var notifTimelineSet = new EventTimelineSet(null, {
timelineSupport: true
const notifTimelineSet = new EventTimelineSet(null, {
timelineSupport: true,
});
// XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync.
notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);

View file

@ -17,8 +17,8 @@ limitations under the License.
'use strict';
var React = require('react');
var ReactDOM = require('react-dom');
const React = require('react');
const ReactDOM = require('react-dom');
import Analytics from './Analytics';
import sdk from './index';
@ -137,15 +137,15 @@ class ModalManager {
* @param {String} className CSS class to apply to the modal wrapper
*/
createDialogAsync(loader, props, className) {
var self = this;
const self = this;
const modal = {};
// never call this from onFinished() otherwise it will loop
//
// 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);
var i = self._modals.indexOf(modal);
const i = self._modals.indexOf(modal);
if (i >= 0) {
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!
modal.elem = (
<AsyncWrapper key={modalCount} loader={loader} {...props}
onFinished={closeDialog}/>
onFinished={closeDialog} />
);
modal.onFinished = props ? props.onFinished : null;
modal.className = className;
@ -191,13 +191,13 @@ class ModalManager {
return;
}
var modal = this._modals[0];
var dialog = (
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '') }>
const modal = this._modals[0];
const dialog = (
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
<div className="mx_Dialog">
{modal.elem}
{ modal.elem }
</div>
<div className="mx_Dialog_background" onClick={ this.closeAll }></div>
<div className="mx_Dialog_background" onClick={this.closeAll}></div>
</div>
);

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
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.
@ -79,10 +80,11 @@ const Notifier = {
if (ev.getContent().body) msg = ev.getContent().body;
}
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
ev.sender, 40, 40, 'crop'
) : null;
if (!this.isBodyEnabled()) {
msg = '';
}
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop') : null;
const notif = plaf.displayNotification(title, msg, avatarUrl, room);
// if displayNotification returns non-null, the platform supports
@ -96,17 +98,16 @@ const Notifier = {
_playAudioNotification: function(ev, room) {
const e = document.getElementById("messageAudio");
if (e) {
e.load();
e.play();
}
},
start: function() {
this.boundOnRoomTimeline = this.onRoomTimeline.bind(this);
this.boundOnEvent = this.onEvent.bind(this);
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
this.boundOnRoomReceipt = this.onRoomReceipt.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('Event.decrypted', this.boundOnEventDecrypted);
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
@ -116,7 +117,7 @@ const Notifier = {
stop: function() {
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('Event.decrypted', this.boundOnEventDecrypted);
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
@ -195,6 +196,19 @@ const Notifier = {
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) {
if (!global.localStorage) return;
global.localStorage.setItem('audio_notifications_enabled',
@ -247,12 +261,9 @@ const Notifier = {
}
},
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
if (toStartOfTimeline) return;
if (!room) return;
onEvent: function(ev) {
if (!this.isSyncing) return; // don't alert for any messages initially
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',
// it hasn't yet been decrypted, so wait until it is.
@ -306,7 +317,7 @@ const Notifier = {
this._playAudioNotification(ev, room);
}
}
}
},
};
if (!global.mxNotifier) {

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var MatrixClientPeg = require("./MatrixClientPeg");
var dis = require("./dispatcher");
const MatrixClientPeg = require("./MatrixClientPeg");
const dis = require("./dispatcher");
// Time in ms after that a user is considered as unavailable/away
var UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
var PRESENCE_STATES = ["online", "offline", "unavailable"];
const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
const PRESENCE_STATES = ["online", "offline", "unavailable"];
class Presence {
@ -71,14 +71,14 @@ class Presence {
if (!this.running) {
return;
}
var old_state = this.state;
const old_state = this.state;
this.state = newState;
if (MatrixClientPeg.get().isGuest()) {
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() {
console.log("Presence: %s", newState);
}, function(err) {
@ -104,7 +104,7 @@ class Presence {
* @private
*/
_resetTimer() {
var self = this;
const self = this;
this.setState("online");
// Re-arm the timer
clearTimeout(this.timer);

View file

@ -44,9 +44,9 @@ export const contentStateToHTML = (contentState: ContentState) => {
return stateToHTML(contentState, {
inlineStyles: {
UNDERLINE: {
element: 'u'
}
}
element: 'u',
},
},
});
};
@ -59,7 +59,7 @@ function unicodeToEmojiUri(str) {
let replaceWith, unicode, alt;
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
let mappedUnicode = emojione.mapUnicodeToShort();
const mappedUnicode = emojione.mapUnicodeToShort();
}
str = str.replace(emojione.regUnicode, function(unicodeChar) {
@ -67,8 +67,14 @@ function unicodeToEmojiUri(str) {
// if the unicodeChar doesnt exist just return the entire match
return unicodeChar;
} 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
unicode = emojione.jsEscapeMap[unicodeChar];
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
let emojiDecorator = {
const emojiDecorator = {
strategy: (contentState, contentBlock, callback) => {
findWithRegex(EMOJI_REGEX, contentBlock, callback);
},
component: (props) => {
let uri = unicodeToEmojiUri(props.children[0].props.text);
let shortname = emojione.toShort(props.children[0].props.text);
let style = {
const uri = unicodeToEmojiUri(props.children[0].props.text);
const shortname = emojione.toShort(props.children[0].props.text);
const style = {
display: 'inline-block',
width: '1em',
maxHeight: '1em',
@ -106,7 +112,7 @@ let emojiDecorator = {
backgroundPosition: 'center center',
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 {
let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
const markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
(style) => ({
strategy: (contentState, contentBlock, callback) => {
return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);
},
component: (props) => (
<span className={"mx_MarkdownElement mx_Markdown_" + style}>
{props.children}
{ props.children }
</span>
)
),
}));
markdownDecorators.push({
@ -136,9 +142,9 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
},
component: (props) => (
<a href="#" className="mx_MarkdownElement mx_Markdown_LINK">
{props.children}
{ props.children }
</a>
)
),
});
// markdownDecorators.push(emojiDecorator);
// 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;
currentKey && currentKey !== endKey;
currentKey = contentState.getKeyAfter(currentKey)) {
let blockText = getText(currentKey);
const blockText = getText(currentKey);
text += blockText.substring(startOffset, blockText.length);
// from now on, we'll take whole blocks
@ -182,7 +188,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
export function selectionStateToTextOffsets(selectionState: SelectionState,
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
let offset = 0, start = 0, end = 0;
for (let block of contentBlocks) {
for (const block of contentBlocks) {
if (selectionState.getStartKey() === block.getKey()) {
start = offset + selectionState.getStartOffset();
}
@ -259,7 +265,7 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor
.set('focusOffset', end);
const emojiText = plainText.substring(start, end);
newContentState = newContentState.createEntity(
'emoji', 'IMMUTABLE', { emojiUnicode: emojiText }
'emoji', 'IMMUTABLE', { emojiUnicode: emojiText },
);
const entityKey = newContentState.getLastCreatedEntityKey();
newContentState = Modifier.replaceText(

View file

@ -28,7 +28,7 @@ export function inviteToRoom(roomId, addr) {
if (addrType == 'email') {
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
} else if (addrType == 'mx') {
} else if (addrType == 'mx-user-id') {
return MatrixClientPeg.get().invite(roomId, addr);
} else {
throw new Error('Unsupported address');
@ -50,8 +50,8 @@ export function inviteMultipleToRoom(roomId, addrs) {
}
export function showStartChatInviteDialog() {
const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog");
Modal.createTrackedDialog('Start a chat', '', UserPickerDialog, {
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
title: _t('Start a chat'),
description: _t("Who would you like to communicate with?"),
placeholder: _t("Email, name or matrix ID"),
@ -61,8 +61,8 @@ export function showStartChatInviteDialog() {
}
export function showRoomInviteDialog(roomId) {
const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog");
Modal.createTrackedDialog('Chat Invite', '', UserPickerDialog, {
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Chat Invite', '', AddressPickerDialog, {
title: _t('Invite new room members'),
description: _t('Who would you like to add to this room?'),
button: _t('Send Invites'),
@ -127,7 +127,7 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) {
}
function _isDmChat(addrTexts) {
if (addrTexts.length === 1 && getAddressType(addrTexts[0])) {
if (addrTexts.length === 1 && getAddressType(addrTexts[0]) === 'mx') {
return true;
} else {
return false;

View file

@ -62,8 +62,7 @@ export function isConfCallRoom(room, me, conferenceHandler) {
export function looksLikeDirectMessageRoom(room, me) {
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
const tagNames = Object.keys(room.tags);
// Used for 1:1 direct chats

View file

@ -15,10 +15,10 @@ limitations under the License.
*/
import Promise from 'bluebird';
var request = require('browser-request');
const request = require('browser-request');
var SdkConfig = require('./SdkConfig');
var MatrixClientPeg = require('./MatrixClientPeg');
const SdkConfig = require('./SdkConfig');
const MatrixClientPeg = require('./MatrixClientPeg');
class ScalarAuthClient {
@ -38,7 +38,7 @@ class ScalarAuthClient {
// Returns a scalar_token string
getScalarToken() {
var tok = window.localStorage.getItem("mx_scalar_token");
const tok = window.localStorage.getItem("mx_scalar_token");
if (tok) return Promise.resolve(tok);
// No saved token, so do the dance to get one. First, we
@ -53,9 +53,9 @@ class ScalarAuthClient {
}
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({
method: 'POST',
uri: scalar_rest_url+'/register',
@ -77,7 +77,7 @@ class ScalarAuthClient {
}
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 += "&room_id=" + encodeURIComponent(roomId);
if (id) {

View file

@ -356,12 +356,12 @@ function getWidgets(event, roomId) {
}
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
// Only return widgets which have required fields
let widgetStateEvents = [];
const widgetStateEvents = [];
stateEvents.forEach((ev) => {
if (ev.getContent().type && ev.getContent().url) {
widgetStateEvents.push(ev.event); // return the raw event
}
})
});
sendResponse(event, widgetStateEvents);
}
@ -376,7 +376,7 @@ function setPlumbingState(event, roomId, status) {
sendError(event, _t('You need to be logged in.'));
return;
}
client.sendStateEvent(roomId, "m.room.plumbing", { status : status }).done(() => {
client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).done(() => {
sendResponse(event, {
success: true,
});
@ -415,11 +415,11 @@ function setBotPower(event, roomId, userId, level) {
}
client.getStateEvent(roomId, "m.room.power_levels", "").then((powerLevels) => {
let powerEvent = new MatrixEvent(
const powerEvent = new MatrixEvent(
{
type: "m.room.power_levels",
content: powerLevels,
}
},
);
client.setPowerLevel(roomId, userId, level, powerEvent).done(() => {
@ -485,8 +485,7 @@ function canSendEvent(event, roomId) {
let canSend = false;
if (isState) {
canSend = room.currentState.maySendStateEvent(evType, me);
}
else {
} else {
canSend = room.currentState.maySendEvent(evType, me);
}
@ -517,8 +516,8 @@ function returnStateEvent(event, roomId, eventType, stateKey) {
sendResponse(event, stateEvent.getContent());
}
var currentRoomId = null;
var currentRoomAlias = null;
let currentRoomId = null;
let currentRoomAlias = null;
// Listen for when a room is viewed
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
// 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) {
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
const e = new Error(
"ScalarMessaging: mismatched startListening / stopListening detected." +
" Negative count"
" Negative count",
);
console.error(e);
}

View file

@ -84,6 +84,9 @@ class Skinner {
// 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
// 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) {
global.mxSkinner = new Skinner();
}

View file

@ -240,6 +240,59 @@ const commands = {
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
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
if (args) {
@ -292,6 +345,13 @@ const commands = {
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: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, 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
limitations under the License.
*/
import MatrixClientPeg from "./MatrixClientPeg";
import CallHandler from "./CallHandler";
import MatrixClientPeg from './MatrixClientPeg';
import CallHandler from './CallHandler';
import { _t } from './languageHandler';
import * as Roles from './Roles';
function textForMemberEvent(ev) {
// XXX: SYJS-16 "sender is sometimes null for join messages"
var senderName = ev.sender ? ev.sender.name : ev.getSender();
var targetName = ev.target ? ev.target.name : ev.getStateKey();
var ConferenceHandler = CallHandler.getConferenceHandler();
var reason = ev.getContent().reason ? (
_t('Reason') + ': ' + ev.getContent().reason
) : "";
switch (ev.getContent().membership) {
case 'invite':
var threePidContent = ev.getContent().third_party_invite;
const senderName = ev.sender ? ev.sender.name : ev.getSender();
const targetName = ev.target ? ev.target.name : ev.getStateKey();
const prevContent = ev.getPrevContent();
const content = ev.getContent();
const ConferenceHandler = CallHandler.getConferenceHandler();
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
switch (content.membership) {
case 'invite': {
const threePidContent = content.third_party_invite;
if (threePidContent) {
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 {
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())) {
return _t('%(senderName)s requested a VoIP conference.', {senderName: senderName});
return _t('%(senderName)s requested a VoIP conference.', {senderName});
} else {
return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
}
else {
return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName});
}
}
case 'ban':
return _t(
'%(senderName)s banned %(targetName)s.',
{senderName: senderName, targetName: targetName}
) + ' ' + reason;
return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason;
case 'join':
if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') {
if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) {
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname, displayName: ev.getContent().displayname});
} else if (!ev.getPrevContent().displayname && ev.getContent().displayname) {
return _t('%(senderName)s set their display name to %(displayName)s.', {senderName: ev.getSender(), displayName: ev.getContent().displayname});
} else if (ev.getPrevContent().displayname && !ev.getContent().displayname) {
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname});
} else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) {
return _t('%(senderName)s removed their profile picture.', {senderName: senderName});
} else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) {
return _t('%(senderName)s changed their profile picture.', {senderName: senderName});
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
return _t('%(senderName)s set a profile picture.', {senderName: senderName});
if (prevContent && prevContent.membership === 'join') {
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {
senderName,
oldDisplayName: prevContent.displayname,
displayName: content.displayname,
});
} else if (!prevContent.displayname && content.displayname) {
return _t('%(senderName)s set their display name to %(displayName)s.', {
senderName,
displayName: content.displayname,
});
} 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 {
// suppress null rejoins
return '';
@ -71,73 +82,69 @@ function textForMemberEvent(ev) {
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
return _t('VoIP conference started.');
}
else {
return _t('%(targetName)s joined the room.', {targetName: targetName});
} else {
return _t('%(targetName)s joined the room.', {targetName});
}
}
case 'leave':
if (ev.getSender() === ev.getStateKey()) {
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
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") {
return _t('%(targetName)s rejected the invitation.', {targetName: targetName});
}
else {
return _t('%(targetName)s left the room.', {targetName: targetName});
}
}
else if (ev.getPrevContent().membership === "ban") {
return _t('%(senderName)s unbanned %(targetName)s.', {senderName: senderName, targetName: targetName});
}
else if (ev.getPrevContent().membership === "join") {
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});
} else if (prevContent.membership === "ban") {
return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
} else if (prevContent.membership === "join") {
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
} else if (prevContent.membership === "invite") {
return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
senderName,
targetName,
}) + ' ' + reason;
} else {
return _t('%(targetName)s left the room.', {targetName});
}
}
}
function textForTopicEvent(ev) {
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {senderDisplayName: senderDisplayName, topic: ev.getContent().topic});
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
senderDisplayName,
topic: ev.getContent().topic,
});
}
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) {
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) {
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
var message = senderDisplayName + ': ' + ev.getContent().body;
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
let message = senderDisplayName + ': ' + ev.getContent().body;
if (ev.getContent().msgtype === "m.emote") {
message = "* " + senderDisplayName + " " + message;
} 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;
}
function textForCallAnswerEvent(event) {
var senderName = event.sender ? event.sender.name : _t('Someone');
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return _t('%(senderName)s answered the call.', {senderName: senderName}) + ' ' + supported;
const senderName = event.sender ? event.sender.name : _t('Someone');
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
}
function textForCallHangupEvent(event) {
@ -159,48 +166,52 @@ function textForCallHangupEvent(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?
var type = "voice";
let callType = "voice";
if (event.getContent().offer && event.getContent().offer.sdp &&
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
type = "video";
callType = "video";
}
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return _t('%(senderName)s placed a %(callType)s call.', {senderName: senderName, callType: type}) + ' ' + supported;
const supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return _t('%(senderName)s placed a %(callType)s call.', {senderName, callType}) + ' ' + supported;
}
function textForThreePidInviteEvent(event) {
var 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});
const senderName = event.sender ? event.sender.name : event.getSender();
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
senderName,
targetDisplayName: event.getContent().display_name,
});
}
function textForHistoryVisibilityEvent(event) {
var senderName = event.sender ? event.sender.name : event.getSender();
var vis = event.getContent().history_visibility;
// XXX: This i18n just isn't going to work for languages with different sentence structure.
var text = _t('%(senderName)s made future room history visible to', {senderName: senderName}) + ' ';
if (vis === "invited") {
text += _t('all room members, from the point they are invited') + '.';
const senderName = event.sender ? event.sender.name : event.getSender();
switch (event.getContent().history_visibility) {
case 'invited':
return _t('%(senderName)s made future room history visible to all room members, '
+ 'from the point they are invited.', {senderName});
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) {
var 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});
const senderName = event.sender ? event.sender.name : event.getSender();
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
@ -211,18 +222,18 @@ function textForPowerEvent(event) {
}
const userDefault = event.getContent().users_default || 0;
// Construct set of userIds
let users = [];
const users = [];
Object.keys(event.getContent().users).forEach(
(userId) => {
if (users.indexOf(userId) === -1) users.push(userId);
}
},
);
Object.keys(event.getPrevContent().users).forEach(
(userId) => {
if (users.indexOf(userId) === -1) users.push(userId);
}
},
);
let diff = [];
const diff = [];
// XXX: This is also surely broken for i18n
users.forEach((userId) => {
// Previous power level
@ -232,10 +243,10 @@ function textForPowerEvent(event) {
if (to !== from) {
diff.push(
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
userId: userId,
userId,
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
toPowerLevel: Roles.textualPowerLevel(to, userDefault)
})
toPowerLevel: Roles.textualPowerLevel(to, userDefault),
}),
);
}
});
@ -243,16 +254,22 @@ function textForPowerEvent(event) {
return '';
}
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
senderName: senderName,
powerLevelDiffText: diff.join(", ")
senderName,
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) {
const senderName = event.sender ? event.sender.name : event.getSender();
const previousContent = event.getPrevContent() || {};
const senderName = event.getSender();
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
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
if (widgetName && widgetName.length > 0) {
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
// equivalent to that condition.
if (url) {
if (prevUrl) {
return _t('%(widgetName)s widget modified by %(senderName)s', {
widgetName, senderName,
});
} else {
return _t('%(widgetName)s widget added by %(senderName)s', {
widgetName, senderName,
});
}
} else {
return _t('%(widgetName)s widget removed by %(senderName)s', {
widgetName, senderName,
@ -271,26 +294,30 @@ function textForWidgetEvent(event) {
}
}
var handlers = {
const handlers = {
'm.room.message': textForMessageEvent,
'm.room.name': textForRoomNameEvent,
'm.room.topic': textForTopicEvent,
'm.room.member': textForMemberEvent,
'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.history_visibility': textForHistoryVisibilityEvent,
'm.room.encryption': textForEncryptionEvent,
'm.room.power_levels': textForPowerEvent,
'm.room.pinned_events': textForPinnedEvent,
'im.vector.modular.widgets': textForWidgetEvent,
};
module.exports = {
textForEvent: function(ev) {
var hdlr = handlers[ev.getType()];
if (!hdlr) return "";
return hdlr(ev);
}
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
if (handler) return handler(ev);
return '';
},
};

View file

@ -18,10 +18,10 @@ limitations under the License.
// module.exports otherwise this will break when included by both
// react-sdk and apps layered on top.
var DEBUG = 0;
const DEBUG = 0;
// The colour keys to be replaced as referred to in CSS
var keyRgb = [
const keyRgb = [
"rgb(118, 207, 166)", // Vector Green
"rgb(234, 245, 240)", // Vector Light Green
"rgb(211, 239, 225)", // BottomLeftMenu overlay (20% Vector Green)
@ -35,7 +35,7 @@ var keyRgb = [
// x = (255 - 234) / (255 - 118) = 0.16
// The colour keys to be replaced as referred to in SVGs
var keyHex = [
const keyHex = [
"#76CFA6", // Vector Green
"#EAF5F0", // 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
// defaults to our keys.
var colors = [
const colors = [
keyHex[0],
keyHex[1],
keyHex[2],
keyHex[3],
];
var cssFixups = [
const cssFixups = [
// {
// style: a style object that should be fixed up taken from a stylesheet
// attr: name of the attribute to be clobbered, e.g. 'color'
@ -60,7 +60,7 @@ var cssFixups = [
];
// CSS attributes to be fixed up
var cssAttrs = [
const cssAttrs = [
"color",
"backgroundColor",
"borderColor",
@ -69,17 +69,17 @@ var cssAttrs = [
"borderLeftColor",
];
var svgAttrs = [
const svgAttrs = [
"fill",
"stroke",
];
var cached = false;
let cached = false;
function calcCssFixups() {
if (DEBUG) console.log("calcSvgFixups start");
for (var i = 0; i < document.styleSheets.length; i++) {
var ss = document.styleSheets[i];
for (let i = 0; i < document.styleSheets.length; i++) {
const ss = document.styleSheets[i];
if (!ss) continue; // well done safari >:(
// Chromium apparently sometimes returns null here; unsure why.
// see $14534907369972FRXBx:matrix.org in HQ
@ -104,12 +104,12 @@ function calcCssFixups() {
if (ss.href && !ss.href.match(/\/bundle.*\.css$/)) continue;
if (!ss.cssRules) continue;
for (var j = 0; j < ss.cssRules.length; j++) {
var rule = ss.cssRules[j];
for (let j = 0; j < ss.cssRules.length; j++) {
const rule = ss.cssRules[j];
if (!rule.style) continue;
for (var k = 0; k < cssAttrs.length; k++) {
var attr = cssAttrs[k];
for (var l = 0; l < keyRgb.length; l++) {
for (let k = 0; k < cssAttrs.length; k++) {
const attr = cssAttrs[k];
for (let l = 0; l < keyRgb.length; l++) {
if (rule.style[attr] === keyRgb[l]) {
cssFixups.push({
style: rule.style,
@ -126,8 +126,8 @@ function calcCssFixups() {
function applyCssFixups() {
if (DEBUG) console.log("applyCssFixups start");
for (var i = 0; i < cssFixups.length; i++) {
var cssFixup = cssFixups[i];
for (let i = 0; i < cssFixups.length; i++) {
const cssFixup = cssFixups[i];
cssFixup.style[cssFixup.attr] = colors[cssFixup.index];
}
if (DEBUG) console.log("applyCssFixups end");
@ -140,15 +140,15 @@ function hexToRgb(color) {
color[1] + color[1] +
color[2] + color[2];
}
var val = parseInt(color, 16);
var r = (val >> 16) & 255;
var g = (val >> 8) & 255;
var b = val & 255;
const val = parseInt(color, 16);
const r = (val >> 16) & 255;
const g = (val >> 8) & 255;
const b = val & 255;
return [r, g, b];
}
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);
}
@ -167,12 +167,11 @@ module.exports = {
*
* @param {Function} tintable Function to call when the tint changes.
*/
registerTintable : function(tintable) {
registerTintable: function(tintable) {
tintables.push(tintable);
},
tint: function(primaryColor, secondaryColor, tertiaryColor) {
if (!cached) {
calcCssFixups();
cached = true;
@ -185,7 +184,7 @@ module.exports = {
if (!secondaryColor) {
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[1] = x * rgb[1] + (1 - x) * 255;
rgb[2] = x * rgb[2] + (1 - x) * 255;
@ -194,8 +193,8 @@ module.exports = {
if (!tertiaryColor) {
const x = 0.19;
var rgb1 = hexToRgb(primaryColor);
var rgb2 = hexToRgb(secondaryColor);
const rgb1 = hexToRgb(primaryColor);
const rgb2 = hexToRgb(secondaryColor);
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
@ -204,8 +203,7 @@ module.exports = {
if (colors[0] === primaryColor &&
colors[1] === secondaryColor &&
colors[2] === tertiaryColor)
{
colors[2] === tertiaryColor) {
return;
}
@ -248,14 +246,13 @@ module.exports = {
// key colour; cache the element and apply.
if (DEBUG) console.log("calcSvgFixups start for " + svgs);
var fixups = [];
for (var i = 0; i < svgs.length; i++) {
const fixups = [];
for (let i = 0; i < svgs.length; i++) {
var svgDoc;
try {
svgDoc = svgs[i].contentDocument;
}
catch(e) {
var msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
} catch(e) {
let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
if (e.message) {
msg += e.message;
}
@ -265,12 +262,12 @@ module.exports = {
console.error(e);
}
if (!svgDoc) continue;
var tags = svgDoc.getElementsByTagName("*");
for (var j = 0; j < tags.length; j++) {
var tag = tags[j];
for (var k = 0; k < svgAttrs.length; k++) {
var attr = svgAttrs[k];
for (var l = 0; l < keyHex.length; l++) {
const tags = svgDoc.getElementsByTagName("*");
for (let j = 0; j < tags.length; j++) {
const tag = tags[j];
for (let k = 0; k < svgAttrs.length; k++) {
const attr = svgAttrs[k];
for (let l = 0; l < keyHex.length; l++) {
if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) {
fixups.push({
node: tag,
@ -289,10 +286,10 @@ module.exports = {
applySvgFixups: function(fixups) {
if (DEBUG) console.log("applySvgFixups start for " + fixups);
for (var i = 0; i < fixups.length; i++) {
var svgFixup = fixups[i];
for (let i = 0; i < fixups.length; i++) {
const svgFixup = fixups[i];
svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]);
}
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.
*/
var MatrixClientPeg = require('./MatrixClientPeg');
const MatrixClientPeg = require('./MatrixClientPeg');
import UserSettingsStore from './UserSettingsStore';
import shouldHideEvent from './shouldHideEvent';
var sdk = require('./index');
const sdk = require('./index');
module.exports = {
/**
@ -34,17 +34,17 @@ module.exports = {
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
return false;
}
var EventTile = sdk.getComponent('rooms.EventTile');
const EventTile = sdk.getComponent('rooms.EventTile');
return EventTile.haveTileForEvent(ev);
},
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.
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
// despite the name of the method :((
var readUpToId = room.getEventReadUpTo(myUserId);
const readUpToId = room.getEventReadUpTo(myUserId);
// as we don't send RRs for our own messages, make sure we special case that
// if *we* sent the last message into the room, we consider it not unread!
@ -54,8 +54,7 @@ module.exports = {
// https://github.com/vector-im/riot-web/issues/3363
if (room.timeline.length &&
room.timeline[room.timeline.length - 1].sender &&
room.timeline[room.timeline.length - 1].sender.userId === myUserId)
{
room.timeline[room.timeline.length - 1].sender.userId === myUserId) {
return false;
}
@ -67,8 +66,8 @@ module.exports = {
const syncedSettings = UserSettingsStore.getSyncedSettings();
// Loop through messages, starting with the most recent...
for (var i = room.timeline.length - 1; i >= 0; --i) {
var ev = room.timeline[i];
for (let i = room.timeline.length - 1; i >= 0; --i) {
const ev = room.timeline[i];
if (ev.getId() == readUpToId) {
// 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
// false negatives here.
return true;
}
},
};

View file

@ -16,11 +16,12 @@ limitations under the License.
const emailRegex = /^\S+@\S+\.\S+$/;
const mxidRegex = /^@\S+:\S+$/;
const mxUserIdRegex = /^@\S+:\S+$/;
const mxRoomIdRegex = /^!\S+:\S+$/;
import PropTypes from 'prop-types';
export const addressTypes = [
'mx', 'email',
'mx-user-id', 'mx-room-id', 'email',
];
// PropType definition for an object describing
@ -41,13 +42,16 @@ export const UserAddressType = PropTypes.shape({
export function getAddressType(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
if (isEmailAddress) {
return 'email';
} else if (isMatrixId) {
return 'mx';
} else if (isUserId) {
return 'mx-user-id';
} else if (isRoomId) {
return 'mx-room-id';
} else {
return null;
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
@ -17,27 +18,55 @@ limitations under the License.
import Promise from 'bluebird';
import MatrixClientPeg from './MatrixClientPeg';
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.
*/
export default {
LABS_FEATURES: [
const FEATURES = [
{
name: "-",
id: 'matrix_apps',
default: true,
// XXX: Always use default, ignore localStorage and remove from labs
override: true,
id: 'feature_groups',
name: _td("Communities"),
},
],
{
id: 'feature_pinning',
name: _td("Message Pinning"),
},
];
// horrible but it works. The locality makes this somewhat more palatable.
doTranslations: function() {
this.LABS_FEATURES[0].name = _t("Matrix Apps");
export default {
getLabsFeatures() {
const featuresConfig = SdkConfig.get()['features'] || {};
// The old flag: honourned for backwards compat
const enableLabs = SdkConfig.get()['enableLabs'];
let labsFeatures;
if (enableLabs) {
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() {
@ -73,6 +102,17 @@ export default {
Notifier.setEnabled(enable);
},
getEnableNotificationBody: function() {
return Notifier.isBodyEnabled();
},
setEnableNotificationBody: function(enable) {
if (!Notifier.supportsDesktopNotifications()) {
return;
}
Notifier.setBodyEnabled(enable);
},
getEnableAudioNotifications: function() {
return Notifier.isAudioEnabled();
},
@ -174,33 +214,33 @@ export default {
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 {
// Disable labs for guests.
if (MatrixClientPeg.get().isGuest()) return false;
const featuresConfig = SdkConfig.get()['features'];
const feature = this.getFeatureById(featureId);
if (!feature) {
console.warn(`Unknown feature "${featureId}"`);
// The old flag: honourned for backwards compat
const enableLabs = SdkConfig.get()['enableLabs'];
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 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) {

View file

@ -1,6 +1,6 @@
var React = require('react');
var ReactDom = require('react-dom');
var Velocity = require('velocity-vector');
const React = require('react');
const ReactDom = require('react-dom');
const Velocity = require('velocity-vector');
/**
* 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
*/
_updateChildren: function(newChildren) {
var self = this;
var oldChildren = this.children || {};
const self = this;
const oldChildren = this.children || {};
this.children = {};
React.Children.toArray(newChildren).forEach(function(c) {
if (oldChildren[c.key]) {
var old = oldChildren[c.key];
var oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
const old = oldChildren[c.key];
const oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
if (oldNode && oldNode.style.left != c.props.style.left) {
Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() {
@ -71,18 +71,18 @@ module.exports = React.createClass({
} else {
// new element. If we have a startStyle, use that as the style and go through
// the enter animations
var newProps = {};
var restingStyle = c.props.style;
const newProps = {};
const restingStyle = c.props.style;
var startStyles = self.props.startStyles;
const startStyles = self.props.startStyles;
if (startStyles.length > 0) {
var startStyle = startStyles[0];
const startStyle = startStyles[0];
newProps.style = startStyle;
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
}
newProps.ref = (n => self._collectNode(
c.key, n, restingStyle
newProps.ref = ((n) => self._collectNode(
c.key, n, restingStyle,
));
self.children[c.key] = React.cloneElement(c, newProps);
@ -103,8 +103,8 @@ module.exports = React.createClass({
this.nodes[k] === undefined &&
this.props.startStyles.length > 0
) {
var startStyles = this.props.startStyles;
var transitionOpts = this.props.enterTransitionOpts;
const startStyles = this.props.startStyles;
const transitionOpts = this.props.enterTransitionOpts;
const domNode = ReactDom.findDOMNode(node);
// start from startStyle 1: 0 is the one we gave it
// to start with, so now we animate 1 etc.
@ -154,7 +154,7 @@ module.exports = React.createClass({
render: function() {
return (
<span>
{Object.values(this.children)}
{ Object.values(this.children) }
</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
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
function bounce( p ) {
var pow2,
let pow2,
bounce = 4;
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.
*/
var MatrixClientPeg = require("./MatrixClientPeg");
const MatrixClientPeg = require("./MatrixClientPeg");
import { _t } from './languageHandler';
module.exports = {
usersTypingApartFromMeAndIgnored: function(room) {
return this.usersTyping(
room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers()),
);
},
usersTypingApartFromMe: function(room) {
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.
*/
usersTyping: function(room, exclude) {
var whoIsTyping = [];
const whoIsTyping = [];
if (exclude === undefined) {
exclude = [];
}
var memberKeys = Object.keys(room.currentState.members);
for (var i = 0; i < memberKeys.length; ++i) {
var userId = memberKeys[i];
const memberKeys = Object.keys(room.currentState.members);
for (let i = 0; i < memberKeys.length; ++i) {
const userId = memberKeys[i];
if (room.currentState.members[userId].typing) {
if (exclude.indexOf(userId) == -1) {
@ -70,5 +76,5 @@ module.exports = {
const lastPerson = names.pop();
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.
*/
var React = require("react");
const React = require("react");
import { _t } from '../../../languageHandler';
var sdk = require('../../../index');
var MatrixClientPeg = require("../../../MatrixClientPeg");
const sdk = require('../../../index');
const MatrixClientPeg = require("../../../MatrixClientPeg");
module.exports = React.createClass({
displayName: 'EncryptedEventDialog',
@ -33,7 +33,7 @@ module.exports = React.createClass({
componentWillMount: function() {
this._unmounted = false;
var client = MatrixClientPeg.get();
const client = MatrixClientPeg.get();
// first try to load the device from our store.
//
@ -60,7 +60,7 @@ module.exports = React.createClass({
componentWillUnmount: function() {
this._unmounted = true;
var client = MatrixClientPeg.get();
const client = MatrixClientPeg.get();
if (client) {
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
}
@ -89,12 +89,12 @@ module.exports = React.createClass({
},
_renderDeviceInfo: function() {
var device = this.state.device;
const device = this.state.device;
if (!device) {
return (<i>{ _t('unknown device') }</i>);
}
var verificationStatus = (<b>{ _t('NOT verified') }</b>);
let verificationStatus = (<b>{ _t('NOT verified') }</b>);
if (device.isBlocked()) {
verificationStatus = (<b>{ _t('Blacklisted') }</b>);
} else if (device.isVerified()) {
@ -118,7 +118,7 @@ module.exports = React.createClass({
</tr>
<tr>
<td>{ _t('Ed25519 fingerprint') }</td>
<td><code>{device.getFingerprint()}</code></td>
<td><code>{ device.getFingerprint() }</code></td>
</tr>
</tbody>
</table>
@ -126,7 +126,7 @@ module.exports = React.createClass({
},
_renderEventInfo: function() {
var event = this.props.event;
const event = this.props.event;
return (
<table>
@ -165,36 +165,36 @@ module.exports = React.createClass({
},
render: function() {
var DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
var buttons = null;
let buttons = null;
if (this.state.device) {
buttons = (
<DeviceVerifyButtons device={ this.state.device }
userId={ this.props.event.getSender() }
<DeviceVerifyButtons device={this.state.device}
userId={this.props.event.getSender()}
/>
);
}
return (
<div className="mx_EncryptedEventDialog" onKeyDown={ this.onKeyDown }>
<div className="mx_EncryptedEventDialog" onKeyDown={this.onKeyDown}>
<div className="mx_Dialog_title">
{ _t('End-to-end encryption information') }
</div>
<div className="mx_Dialog_content">
<h4>{ _t('Event information') }</h4>
{this._renderEventInfo()}
{ this._renderEventInfo() }
<h4>{ _t('Sender device information') }</h4>
{this._renderDeviceInfo()}
{ this._renderDeviceInfo() }
</div>
<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') }
</button>
{buttons}
{ buttons }
</div>
</div>
);
}
},
});

View file

@ -136,13 +136,13 @@ export default React.createClass({
) }
</p>
<div className='error'>
{this.state.errStr}
{ this.state.errStr }
</div>
<div className='mx_E2eKeysDialog_inputTable'>
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase1'>
{_t("Enter passphrase")}
{ _t("Enter passphrase") }
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
@ -155,7 +155,7 @@ export default React.createClass({
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase2'>
{_t("Confirm passphrase")}
{ _t("Confirm passphrase") }
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
@ -172,7 +172,7 @@ export default React.createClass({
disabled={disableForm}
/>
<button onClick={this._onCancelClick} disabled={disableForm}>
{_t("Cancel")}
{ _t("Cancel") }
</button>
</div>
</form>

View file

@ -134,13 +134,13 @@ export default React.createClass({
) }
</p>
<div className='error'>
{this.state.errStr}
{ this.state.errStr }
</div>
<div className='mx_E2eKeysDialog_inputTable'>
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='importFile'>
{_t("File to import")}
{ _t("File to import") }
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
@ -153,14 +153,14 @@ export default React.createClass({
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase'>
{_t("Enter passphrase")}
{ _t("Enter passphrase") }
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='passphrase' id='passphrase'
size='64' type='password'
onChange={this._onFormChange}
disabled={disableForm}/>
disabled={disableForm} />
</div>
</div>
</div>
@ -170,7 +170,7 @@ export default React.createClass({
disabled={!this.state.enableSubmit || disableForm}
/>
<button onClick={this._onCancelClick} disabled={disableForm}>
{_t("Cancel")}
{ _t("Cancel") }
</button>
</div>
</form>

View file

@ -45,7 +45,7 @@ const PROVIDERS = [
EmojiProvider,
CommandProvider,
DuckDuckGoProvider,
].map(completer => completer.getInstance());
].map((completer) => completer.getInstance());
// Providers will get rejected if they take longer than this.
const PROVIDER_COMPLETION_TIMEOUT = 3000;

View file

@ -16,7 +16,7 @@ limitations under the License.
*/
import React from 'react';
import { _t } from '../languageHandler';
import { _t, _td } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import FuzzyMatcher from './FuzzyMatcher';
import {TextualCompletion} from './Components';
@ -27,72 +27,82 @@ const COMMANDS = [
{
command: '/me',
args: '<message>',
description: 'Displays action',
description: _td('Displays action'),
},
{
command: '/ban',
args: '<user-id> [reason]',
description: 'Bans user with given id',
description: _td('Bans user with given id'),
},
{
command: '/unban',
args: '<user-id>',
description: 'Unbans user with given id',
description: _td('Unbans user with given id'),
},
{
command: '/op',
args: '<user-id> [<power-level>]',
description: 'Define the power level of a user',
description: _td('Define the power level of a user'),
},
{
command: '/deop',
args: '<user-id>',
description: 'Deops user with given id',
description: _td('Deops user with given id'),
},
{
command: '/invite',
args: '<user-id>',
description: 'Invites user with given id to current room',
description: _td('Invites user with given id to current room'),
},
{
command: '/join',
args: '<room-alias>',
description: 'Joins room with given alias',
description: _td('Joins room with given alias'),
},
{
command: '/part',
args: '[<room-alias>]',
description: 'Leave room',
description: _td('Leave room'),
},
{
command: '/topic',
args: '<topic>',
description: 'Sets the room topic',
description: _td('Sets the room topic'),
},
{
command: '/kick',
args: '<user-id> [reason]',
description: 'Kicks user with given id',
description: _td('Kicks user with given id'),
},
{
command: '/nick',
args: '<display-name>',
description: 'Changes your display nickname',
description: _td('Changes your display nickname'),
},
{
command: '/ddg',
args: '<query>',
description: 'Searches DuckDuckGo for results',
description: _td('Searches DuckDuckGo for results'),
},
{
command: '/tint',
args: '<color1> [<color2>]',
description: 'Changes colour scheme of current room',
description: _td('Changes colour scheme of current room'),
},
{
command: '/verify',
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
];
@ -119,7 +129,7 @@ export default class CommandProvider extends AutocompleteProvider {
component: (<TextualCompletion
title={result.command}
subtitle={result.args}
description={ _t(result.description) }
description={_t(result.description)}
/>),
range,
};
@ -140,7 +150,7 @@ export default class CommandProvider extends AutocompleteProvider {
renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_block">
{completions}
{ completions }
</div>;
}
}

View file

@ -30,13 +30,13 @@ export class TextualCompletion extends React.Component {
subtitle,
description,
className,
...restProps,
...restProps
} = this.props;
return (
<div className={classNames('mx_Autocomplete_Completion_block', className)} {...restProps}>
<span className="mx_Autocomplete_Completion_title">{title}</span>
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
<span className="mx_Autocomplete_Completion_description">{description}</span>
<span className="mx_Autocomplete_Completion_title">{ title }</span>
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
<span className="mx_Autocomplete_Completion_description">{ description }</span>
</div>
);
}
@ -56,14 +56,14 @@ export class PillCompletion extends React.Component {
description,
initialComponent,
className,
...restProps,
...restProps
} = this.props;
return (
<div className={classNames('mx_Autocomplete_Completion_pill', className)} {...restProps}>
{initialComponent}
<span className="mx_Autocomplete_Completion_title">{title}</span>
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
<span className="mx_Autocomplete_Completion_description">{description}</span>
{ initialComponent }
<span className="mx_Autocomplete_Completion_title">{ title }</span>
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
<span className="mx_Autocomplete_Completion_description">{ description }</span>
</div>
);
}

View file

@ -38,7 +38,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
}
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) {
return [];
}
@ -47,7 +47,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
method: 'GET',
});
const json = await response.json();
let results = json.Results.map(result => {
const results = json.Results.map((result) => {
return {
completion: result.Text,
component: (
@ -105,7 +105,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_block">
{completions}
{ completions }
</div>;
}
}

View file

@ -25,6 +25,7 @@ import {PillCompletion} from './Components';
import type {SelectionRange, Completion} from './Autocompleter';
import _uniq from 'lodash/uniq';
import _sortBy from 'lodash/sortBy';
import UserSettingsStore from '../UserSettingsStore';
import EmojiData from '../stripped-emoji.json';
@ -96,6 +97,10 @@ export default class EmojiProvider extends AutocompleteProvider {
}
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');
let completions = [];
@ -133,7 +138,7 @@ export default class EmojiProvider extends AutocompleteProvider {
return {
completion: unicode,
component: (
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{unicode}</EmojiText>} />
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{ unicode }</EmojiText>} />
),
range,
};
@ -147,14 +152,13 @@ export default class EmojiProvider extends AutocompleteProvider {
}
static getInstance() {
if (instance == null)
{instance = new EmojiProvider();}
if (instance == null) {instance = new EmojiProvider();}
return instance;
}
renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill">
{completions}
{ completions }
</div>;
}
}

View file

@ -106,7 +106,7 @@ export default class RoomProvider extends AutocompleteProvider {
renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
{completions}
{ completions }
</div>;
}
}

View file

@ -33,7 +33,8 @@ const USER_REGEX = /@\S*/g;
let instance = null;
export default class UserProvider extends AutocompleteProvider {
users: Array<RoomMember> = [];
users: Array<RoomMember> = null;
room: Room = null;
constructor() {
super(USER_REGEX, {
@ -54,8 +55,11 @@ export default class UserProvider extends AutocompleteProvider {
return [];
}
// lazy-load user list into matcher
if (this.users === null) this._makeUsers();
let completions = [];
let {command, range} = this.getCurrentCommand(query, selection, force);
const {command, range} = this.getCurrentCommand(query, selection, force);
if (command) {
completions = this.matcher.match(command[0]).map((user) => {
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,
component: (
<PillCompletion
initialComponent={<MemberAvatar member={user} width={24} height={24}/>}
initialComponent={<MemberAvatar member={user} width={24} height={24} />}
title={displayName}
description={user.userId} />
),
@ -83,7 +87,12 @@ export default class UserProvider extends AutocompleteProvider {
}
setUserListFromRoom(room: Room) {
const events = room.getLiveTimeline().getEvents();
this.room = room;
this.users = null;
}
_makeUsers() {
const events = this.room.getLiveTimeline().getEvents();
const lastSpoken = {};
for(const event of events) {
@ -91,7 +100,7 @@ export default class UserProvider extends AutocompleteProvider {
}
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;
});
@ -103,7 +112,8 @@ export default class UserProvider extends AutocompleteProvider {
}
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
this.users.splice(
@ -122,7 +132,7 @@ export default class UserProvider extends AutocompleteProvider {
renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
{completions}
{ completions }
</div>;
}

View file

@ -17,9 +17,9 @@ limitations under the License.
'use strict';
var classNames = require('classnames');
var React = require('react');
var ReactDOM = require('react-dom');
const classNames = require('classnames');
const React = require('react');
const ReactDOM = require('react-dom');
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
@ -36,7 +36,7 @@ module.exports = {
},
getOrCreateContainer: function() {
var container = document.getElementById(this.ContextualMenuContainerId);
let container = document.getElementById(this.ContextualMenuContainerId);
if (!container) {
container = document.createElement("div");
@ -48,9 +48,9 @@ module.exports = {
},
createMenu: function(Element, props) {
var self = this;
const self = this;
var closeMenu = function() {
const closeMenu = function() {
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
if (props && props.onFinished) {
@ -58,17 +58,17 @@ module.exports = {
}
};
var position = {
const position = {
top: props.top,
};
var chevronOffset = {};
const chevronOffset = {};
if (props.chevronOffset) {
chevronOffset.top = props.chevronOffset;
}
// To override the default chevron colour, if it's been set
var chevronCSS = "";
let chevronCSS = "";
if (props.menuColour) {
chevronCSS = `
.mx_ContextualMenu_chevron_left:after {
@ -81,7 +81,7 @@ module.exports = {
`;
}
var chevron = null;
let chevron = null;
if (props.left) {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>;
position.left = props.left;
@ -90,15 +90,15 @@ module.exports = {
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_left': props.left,
'mx_ContextualMenu_right': !props.left,
});
var menuStyle = {};
const menuStyle = {};
if (props.menuWidth) {
menuStyle.width = props.menuWidth;
}
@ -113,14 +113,14 @@ module.exports = {
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// 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={menuClasses} style={menuStyle}>
{chevron}
<Element {...props} onFinished={closeMenu}/>
{ chevron }
<Element {...props} onFinished={closeMenu} />
</div>
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
<style>{chevronCSS}</style>
<style>{ chevronCSS }</style>
</div>
);

View file

@ -61,7 +61,7 @@ module.exports = React.createClass({
},
onCreateRoom: function() {
var options = {};
const options = {};
if (this.state.room_name) {
options.name = this.state.room_name;
@ -79,14 +79,14 @@ module.exports = React.createClass({
{
type: "m.room.join_rules",
content: {
"join_rule": this.state.is_private ? "invite" : "public"
}
"join_rule": this.state.is_private ? "invite" : "public",
},
},
{
type: "m.room.history_visibility",
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;
var alias = this.getAliasLocalpart();
const alias = this.getAliasLocalpart();
if (alias) {
options.room_alias_name = alias;
}
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
if (!cli) {
// TODO: Error.
console.error("Cannot create room: No matrix client.");
return;
}
var deferred = cli.createRoom(options);
const deferred = cli.createRoom(options);
if (this.state.encrypt) {
// TODO
@ -116,7 +116,7 @@ module.exports = React.createClass({
phase: this.phases.CREATING,
});
var self = this;
const self = this;
deferred.then(function(resp) {
self.setState({
@ -209,7 +209,7 @@ module.exports = React.createClass({
onAliasChanged: function(alias) {
this.setState({
alias: alias
alias: alias,
});
},
@ -220,64 +220,64 @@ module.exports = React.createClass({
},
render: function() {
var curr_phase = this.state.phase;
const curr_phase = this.state.phase;
if (curr_phase == this.phases.CREATING) {
var Loader = sdk.getComponent("elements.Spinner");
const Loader = sdk.getComponent("elements.Spinner");
return (
<Loader/>
<Loader />
);
} else {
var error_box = "";
let error_box = "";
if (curr_phase == this.phases.ERROR) {
error_box = (
<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>
);
}
var CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton");
var RoomAlias = sdk.getComponent("create_room.RoomAlias");
var Presets = sdk.getComponent("create_room.Presets");
var UserSelector = sdk.getComponent("elements.UserSelector");
var SimpleRoomHeader = sdk.getComponent("rooms.SimpleRoomHeader");
const CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton");
const RoomAlias = sdk.getComponent("create_room.RoomAlias");
const Presets = sdk.getComponent("create_room.Presets");
const UserSelector = sdk.getComponent("elements.UserSelector");
const SimpleRoomHeader = sdk.getComponent("rooms.SimpleRoomHeader");
var domain = MatrixClientPeg.get().getDomain();
const domain = MatrixClientPeg.get().getDomain();
return (
<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">
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder={_t('Name')}/> <br />
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder={_t('Topic')}/> <br />
<RoomAlias ref="alias" alias={this.state.alias} homeserver={ domain } onChange={this.onAliasChanged}/> <br />
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br />
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder={_t('Name')} /> <br />
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder={_t('Topic')} /> <br />
<RoomAlias ref="alias" alias={this.state.alias} homeserver={domain} onChange={this.onAliasChanged} /> <br />
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged} /> <br />
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset} /> <br />
<div>
<label>
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/>
{_t('Make this room private')}
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged} />
{ _t('Make this room private') }
</label>
</div>
<div>
<label>
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged}/>
{_t('Share message history with new users')}
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged} />
{ _t('Share message history with new users') }
</label>
</div>
<div className="mx_CreateRoom_encrypt">
<label>
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/>
{_t('Encrypt room')}
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged} />
{ _t('Encrypt room') }
</label>
</div>
<div>
<CreateRoomButton onCreateRoom={this.onCreateRoom} /> <br />
</div>
{error_box}
{ error_box }
</div>
</div>
);
}
}
},
});

View file

@ -24,7 +24,7 @@ import { _t, _tJsx } from '../../languageHandler';
/*
* Component which shows the filtered file using a TimelinePanel
*/
var FilePanel = React.createClass({
const FilePanel = React.createClass({
displayName: 'FilePanel',
propTypes: {
@ -55,33 +55,33 @@ var FilePanel = React.createClass({
},
updateTimelineSet: function(roomId) {
var client = MatrixClientPeg.get();
var room = client.getRoom(roomId);
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
this.noRoom = !room;
if (room) {
var filter = new Matrix.Filter(client.credentials.userId);
const filter = new Matrix.Filter(client.credentials.userId);
filter.setDefinition(
{
"room": {
"timeline": {
"contains_url": true
"contains_url": true,
},
},
},
}
}
);
// FIXME: we shouldn't be doing this every time we change room - see comment above.
client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then(
(filterId)=>{
filter.filterId = filterId;
var timelineSet = room.getOrCreateFilteredTimelineSet(filter);
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
this.setState({ timelineSet: timelineSet });
},
(error)=>{
console.error("Failed to get or create file panel filter", error);
}
},
);
} else {
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
@ -92,18 +92,18 @@ var FilePanel = React.createClass({
if (MatrixClientPeg.get().isGuest()) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty">
{_tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{sub}</a>)}
{ _tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{ sub }</a>) }
</div>
</div>;
} else if (this.noRoom) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty">{_t("You must join the room to see its files")}</div>
<div className="mx_RoomView_empty">{ _t("You must join the room to see its files") }</div>
</div>;
}
// wrap a TimelinePanel with the jump-to-event bits turned off.
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
var Loader = sdk.getComponent("elements.Spinner");
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const Loader = sdk.getComponent("elements.Spinner");
if (this.state.timelineSet) {
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
@ -114,17 +114,16 @@ var FilePanel = React.createClass({
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={this.state.timelineSet}
showUrlPreview = { false }
showUrlPreview = {false}
tileShape="file_grid"
opacity={ this.props.opacity }
opacity={this.props.opacity}
empty={_t('There are no visible files in this room')}
/>
);
}
else {
} else {
return (
<div className="mx_FilePanel">
<Loader/>
<Loader />
</div>
);
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd.
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.
@ -16,6 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import Promise from 'bluebird';
import MatrixClientPeg from '../../MatrixClientPeg';
import sdk from '../../index';
import dis from '../../dispatcher';
@ -25,6 +27,11 @@ import AccessibleButton from '../views/elements/AccessibleButton';
import Modal from '../../Modal';
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({
room_id: PropTypes.string.isRequired,
profile: PropTypes.shape({
@ -37,6 +44,9 @@ const RoomSummaryType = PropTypes.shape({
const UserSummaryType = PropTypes.shape({
summaryInfo: PropTypes.shape({
user_id: PropTypes.string.isRequired,
role_id: PropTypes.string,
avatar_url: PropTypes.string,
displayname: PropTypes.string,
}).isRequired,
});
@ -50,19 +60,81 @@ const CategoryRoomList = React.createClass({
name: PropTypes.string,
}).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() {
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) => {
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) {
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>
{catHeader}
{roomNodes}
return <div className="mx_GroupView_featuredThings_container">
{ catHeader }
{ roomNodes }
{ addButton }
</div>;
},
});
@ -72,6 +144,8 @@ const FeaturedRoom = React.createClass({
props: {
summaryInfo: RoomSummaryType.isRequired,
editing: PropTypes.bool.isRequired,
groupId: PropTypes.string.isRequired,
},
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() {
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
const roomName = this.props.summaryInfo.profile.name ||
this.props.summaryInfo.profile.canonical_alias ||
_t("Unnamed Room");
const oobData = {
roomId: this.props.summaryInfo.room_id,
avatarUrl: this.props.summaryInfo.profile.avatar_url,
name: this.props.summaryInfo.profile.name,
name: roomName,
};
let permalink = null;
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
}
let roomNameNode = null;
if (permalink) {
roomNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.profile.name}</a>;
roomNameNode = <a href={permalink} onClick={this.onClick} >{ roomName }</a>;
} 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}>
<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>;
},
});
@ -121,19 +236,75 @@ const RoleUserList = React.createClass({
name: PropTypes.string,
}).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() {
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) => {
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) {
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>
{roleHeader}
{userNodes}
return <div className="mx_GroupView_featuredThings_container">
{ roleHeader }
{ userNodes }
{ addButton }
</div>;
},
});
@ -143,6 +314,8 @@ const FeaturedUser = React.createClass({
props: {
summaryInfo: UserSummaryType.isRequired,
editing: PropTypes.bool.isRequired,
groupId: PropTypes.string.isRequired,
},
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() {
// 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 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}>
<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>;
},
});
const GroupContext = {
groupStore: React.PropTypes.instanceOf(GroupStore).isRequired,
};
CategoryRoomList.contextTypes = GroupContext;
FeaturedRoom.contextTypes = GroupContext;
RoleUserList.contextTypes = GroupContext;
FeaturedUser.contextTypes = GroupContext;
export default React.createClass({
displayName: 'GroupView',
@ -176,6 +394,16 @@ export default React.createClass({
groupId: PropTypes.string.isRequired,
},
childContextTypes: {
groupStore: React.PropTypes.instanceOf(GroupStore),
},
getChildContext: function() {
return {
groupStore: this._groupStore,
};
},
getInitialState: function() {
return {
summary: null,
@ -183,12 +411,21 @@ export default React.createClass({
editing: false,
saving: false,
uploadingAvatar: false,
membershipBusy: false,
publicityBusy: false,
};
},
componentWillMount: function() {
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) {
@ -197,18 +434,37 @@ export default React.createClass({
summary: null,
error: null,
}, () => {
this._loadGroupFromServer(newProps.groupId);
this._initGroupStore(newProps.groupId);
});
}
},
_loadGroupFromServer: function(groupId) {
MatrixClientPeg.get().getGroupSummary(groupId).done((res) => {
_onGroupMyMembership: function(group) {
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({
summary: res,
summary,
isGroupPublicised: this._groupStore.getGroupPublicity(),
isUserPrivileged: this._groupStore.isUserPrivileged(),
error: null,
});
}, (err) => {
});
this._groupStore.on('error', (err) => {
console.error(err);
this.setState({
summary: null,
error: err,
@ -216,6 +472,10 @@ export default React.createClass({
});
},
_onShowRhsClick: function(ev) {
dis.dispatch({ action: 'show_right_panel' });
},
_onEditClick: function() {
this.setState({
editing: true,
@ -230,15 +490,15 @@ export default React.createClass({
});
},
_onNameChange: function(e) {
const newProfileForm = Object.assign(this.state.profileForm, { name: e.target.value });
_onNameChange: function(value) {
const newProfileForm = Object.assign(this.state.profileForm, { name: value });
this.setState({
profileForm: newProfileForm,
});
},
_onShortDescChange: function(e) {
const newProfileForm = Object.assign(this.state.profileForm, { short_description: e.target.value });
_onShortDescChange: function(value) {
const newProfileForm = Object.assign(this.state.profileForm, { short_description: value });
this.setState({
profileForm: newProfileForm,
});
@ -281,24 +541,113 @@ export default React.createClass({
editing: false,
summary: null,
});
this._loadGroupFromServer(this.props.groupId);
this._initGroupStore(this.props.groupId);
}).catch((e) => {
this.setState({
saving: false,
});
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, {
title: _t('Error'),
description: _t('Failed to update group'),
description: _t('Failed to update community'),
});
}).done();
},
_getFeaturedRoomsNode() {
const summary = this.state.summary;
_onAcceptInviteClick: function() {
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 categoryRooms = {};
@ -315,29 +664,32 @@ export default React.createClass({
}
});
let defaultCategoryNode = null;
if (defaultCategoryRooms.length > 0) {
defaultCategoryNode = <CategoryRoomList rooms={defaultCategoryRooms} />;
}
const defaultCategoryNode = <CategoryRoomList
rooms={defaultCategoryRooms}
groupId={this.props.groupId}
editing={this.state.editing} />;
const categoryRoomNodes = Object.keys(categoryRooms).map((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">
<div className="mx_GroupView_featuredThings_header">
{_t('Featured Rooms:')}
{ _t('Featured Rooms:') }
</div>
{defaultCategoryNode}
{categoryRoomNodes}
{ defaultCategoryNode }
{ categoryRoomNodes }
</div>;
},
_getFeaturedUsersNode() {
_getFeaturedUsersNode: function() {
const summary = this.state.summary;
if (summary.users_section.users.length == 0) return null;
const noRoleUsers = [];
const roleUsers = {};
summary.users_section.users.forEach((u) => {
@ -353,46 +705,156 @@ export default React.createClass({
}
});
let noRoleNode = null;
if (noRoleUsers.length > 0) {
noRoleNode = <RoleUserList users={noRoleUsers} />;
}
const noRoleNode = <RoleUserList
users={noRoleUsers}
groupId={this.props.groupId}
editing={this.state.editing} />;
const roleUserNodes = Object.keys(roleUsers).map((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">
<div className="mx_GroupView_featuredThings_header">
{_t('Featured Users:')}
{ _t('Featured Users:') }
</div>
{noRoleNode}
{roleUserNodes}
{ noRoleNode }
{ roleUserNodes }
</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() {
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
const Loader = sdk.getComponent("elements.Spinner");
const Spinner = sdk.getComponent("elements.Spinner");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
if (this.state.summary === null && this.state.error === null || this.state.saving) {
return <Loader />;
return <Spinner />;
} else if (this.state.summary) {
const summary = this.state.summary;
let avatarNode;
let nameNode;
let shortDescNode;
let rightButtons;
let roomBody;
const bodyNodes = [
this._getMembershipSection(),
this.state.editing ? this._getMemberSettingsSection() : null,
this._getLongDescriptionNode(),
this._getRoomsNode(),
];
const rightButtons = [];
const headerClasses = {
mx_GroupView_header: true,
};
if (this.state.editing) {
let avatarImage;
if (this.state.uploadingAvatar) {
avatarImage = <Loader />;
avatarImage = <Spinner />;
} else {
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
avatarImage = <GroupAvatar groupId={this.props.groupId}
@ -404,45 +866,53 @@ export default React.createClass({
avatarNode = (
<div className="mx_GroupView_avatarPicker">
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
{avatarImage}
{ avatarImage }
</label>
<div className="mx_GroupView_avatarPicker_edit">
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
<img src="img/camera.svg"
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
alt={_t("Upload avatar")} title={_t("Upload avatar")}
width="17" height="15" />
</label>
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected}/>
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected} />
</div>
</div>
);
nameNode = <input type="text"
value={this.state.profileForm.name}
onChange={this._onNameChange}
placeholder={_t('Group Name')}
const EditableText = sdk.getComponent("elements.EditableText");
nameNode = <EditableText ref="nameEditor"
className="mx_GroupView_editable"
placeholderClassName="mx_GroupView_placeholder"
placeholder={_t('Community Name')}
blurToCancel={false}
initialValue={this.state.profileForm.name}
onValueChanged={this._onNameChange}
tabIndex="1"
/>;
shortDescNode = <input type="text"
value={this.state.profileForm.short_description}
onChange={this._onShortDescChange}
placeholder={_t('Description')}
dir="auto" />;
shortDescNode = <EditableText ref="descriptionEditor"
className="mx_GroupView_editable"
placeholderClassName="mx_GroupView_placeholder"
placeholder={_t("Description")}
blurToCancel={false}
initialValue={this.state.profileForm.short_description}
onValueChanged={this._onShortDescChange}
tabIndex="2"
/>;
rightButtons = <span>
<AccessibleButton className="mx_GroupView_saveButton mx_RoomHeader_textButton" onClick={this._onSaveClick}>
{_t('Save')}
</AccessibleButton>
<AccessibleButton className='mx_GroupView_cancelButton' onClick={this._onCancelClick}>
<img src="img/cancel.svg" className='mx_filterFlipColor'
width="18" height="18" alt={_t("Cancel")}/>
</AccessibleButton>
</span>;
roomBody = <div>
<textarea className="mx_GroupView_editLongDesc" value={this.state.profileForm.long_description}
onChange={this._onLongDescChange}
tabIndex="3"
/>
</div>;
dir="auto" />;
rightButtons.push(
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onSaveClick} key="_saveButton"
>
{ _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 {
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
avatarNode = <GroupAvatar
@ -451,32 +921,34 @@ export default React.createClass({
width={48} height={48}
/>;
if (summary.profile && summary.profile.name) {
nameNode = <div>
<span>{summary.profile.name}</span>
nameNode = <div onClick={this._onEditClick}>
<span>{ summary.profile.name }</span>
<span className="mx_GroupView_header_groupid">
({this.props.groupId})
({ this.props.groupId })
</span>
</div>;
} else {
nameNode = <span>{this.props.groupId}</span>;
nameNode = <span onClick={this._onEditClick}>{ this.props.groupId }</span>;
}
shortDescNode = <span>{summary.profile.short_description}</span>;
let description = null;
if (summary.profile && summary.profile.long_description) {
description = sanitizedHtmlNode(summary.profile.long_description);
if (summary.profile && summary.profile.short_description) {
shortDescNode = <span onClick={this._onEditClick}>{ summary.profile.short_description }</span>;
}
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")}
rightButtons.push(
<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>;
<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>,
);
}
headerClasses.mx_GroupView_header_view = true;
}
@ -486,40 +958,42 @@ export default React.createClass({
<div className={classnames(headerClasses)}>
<div className="mx_GroupView_header_leftCol">
<div className="mx_GroupView_header_avatar">
{avatarNode}
{ avatarNode }
</div>
<div className="mx_GroupView_header_info">
<div className="mx_GroupView_header_name">
{nameNode}
{ nameNode }
</div>
<div className="mx_GroupView_header_shortDesc">
{shortDescNode}
{ shortDescNode }
</div>
</div>
</div>
<div className="mx_GroupView_header_rightCol">
{rightButtons}
{ rightButtons }
</div>
</div>
{roomBody}
<GeminiScrollbar className="mx_GroupView_body">
{ bodyNodes }
</GeminiScrollbar>
</div>
);
} else if (this.state.error) {
if (this.state.error.httpStatus === 404) {
return (
<div className="mx_GroupView_error">
Group {this.props.groupId} not found
{ _t('Community %(groupId)s not found', {groupId: this.props.groupId}) }
</div>
);
} else {
let extraText;
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 (
<div className="mx_GroupView_error">
Failed to load {this.props.groupId}
{extraText}
{ _t('Failed to load %(groupId)', {groupId: this.props.groupId }) }
{ extraText }
</div>
);
}

View file

@ -107,7 +107,7 @@ export default React.createClass({
const msg = error.message || error.toString();
this.setState({
errorText: msg
errorText: msg,
});
}).done();
@ -207,7 +207,7 @@ export default React.createClass({
if (this.state.errorText) {
error = (
<div className="error">
{this.state.errorText}
{ this.state.errorText }
</div>
);
}
@ -215,8 +215,8 @@ export default React.createClass({
return (
<div>
<div>
{this._renderCurrentStage()}
{error}
{ this._renderCurrentStage() }
{ error }
</div>
</div>
);

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
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.
@ -81,10 +82,6 @@ export default React.createClass({
// stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient;
// _scrollStateMap is a map from room id to the scroll state returned by
// RoomView.getScrollState()
this._scrollStateMap = {};
CallMediaHandler.loadDevices();
document.addEventListener('keydown', this._onKeyDown);
@ -116,10 +113,6 @@ export default React.createClass({
return Boolean(MatrixClientPeg.get());
},
getScrollStateForRoom: function(roomId) {
return this._scrollStateMap[roomId];
},
canResetTimelineInRoom: function(roomId) {
if (!this.refs.roomView) {
return true;
@ -139,6 +132,9 @@ export default React.createClass({
useCompactLayout: event.getContent().useCompactLayout,
});
}
if (event.getType() === "m.ignored_user_list") {
dis.dispatch({action: "ignore_state_changed"});
}
},
_onKeyDown: function(ev) {
@ -169,7 +165,7 @@ export default React.createClass({
case KeyCode.UP:
case KeyCode.DOWN:
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';
dis.dispatch({action: action});
handled = true;
@ -211,8 +207,7 @@ export default React.createClass({
_onScrollKeyPressed: function(ev) {
if (this.refs.roomView) {
this.refs.roomView.handleScrollKey(ev);
}
else if (this.refs.roomDirectory) {
} else if (this.refs.roomDirectory) {
this.refs.roomDirectory.handleScrollKey(ev);
}
},
@ -246,22 +241,20 @@ export default React.createClass({
eventPixelOffset={this.props.initialEventPixelOffset}
key={this.props.currentRoomId || 'roomview'}
opacity={this.props.middleOpacity}
collapsedRhs={this.props.collapse_rhs}
collapsedRhs={this.props.collapseRhs}
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;
case PageTypes.UserSettings:
page_element = <UserSettings
onClose={this.props.onUserSettingsClose}
brand={this.props.config.brand}
enableLabs={this.props.config.enableLabs}
referralBaseUrl={this.props.config.referralBaseUrl}
teamToken={this.props.teamToken}
/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity} />;
break;
case PageTypes.MyGroups:
@ -271,9 +264,9 @@ export default React.createClass({
case PageTypes.CreateRoom:
page_element = <CreateRoom
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;
case PageTypes.RoomDirectory:
@ -306,8 +299,9 @@ export default React.createClass({
case PageTypes.GroupView:
page_element = <GroupView
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;
}
@ -325,7 +319,7 @@ export default React.createClass({
topBar = <MatrixToolbar />;
}
var bodyClasses = 'mx_MatrixChat';
let bodyClasses = 'mx_MatrixChat';
if (topBar) {
bodyClasses += ' mx_MatrixChat_toolbarShowing';
}
@ -335,17 +329,17 @@ export default React.createClass({
return (
<div className='mx_MatrixChat_wrapper'>
{topBar}
{ topBar }
<div className={bodyClasses}>
<LeftPanel
selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapse_lhs || false}
collapsed={this.props.collapseLhs || false}
opacity={this.props.leftOpacity}
/>
<main className='mx_MatrixChat_middlePanel'>
{page_element}
{ page_element }
</main>
{right_panel}
{ right_panel }
</div>
</div>
);

View file

@ -32,13 +32,12 @@ import dis from "../../dispatcher";
import Modal from "../../Modal";
import Tinter from "../../Tinter";
import sdk from '../../index';
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../Invite';
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite';
import * as Rooms from '../../Rooms';
import linkifyMatrix from "../../linkify-matrix";
import * as Lifecycle from '../../Lifecycle';
// LifecycleStore is not used but does listen to and dispatch actions
require('../../stores/LifecycleStore');
import RoomViewStore from '../../stores/RoomViewStore';
import PageTypes from '../../PageTypes';
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
viewUserId: null,
collapse_lhs: false,
collapse_rhs: false,
collapseLhs: false,
collapseRhs: false,
leftOpacity: 1.0,
middleOpacity: 1.0,
rightOpacity: 1.0,
@ -214,9 +213,6 @@ module.exports = React.createClass({
componentWillMount: function() {
SdkConfig.put(this.props.config);
this._roomViewStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdated);
this._onRoomViewStoreUpdated();
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
// Used by _viewRoom before getting state from sync
@ -353,7 +349,6 @@ module.exports = React.createClass({
UDEHandler.stopListening();
window.removeEventListener("focus", this.onFocus);
window.removeEventListener('resize', this.handleResize);
this._roomViewStoreToken.remove();
},
componentDidUpdate: function() {
@ -439,7 +434,7 @@ module.exports = React.createClass({
break;
case 'view_user':
// FIXME: ugly hack to expand the RightPanel and then re-dispatch.
if (this.state.collapse_rhs) {
if (this.state.collapseRhs) {
setTimeout(()=>{
dis.dispatch({
action: 'show_right_panel',
@ -521,22 +516,22 @@ module.exports = React.createClass({
break;
case 'hide_left_panel':
this.setState({
collapse_lhs: true,
collapseLhs: true,
});
break;
case 'show_left_panel':
this.setState({
collapse_lhs: false,
collapseLhs: false,
});
break;
case 'hide_right_panel':
this.setState({
collapse_rhs: true,
collapseRhs: true,
});
break;
case 'show_right_panel':
this.setState({
collapse_rhs: false,
collapseRhs: false,
});
break;
case 'ui_opacity': {
@ -587,10 +582,6 @@ module.exports = React.createClass({
}
},
_onRoomViewStoreUpdated: function() {
this.setState({ currentRoomId: RoomViewStore.getRoomId() });
},
_setPage: function(pageType) {
this.setState({
page_type: pageType,
@ -677,10 +668,10 @@ module.exports = React.createClass({
this.focusComposer = true;
const newState = {
currentRoomId: roomInfo.room_id || null,
page_type: PageTypes.RoomView,
thirdPartyInvite: roomInfo.third_party_invite,
roomOobData: roomInfo.oob_data,
autoJoin: roomInfo.auto_join,
};
if (roomInfo.room_alias) {
@ -782,15 +773,13 @@ module.exports = React.createClass({
dis.dispatch({action: 'view_set_mxid'});
return;
}
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
Modal.createTrackedDialog('Create Room', '', TextInputDialog, {
title: _t('Create Room'),
description: _t('Room name (optional)'),
button: _t('Create Room'),
onFinished: (shouldCreate, name) => {
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
onFinished: (shouldCreate, name, noFederate) => {
if (shouldCreate) {
const createOpts = {};
if (name) createOpts.name = name;
if (noFederate) createOpts.creation_content = {'m.federate': false};
createRoom({createOpts}).done();
}
},
@ -862,7 +851,7 @@ module.exports = React.createClass({
title: _t("Leave room"),
description: (
<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>
),
onFinished: (shouldLeave) => {
@ -1002,8 +991,8 @@ module.exports = React.createClass({
this.setStateForNewView({
view: VIEWS.LOGIN,
ready: false,
collapse_lhs: false,
collapse_rhs: false,
collapseLhs: false,
collapseRhs: false,
currentRoomId: null,
page_type: PageTypes.RoomDirectory,
});
@ -1459,7 +1448,7 @@ module.exports = React.createClass({
return (
<div className="mx_MatrixChat_splash">
<Spinner />
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
{ _t('Logout') }
</a>
</div>

View file

@ -65,7 +65,7 @@ module.exports = React.createClass({
suppressFirstDateSeparator: React.PropTypes.bool,
// 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
// 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
// +1: read marker is below the window
getReadMarkerPosition: function() {
var readMarker = this.refs.readMarkerNode;
var messageWrapper = this.refs.scrollPanel;
const readMarker = this.refs.readMarkerNode;
const messageWrapper = this.refs.scrollPanel;
if (!readMarker || !messageWrapper) {
return null;
}
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
var readMarkerRect = readMarker.getBoundingClientRect();
const wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
const readMarkerRect = readMarker.getBoundingClientRect();
// the read-marker pretends to have zero height when it is actually
// two pixels high; +2 here to account for that.
@ -241,6 +241,10 @@ module.exports = React.createClass({
// TODO: Implement granular (per-room) hide options
_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');
if (!EventTile.haveTileForEvent(mxEv)) {
return false; // no tile = no show
@ -258,7 +262,7 @@ module.exports = React.createClass({
this.eventNodes = {};
var i;
let i;
// first figure out which is the last event in the list which we're
// 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.
let lastShownEvent;
var lastShownNonLocalEchoIndex = -1;
let lastShownNonLocalEchoIndex = -1;
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)) {
continue;
}
@ -288,12 +292,12 @@ module.exports = React.createClass({
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
var readMarkerVisible = false;
let readMarkerVisible = false;
// if the readmarker has moved, cancel any active ghost.
if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
@ -305,16 +309,16 @@ module.exports = React.createClass({
const isMembershipChange = (e) => e.getType() === 'm.room.member';
for (i = 0; i < this.props.events.length; i++) {
let mxEv = this.props.events[i];
let eventId = mxEv.getId();
let last = (mxEv === lastShownEvent);
const mxEv = this.props.events[i];
const eventId = mxEv.getId();
const last = (mxEv === lastShownEvent);
const wantTile = this._shouldShowEvent(mxEv);
// Wrap consecutive member events in a ListSummary, ignore if redacted
if (isMembershipChange(mxEv) && wantTile) {
let readMarkerInMels = false;
let ts1 = mxEv.getTs();
const ts1 = mxEv.getTs();
// Ensure that the key of the MemberEventListSummary does not change with new
// member events. This will prevent it from being re-created unnecessarily, and
// 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");
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);
}
@ -335,7 +339,7 @@ module.exports = React.createClass({
readMarkerInMels = true;
}
let summarisedEvents = [mxEv];
const summarisedEvents = [mxEv];
for (;i + 1 < this.props.events.length; i++) {
const collapsedMxEv = this.props.events[i + 1];
@ -361,8 +365,13 @@ module.exports = React.createClass({
summarisedEvents.push(collapsedMxEv);
}
let highlightInMels = false;
// At this point, i = the index of the last event in the summary sequence
let eventTiles = summarisedEvents.map((e) => {
if (e.getId() === this.props.highlightedEventId) {
highlightInMels = true;
}
// In order to prevent DateSeparators from appearing in the expanded form
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
@ -376,15 +385,13 @@ module.exports = React.createClass({
eventTiles = null;
}
ret.push(
<MemberEventListSummary
key={key}
ret.push(<MemberEventListSummary key={key}
events={summarisedEvents}
onToggle={this._onWidgetLoad} // Update scroll state
startExpanded={highlightInMels}
>
{eventTiles}
</MemberEventListSummary>
);
{ eventTiles }
</MemberEventListSummary>);
if (readMarkerInMels) {
ret.push(this._getReadMarkerTile(visible));
@ -401,7 +408,7 @@ module.exports = React.createClass({
prevEvent = mxEv;
}
var isVisibleReadMarker = false;
let isVisibleReadMarker = false;
if (eventId == this.props.readMarkerEventId) {
var visible = this.props.readMarkerVisible;
@ -441,10 +448,10 @@ module.exports = React.createClass({
_getTilesForEvent: function(prevEvent, mxEv, last) {
const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
var ret = [];
const ret = [];
// is this a continuation of the previous message?
var continuation = false;
let continuation = false;
if (prevEvent !== null
&& 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
// as 'today' for the date separators.
var ts1 = mxEv.getTs();
var eventDate = mxEv.getDate();
let ts1 = mxEv.getTs();
let eventDate = mxEv.getDate();
if (mxEv.status) {
eventDate = new Date();
ts1 = eventDate.getTime();
@ -478,20 +485,20 @@ module.exports = React.createClass({
// do we need a date separator since the last event?
if (this._wantsDateSeparator(prevEvent, eventDate)) {
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>;
ret.push(dateSeparator);
continuation = false;
}
var eventId = mxEv.getId();
var highlight = (eventId == this.props.highlightedEventId);
const eventId = mxEv.getId();
const highlight = (eventId == this.props.highlightedEventId);
// we can't use local echoes as scroll tokens, because their event IDs change.
// Local echos have a send "status".
var scrollToken = mxEv.status ? undefined : eventId;
const scrollToken = mxEv.status ? undefined : eventId;
var readReceipts;
if (this.props.manageReadReceipts) {
let readReceipts;
if (this.props.showReadReceipts) {
readReceipts = this._getReadReceiptsForEvent(mxEv);
}
ret.push(
@ -508,8 +515,8 @@ module.exports = React.createClass({
eventSendStatus={mxEv.status}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
last={last} isSelectedEvent={highlight}/>
</li>
last={last} isSelectedEvent={highlight} />
</li>,
);
return ret;
@ -544,12 +551,15 @@ module.exports = React.createClass({
if (!room) {
return null;
}
let receipts = [];
const receipts = [];
room.getReceiptsForEvent(event).forEach((r) => {
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
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) {
return; // ignore unknown user IDs
}
@ -565,7 +575,7 @@ module.exports = React.createClass({
},
_getReadMarkerTile: function(visible) {
var hr;
let hr;
if (visible) {
hr = <hr className="mx_RoomView_myReadMarker"
style={{opacity: 1, width: '99%'}}
@ -575,7 +585,7 @@ module.exports = React.createClass({
return (
<li key="_readupto" ref="readMarkerNode"
className="mx_RoomView_myReadMarker_container">
{hr}
{ hr }
</li>
);
},
@ -594,7 +604,7 @@ module.exports = React.createClass({
},
_getReadMarkerGhostTile: function() {
var hr = <hr className="mx_RoomView_myReadMarker"
const hr = <hr className="mx_RoomView_myReadMarker"
style={{opacity: 1, width: '99%'}}
ref={this._startAnimation}
/>;
@ -605,7 +615,7 @@ module.exports = React.createClass({
return (
<li key={"_readuptoghost_"+this.currentGhostEventId}
className="mx_RoomView_myReadMarker_container">
{hr}
{ hr }
</li>
);
},
@ -617,7 +627,7 @@ module.exports = React.createClass({
// once dynamic content in the events load, make the scrollPanel check the
// scroll offsets.
_onWidgetLoad: function() {
var scrollPanel = this.refs.scrollPanel;
const scrollPanel = this.refs.scrollPanel;
if (scrollPanel) {
scrollPanel.forceUpdate();
}
@ -628,9 +638,9 @@ module.exports = React.createClass({
},
render: function() {
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
var Spinner = sdk.getComponent("elements.Spinner");
var topSpinner, bottomSpinner;
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
const Spinner = sdk.getComponent("elements.Spinner");
let topSpinner, bottomSpinner;
if (this.props.backPaginating) {
topSpinner = <li key="_topSpinner"><Spinner /></li>;
}
@ -638,25 +648,25 @@ module.exports = React.createClass({
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;
var className = this.props.className + " mx_fadable";
let className = this.props.className + " mx_fadable";
if (this.props.alwaysShowTimestamps) {
className += " mx_MessagePanel_alwaysShowTimestamps";
}
return (
<ScrollPanel ref="scrollPanel" className={ className }
onScroll={ this.props.onScroll }
onResize={ this.onResize }
onFillRequest={ this.props.onFillRequest }
onUnfillRequest={ this.props.onUnfillRequest }
style={ style }
stickyBottom={ this.props.stickyBottom }>
{topSpinner}
{this._getEventTiles()}
{bottomSpinner}
<ScrollPanel ref="scrollPanel" className={className}
onScroll={this.props.onScroll}
onResize={this.onResize}
onFillRequest={this.props.onFillRequest}
onUnfillRequest={this.props.onUnfillRequest}
style={style}
stickyBottom={this.props.stickyBottom}>
{ topSpinner }
{ this._getEventTiles() }
{ bottomSpinner }
</ScrollPanel>
);
},

View file

@ -39,7 +39,7 @@ const GroupTile = React.createClass({
},
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() {
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
Modal.createTrackedDialog('Create Group', '', CreateGroupDialog);
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
},
_fetch: function() {
@ -90,51 +90,51 @@ export default withMatrixClient(React.createClass({
);
});
content = <div>
<div>{_t('You are a member of these groups:')}</div>
{groupNodes}
<div>{ _t('You are a member of these communities:') }</div>
{ groupNodes }
</div>;
} else if (this.state.error) {
content = <div className="mx_MyGroups_error">
{_t('Error whilst fetching joined groups')}
{ _t('Error whilst fetching joined communities') }
</div>;
} else {
content = <Loader />;
}
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_createBox">
<div className="mx_MyGroups_joinCreateHeader">
{_t('Create a new group')}
{ _t('Create a new community') }
</div>
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onCreateGroupClick}>
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
</AccessibleButton>
{_t(
'Create a group to represent your community! '+
{ _t(
'Create a community to represent your community! '+
'Define a set of rooms and your own custom homepage '+
'to mark out your space in the Matrix universe.',
)}
) }
</div>
<div className="mx_MyGroups_joinBox">
<div className="mx_MyGroups_joinCreateHeader">
{_t('Join an existing group')}
{ _t('Join an existing community') }
</div>
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onJoinGroupClick}>
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
</AccessibleButton>
{_tJsx(
'To join an exisitng group you\'ll have to '+
'know its group identifier; this will look '+
{ _tJsx(
'To join an existing community you\'ll have to '+
'know its community identifier; this will look '+
'something like <i>+example:matrix.org</i>.',
/<i>(.*)<\/i>/,
(sub) => <i>{sub}</i>,
)}
(sub) => <i>{ sub }</i>,
) }
</div>
</div>
<div className="mx_MyGroups_content">
{content}
{ content }
</div>
</div>;
},

View file

@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var ReactDOM = require("react-dom");
const React = require('react');
const ReactDOM = require("react-dom");
import { _t } from '../../languageHandler';
var Matrix = require("matrix-js-sdk");
var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
const Matrix = require("matrix-js-sdk");
const sdk = require('../../index');
const MatrixClientPeg = require("../../MatrixClientPeg");
const dis = require("../../dispatcher");
/*
* Component which shows the global notification list using a TimelinePanel
*/
var NotificationPanel = React.createClass({
const NotificationPanel = React.createClass({
displayName: 'NotificationPanel',
propTypes: {
@ -33,10 +33,10 @@ var NotificationPanel = React.createClass({
render: function() {
// wrap a TimelinePanel with the jump-to-event bits turned off.
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
var Loader = sdk.getComponent("elements.Spinner");
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const Loader = sdk.getComponent("elements.Spinner");
var timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) {
return (
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
@ -44,18 +44,17 @@ var NotificationPanel = React.createClass({
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={timelineSet}
showUrlPreview = { false }
opacity={ this.props.opacity }
showUrlPreview = {false}
opacity={this.props.opacity}
tileShape="notif"
empty={ _t('You have no visible notifications') }
empty={_t('You have no visible notifications')}
/>
);
}
else {
} else {
console.error("No notifTimelineSet available!");
return (
<div className="mx_NotificationPanel">
<Loader/>
<Loader />
</div>
);
}

View file

@ -43,6 +43,10 @@ module.exports = React.createClass({
// the end of the live timeline.
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
// the 'Active Call' text in the status bar if there is nothing
// more interesting)
@ -60,6 +64,14 @@ module.exports = React.createClass({
// 'unsent messages' bar
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
onScrollToBottomClick: React.PropTypes.func,
@ -103,7 +115,7 @@ module.exports = React.createClass({
componentWillUnmount: function() {
// 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) {
client.removeListener("sync", this.onSyncStateChange);
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
@ -115,18 +127,18 @@ module.exports = React.createClass({
return;
}
this.setState({
syncState: state
syncState: state,
});
},
onRoomMemberTyping: function(ev, member) {
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
_checkSize: function () {
_checkSize: function() {
if (this.props.onVisible && this._getSize()) {
this.props.onVisible();
}
@ -140,7 +152,8 @@ module.exports = React.createClass({
(this.state.usersTyping.length > 0) ||
this.props.numUnreadMessages ||
!this.props.atEndOfLiveTimeline ||
this.props.hasActiveCall
this.props.hasActiveCall ||
this.props.sentMessageAndIsAlone
) {
return STATUS_BAR_EXPANDED;
} else if (this.props.unsentMessageError) {
@ -157,9 +170,9 @@ module.exports = React.createClass({
if (this.props.numUnreadMessages) {
return (
<div className="mx_RoomStatusBar_scrollDownIndicator"
onClick={ this.props.onScrollToBottomClick }>
onClick={this.props.onScrollToBottomClick}>
<img src="img/newmessages.svg" width="24" height="24"
alt=""/>
alt="" />
</div>
);
}
@ -167,18 +180,18 @@ module.exports = React.createClass({
if (!this.props.atEndOfLiveTimeline) {
return (
<div className="mx_RoomStatusBar_scrollDownIndicator"
onClick={ this.props.onScrollToBottomClick }>
onClick={this.props.onScrollToBottomClick}>
<img src="img/scrolldown.svg" width="24" height="24"
alt={ _t("Scroll to bottom of page") }
title={ _t("Scroll to bottom of page") }/>
alt={_t("Scroll to bottom of page")}
title={_t("Scroll to bottom of page")} />
</div>
);
}
if (this.props.hasActiveCall) {
var TintableSvg = sdk.getComponent("elements.TintableSvg");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
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) {
return (
<div className="mx_RoomStatusBar_typingIndicatorAvatars">
{this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit)}
{ this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit) }
</div>
);
}
@ -221,8 +234,8 @@ module.exports = React.createClass({
if (othersCount > 0) {
avatars.push(
<span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others">
+{othersCount}
</span>
+{ othersCount }
</span>,
);
}
@ -240,12 +253,12 @@ module.exports = React.createClass({
if (this.state.syncState === "ERROR") {
return (
<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">
{_t('Connectivity to the server has been lost.')}
{ _t('Connectivity to the server has been lost.') }
</div>
<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>
);
@ -254,18 +267,18 @@ module.exports = React.createClass({
if (this.props.unsentMessageError) {
return (
<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">
{ this.props.unsentMessageError }
</div>
<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>/],
[
(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="resend" onClick={this.props.onResendAllClick}>{ sub }</a>,
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this.props.onCancelAllClick}>{ sub }</a>,
],
) }
</div>
</div>
);
@ -275,24 +288,24 @@ module.exports = React.createClass({
// set when you've scrolled up
if (this.props.numUnreadMessages) {
// 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 (
<div className="mx_RoomStatusBar_unreadMessagesBar"
onClick={ this.props.onScrollToBottomClick }>
{unreadMsgs}
onClick={this.props.onScrollToBottomClick}>
{ unreadMsgs }
</div>
);
}
const typingString = WhoIsTyping.whoIsTypingString(
this.state.usersTyping,
this.props.whoIsTypingLimit
this.props.whoIsTypingLimit,
);
if (typingString) {
return (
<div className="mx_RoomStatusBar_typingBar">
<EmojiText>{typingString}</EmojiText>
<EmojiText>{ typingString }</EmojiText>
</div>
);
}
@ -300,7 +313,22 @@ module.exports = React.createClass({
if (this.props.hasActiveCall) {
return (
<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>
);
}
@ -310,15 +338,15 @@ module.exports = React.createClass({
render: function() {
var content = this._getContent();
var indicator = this._getIndicator(this.state.usersTyping.length > 0);
const content = this._getContent();
const indicator = this._getIndicator(this.state.usersTyping.length > 0);
return (
<div className="mx_RoomStatusBar">
<div className="mx_RoomStatusBar_indicator">
{indicator}
{ indicator }
</div>
{content}
{ content }
</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.
*/
var React = require("react");
var ReactDOM = require("react-dom");
var GeminiScrollbar = require('react-gemini-scrollbar');
const React = require("react");
const ReactDOM = require("react-dom");
const GeminiScrollbar = require('react-gemini-scrollbar');
import Promise from 'bluebird';
var KeyCode = require('../../KeyCode');
const KeyCode = require('../../KeyCode');
var DEBUG_SCROLL = false;
const DEBUG_SCROLL = false;
// var DEBUG_SCROLL = true;
// 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); },
onUnfillRequest: function(backwards, scrollToken) {},
onScroll: function() {},
onResize: function() {},
};
},
@ -157,7 +158,7 @@ module.exports = React.createClass({
},
componentDidMount: function() {
this.checkFillState();
this.checkScroll();
},
componentDidUpdate: function() {
@ -178,7 +179,7 @@ module.exports = React.createClass({
},
onScroll: function(ev) {
var sn = this._getScrollNode();
const sn = this._getScrollNode();
debuglog("Scroll event: offset now:", sn.scrollTop,
"_lastSetScroll:", this._lastSetScroll);
@ -238,7 +239,7 @@ module.exports = React.createClass({
// about whether the the content is scrolled down right now, irrespective of
// whether it will stay that way when the children update.
isAtBottom: function() {
var sn = this._getScrollNode();
const sn = this._getScrollNode();
// there seems to be some bug with flexbox/gemini/chrome/richvdh's
// understanding of the box model, wherein the scrollNode ends up 2
@ -281,7 +282,7 @@ module.exports = React.createClass({
// |#########| |
// `---------' -
_getExcessHeight: function(backwards) {
var sn = this._getScrollNode();
const sn = this._getScrollNode();
if (backwards) {
return sn.scrollTop - sn.clientHeight - UNPAGINATION_PADDING;
} else {
@ -295,7 +296,7 @@ module.exports = React.createClass({
return;
}
var sn = this._getScrollNode();
const sn = this._getScrollNode();
// if there is less than a screenful of messages above or below the
// 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.
_maybeFill: function(backwards) {
var dir = backwards ? 'b' : 'f';
const dir = backwards ? 'b' : 'f';
if (this._pendingFillRequests[dir]) {
debuglog("ScrollPanel: Already a "+dir+" fill in progress - not starting another");
return;
@ -470,8 +471,8 @@ module.exports = React.createClass({
* mult: -1 to page up, +1 to page down
*/
scrollRelative: function(mult) {
var scrollNode = this._getScrollNode();
var delta = mult * scrollNode.clientHeight * 0.5;
const scrollNode = this._getScrollNode();
const delta = mult * scrollNode.clientHeight * 0.5;
this._setScrollTop(scrollNode.scrollTop + delta);
this._saveScrollState();
},
@ -535,7 +536,7 @@ module.exports = React.createClass({
this.scrollState = {
stuckAtBottom: false,
trackedScrollToken: scrollToken,
pixelOffset: pixelOffset
pixelOffset: pixelOffset,
};
// ... then make it so.
@ -546,10 +547,10 @@ module.exports = React.createClass({
// given offset in the window. A helper for _restoreSavedScrollState.
_scrollToToken: function(scrollToken, pixelOffset) {
/* find the dom node with the right scrolltoken */
var node;
var messages = this.refs.itemlist.children;
for (var i = messages.length-1; i >= 0; --i) {
var m = messages[i];
let node;
const messages = this.refs.itemlist.children;
for (let i = messages.length-1; i >= 0; --i) {
const m = messages[i];
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
// There might only be one scroll token
if (m.dataset.scrollTokens &&
@ -564,10 +565,10 @@ module.exports = React.createClass({
return;
}
var scrollNode = this._getScrollNode();
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
var boundingRect = node.getBoundingClientRect();
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
const scrollNode = this._getScrollNode();
const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
const boundingRect = node.getBoundingClientRect();
const scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")");
@ -575,7 +576,6 @@ module.exports = React.createClass({
if(scrollDelta != 0) {
this._setScrollTop(scrollNode.scrollTop + scrollDelta);
}
},
_saveScrollState: function() {
@ -585,16 +585,16 @@ module.exports = React.createClass({
return;
}
var itemlist = this.refs.itemlist;
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
var messages = itemlist.children;
const itemlist = this.refs.itemlist;
const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
const messages = itemlist.children;
let newScrollState = null;
for (var i = messages.length-1; i >= 0; --i) {
var node = messages[i];
for (let i = messages.length-1; i >= 0; --i) {
const node = messages[i];
if (!node.dataset.scrollTokens) continue;
var boundingRect = node.getBoundingClientRect();
const boundingRect = node.getBoundingClientRect();
newScrollState = {
stuckAtBottom: false,
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
@ -619,8 +619,8 @@ module.exports = React.createClass({
},
_restoreSavedScrollState: function() {
var scrollState = this.scrollState;
var scrollNode = this._getScrollNode();
const scrollState = this.scrollState;
const scrollNode = this._getScrollNode();
if (scrollState.stuckAtBottom) {
this._setScrollTop(Number.MAX_VALUE);
@ -631,9 +631,9 @@ module.exports = React.createClass({
},
_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
scrollNode.scrollTop = Math.min(scrollTop, scrollNode.scrollHeight);
@ -676,7 +676,7 @@ module.exports = React.createClass({
className={this.props.className} style={this.props.style}>
<div className="mx_RoomView_messageListWrapper">
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
{this.props.children}
{ this.props.children }
</ol>
</div>
</GeminiScrollbar>

View file

@ -15,27 +15,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var ReactDOM = require("react-dom");
const React = require('react');
const ReactDOM = require("react-dom");
import Promise from 'bluebird';
var Matrix = require("matrix-js-sdk");
var EventTimeline = Matrix.EventTimeline;
const Matrix = require("matrix-js-sdk");
const EventTimeline = Matrix.EventTimeline;
var sdk = require('../../index');
const sdk = require('../../index');
import { _t } from '../../languageHandler';
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
var ObjectUtils = require('../../ObjectUtils');
var Modal = require("../../Modal");
var UserActivity = require("../../UserActivity");
var KeyCode = require('../../KeyCode');
const MatrixClientPeg = require("../../MatrixClientPeg");
const dis = require("../../dispatcher");
const ObjectUtils = require('../../ObjectUtils');
const Modal = require("../../Modal");
const UserActivity = require("../../UserActivity");
const KeyCode = require('../../KeyCode');
import UserSettingsStore from '../../UserSettingsStore';
var PAGINATE_SIZE = 20;
var INITIAL_SIZE = 20;
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
var DEBUG = false;
const DEBUG = false;
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
@ -59,6 +59,7 @@ var TimelinePanel = React.createClass({
// that room.
timelineSet: React.PropTypes.object.isRequired,
showReadReceipts: React.PropTypes.bool,
// Enable managing RRs and RMs. These require the timelineSet to have a room.
manageReadReceipts: React.PropTypes.bool,
manageReadMarkers: React.PropTypes.bool,
@ -259,7 +260,7 @@ var TimelinePanel = React.createClass({
dis.unregister(this.dispatcherRef);
var client = MatrixClientPeg.get();
const client = MatrixClientPeg.get();
if (client) {
client.removeListener("Room.timeline", this.onRoomTimeline);
client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
@ -274,20 +275,20 @@ var TimelinePanel = React.createClass({
onMessageListUnfillRequest: function(backwards, scrollToken) {
// 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);
// All tiles are inserted by MessagePanel to have a scrollToken === eventId, and
// 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) => {
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) {
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir);
@ -304,9 +305,9 @@ var TimelinePanel = React.createClass({
// set off a pagination request.
onMessageListFillRequest: function(backwards) {
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
var canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
var paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
if (!this.state[canPaginateKey]) {
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);
var newState = {
const newState = {
[paginatingKey]: false,
[canPaginateKey]: r,
events: this._getEvents(),
@ -335,17 +336,24 @@ var TimelinePanel = React.createClass({
// moving the window in this direction may mean that we can now
// paginate in the other where we previously could not.
var otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
var canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
if (!this.state[canPaginateOtherWayKey] &&
this._timelineWindow.canPaginate(otherDirection)) {
debuglog('TimelinePanel: can now', otherDirection, 'paginate again');
newState[canPaginateOtherWayKey] = true;
}
this.setState(newState);
return r;
// Don't resolve until the setState has completed: we need to let
// the component update before we consider the pagination completed,
// 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.updateReadMarker();
break;
case 'ignore_state_changed':
this.forceUpdate();
break;
}
},
@ -409,15 +420,15 @@ var TimelinePanel = React.createClass({
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => {
if (this.unmounted) { return; }
var events = this._timelineWindow.getEvents();
var lastEv = events[events.length-1];
const events = this._timelineWindow.getEvents();
const lastEv = events[events.length-1];
// if we're at the end of the live timeline, append the pending events
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(...this.props.timelineSet.room.getPendingEvents());
}
var updatedState = {events: events};
const updatedState = {events: events};
if (this.props.manageReadMarkers) {
// 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
// more than the timeout on userCurrentlyActive.
//
var myUserId = MatrixClientPeg.get().credentials.userId;
var sender = ev.sender ? ev.sender.userId : null;
const myUserId = MatrixClientPeg.get().credentials.userId;
const sender = ev.sender ? ev.sender.userId : null;
var callback = null;
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
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
// where the RM is pointing to. The read marker will be invisible for
// now anyway, so this doesn't really matter.
var lastDisplayedIndex = this._getLastDisplayedEventIndex({
const lastDisplayedIndex = this._getLastDisplayedEventIndex({
allowPartial: true,
ignoreEchoes: true,
});
@ -644,7 +655,7 @@ var TimelinePanel = React.createClass({
return;
}
var lastDisplayedEvent = this.state.events[lastDisplayedIndex];
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
this._setReadMarker(lastDisplayedEvent.getId(),
lastDisplayedEvent.getTs());
@ -665,7 +676,7 @@ var TimelinePanel = React.createClass({
// we call _timelineWindow.getEvents() rather than using
// this.state.events, because react batches the update to the latter, so it
// may not have been updated yet.
var events = this._timelineWindow.getEvents();
const events = this._timelineWindow.getEvents();
// first find where the current RM is
for (var i = 0; i < events.length; i++) {
@ -678,7 +689,7 @@ var TimelinePanel = React.createClass({
}
// now think about advancing it
var myUserId = MatrixClientPeg.get().credentials.userId;
const myUserId = MatrixClientPeg.get().credentials.userId;
for (i++; i < events.length; i++) {
var ev = events[i];
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
// 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) {
// The messagepanel knows where the RM is, so we must have loaded
// the relevant event.
@ -744,13 +755,13 @@ var TimelinePanel = React.createClass({
forgetReadMarker: function() {
if (!this.props.manageReadMarkers) return;
var rmId = this._getCurrentReadReceipt();
const rmId = this._getCurrentReadReceipt();
// see if we know the timestamp for the rr event
var tl = this.props.timelineSet.getTimelineForEvent(rmId);
var rmTs;
const tl = this.props.timelineSet.getTimelineForEvent(rmId);
let rmTs;
if (tl) {
var event = tl.getEvents().find((e) => { return e.getId() == rmId; });
const event = tl.getEvents().find((e) => { return e.getId() == rmId; });
if (event) {
rmTs = event.getTs();
}
@ -790,7 +801,7 @@ var TimelinePanel = React.createClass({
if (!this.props.manageReadMarkers) return null;
if (!this.refs.messagePanel) return null;
var ret = this.refs.messagePanel.getReadMarkerPosition();
const ret = this.refs.messagePanel.getReadMarkerPosition();
if (ret !== null) {
return ret;
}
@ -833,8 +844,7 @@ var TimelinePanel = React.createClass({
// jump to the live timeline on ctrl-end, rather than the end of the
// timeline window.
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
ev.keyCode == KeyCode.END)
{
ev.keyCode == KeyCode.END) {
this.jumpToLiveTimeline();
} else {
this.refs.messagePanel.handleScrollKey(ev);
@ -842,12 +852,12 @@ var TimelinePanel = React.createClass({
},
_initTimeline: function(props) {
var initialEvent = props.eventId;
var pixelOffset = props.eventPixelOffset;
const initialEvent = props.eventId;
const pixelOffset = props.eventPixelOffset;
// 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.
var offsetBase = 1;
let offsetBase = 1;
if (pixelOffset == null) {
offsetBase = 0.5;
}
@ -876,7 +886,7 @@ var TimelinePanel = React.createClass({
MatrixClientPeg.get(), this.props.timelineSet,
{windowLimit: this.props.timelineCap});
var onLoaded = () => {
const onLoaded = () => {
this._reloadEvents();
// 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});
console.error(
`Error loading timeline panel at ${eventId}: ${error}`,
);
var msg = error.message ? error.message : JSON.stringify(error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const msg = error.message ? error.message : JSON.stringify(error);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var onFinished;
let onFinished;
// 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,
@ -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 was unable to find it.");
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
// 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
_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 (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
@ -996,7 +1006,7 @@ var TimelinePanel = React.createClass({
},
_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()) {
return i;
}
@ -1006,18 +1016,18 @@ var TimelinePanel = React.createClass({
_getLastDisplayedEventIndex: function(opts) {
opts = opts || {};
var ignoreOwn = opts.ignoreOwn || false;
var ignoreEchoes = opts.ignoreEchoes || false;
var allowPartial = opts.allowPartial || false;
const ignoreOwn = opts.ignoreOwn || false;
const ignoreEchoes = opts.ignoreEchoes || false;
const allowPartial = opts.allowPartial || false;
var messagePanel = this.refs.messagePanel;
const messagePanel = this.refs.messagePanel;
if (messagePanel === undefined) return null;
var wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
var myUserId = MatrixClientPeg.get().credentials.userId;
const wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
const myUserId = MatrixClientPeg.get().credentials.userId;
for (var i = this.state.events.length-1; i >= 0; --i) {
var ev = this.state.events[i];
for (let i = this.state.events.length-1; i >= 0; --i) {
const ev = this.state.events[i];
if (ignoreOwn && ev.sender && ev.sender.userId == myUserId) {
continue;
@ -1028,10 +1038,10 @@ var TimelinePanel = React.createClass({
continue;
}
var node = messagePanel.getNodeForEventId(ev.getId());
const node = messagePanel.getNodeForEventId(ev.getId());
if (!node) continue;
var boundingRect = node.getBoundingClientRect();
const boundingRect = node.getBoundingClientRect();
if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
return i;
@ -1049,18 +1059,18 @@ var TimelinePanel = React.createClass({
* SDK.
*/
_getCurrentReadReceipt: function(ignoreSynthesized) {
var client = MatrixClientPeg.get();
const client = MatrixClientPeg.get();
// the client can be null on logout
if (client == null) {
return null;
}
var myUserId = client.credentials.userId;
const myUserId = client.credentials.userId;
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
},
_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
// no change to the RM.
@ -1085,8 +1095,8 @@ var TimelinePanel = React.createClass({
},
render: function() {
var MessagePanel = sdk.getComponent("structures.MessagePanel");
var Loader = sdk.getComponent("elements.Spinner");
const MessagePanel = sdk.getComponent("structures.MessagePanel");
const Loader = sdk.getComponent("elements.Spinner");
// just show a spinner while the timeline loads.
//
@ -1101,7 +1111,7 @@ var TimelinePanel = React.createClass({
// exist.
if (this.state.timelineLoading) {
return (
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
<Loader />
</div>
);
@ -1109,7 +1119,7 @@ var TimelinePanel = React.createClass({
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
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>
);
@ -1123,7 +1133,7 @@ var TimelinePanel = React.createClass({
// forwards, otherwise if somebody hits the bottom of the loaded
// events when viewing historical messages, we get stuck in a loop
// 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
// the HS and fetch the latest events, so we are effectively forward paginating.
@ -1132,26 +1142,26 @@ var TimelinePanel = React.createClass({
);
return (
<MessagePanel ref="messagePanel"
hidden={ this.props.hidden }
backPaginating={ this.state.backPaginating }
forwardPaginating={ forwardPaginating }
events={ this.state.events }
highlightedEventId={ this.props.highlightedEventId }
readMarkerEventId={ this.state.readMarkerEventId }
readMarkerVisible={ this.state.readMarkerVisible }
suppressFirstDateSeparator={ this.state.canBackPaginate }
showUrlPreview = { this.props.showUrlPreview }
manageReadReceipts = { this.props.manageReadReceipts }
ourUserId={ MatrixClientPeg.get().credentials.userId }
stickyBottom={ stickyBottom }
onScroll={ this.onMessageListScroll }
onFillRequest={ this.onMessageListFillRequest }
onUnfillRequest={ this.onMessageListUnfillRequest }
opacity={ this.props.opacity }
isTwelveHour={ this.state.isTwelveHour }
alwaysShowTimestamps={ this.state.alwaysShowTimestamps }
className={ this.props.className }
tileShape={ this.props.tileShape }
hidden={this.props.hidden}
backPaginating={this.state.backPaginating}
forwardPaginating={forwardPaginating}
events={this.state.events}
highlightedEventId={this.props.highlightedEventId}
readMarkerEventId={this.state.readMarkerEventId}
readMarkerVisible={this.state.readMarkerVisible}
suppressFirstDateSeparator={this.state.canBackPaginate}
showUrlPreview={this.props.showUrlPreview}
showReadReceipts={this.props.showReadReceipts}
ourUserId={MatrixClientPeg.get().credentials.userId}
stickyBottom={stickyBottom}
onScroll={this.onMessageListScroll}
onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={this.onMessageListUnfillRequest}
opacity={this.props.opacity}
isTwelveHour={this.state.isTwelveHour}
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
className={this.props.className}
tileShape={this.props.tileShape}
/>
);
},

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var ContentMessages = require('../../ContentMessages');
var dis = require('../../dispatcher');
var filesize = require('filesize');
const React = require('react');
const ContentMessages = require('../../ContentMessages');
const dis = require('../../dispatcher');
const filesize = require('filesize');
import { _t } from '../../languageHandler';
module.exports = React.createClass({displayName: 'UploadBar',
propTypes: {
room: React.PropTypes.object
room: React.PropTypes.object,
},
componentDidMount: function() {
@ -46,7 +46,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
},
render: function() {
var uploads = ContentMessages.getCurrentUploads();
const uploads = ContentMessages.getCurrentUploads();
// for testing UI... - also fix up the ContentMessages.getCurrentUploads().length
// check in RoomView
@ -62,8 +62,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
return <div />;
}
var upload;
for (var i = 0; i < uploads.length; ++i) {
let upload;
for (let i = 0; i < uploads.length; ++i) {
if (uploads[i].roomId == this.props.room.roomId) {
upload = uploads[i];
break;
@ -73,32 +73,32 @@ module.exports = React.createClass({displayName: 'UploadBar',
return <div />;
}
var innerProgressStyle = {
width: ((upload.loaded / (upload.total || 1)) * 100) + '%'
const innerProgressStyle = {
width: ((upload.loaded / (upload.total || 1)) * 100) + '%',
};
var uploadedSize = filesize(upload.loaded);
var totalSize = filesize(upload.total);
let uploadedSize = filesize(upload.loaded);
const totalSize = filesize(upload.total);
if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) {
uploadedSize = uploadedSize.replace(/ .*/, '');
}
// 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 (
<div className="mx_UploadBar">
<div className="mx_UploadBar_uploadProgressOuter">
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></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"
onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
/>
<div className="mx_UploadBar_uploadBytes">
{ uploadedSize } / { totalSize }
</div>
<div className="mx_UploadBar_uploadFilename">{uploadText}</div>
<div className="mx_UploadBar_uploadFilename">{ uploadText }</div>
</div>
);
}
},
});

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
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.
@ -32,7 +33,7 @@ const AddThreepid = require('../../AddThreepid');
const SdkConfig = require('../../SdkConfig');
import Analytics from '../../Analytics';
import AccessibleButton from '../views/elements/AccessibleButton';
import { _t } from '../../languageHandler';
import { _t, _td } from '../../languageHandler';
import * as languageHandler from '../../languageHandler';
import * as FormattingUtils from '../../utils/FormattingUtils';
@ -52,7 +53,7 @@ const gHVersionLabel = function(repo, token='') {
} else {
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).
@ -63,51 +64,59 @@ const gHVersionLabel = function(repo, token='') {
const SETTINGS_LABELS = [
{
id: 'autoplayGifsAndVideos',
label: 'Autoplay GIFs and videos',
label: _td('Autoplay GIFs and videos'),
},
{
id: 'hideReadReceipts',
label: 'Hide read receipts',
label: _td('Hide read receipts'),
},
{
id: 'dontSendTypingNotifications',
label: "Don't send typing notifications",
label: _td("Don't send typing notifications"),
},
{
id: 'alwaysShowTimestamps',
label: 'Always show message timestamps',
label: _td('Always show message timestamps'),
},
{
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',
label: 'Hide join/leave messages (invites/kicks/bans unaffected)',
label: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
},
{
id: 'hideAvatarDisplaynameChanges',
label: 'Hide avatar and display name changes',
label: _td('Hide avatar and display name changes'),
},
{
id: 'useCompactLayout',
label: 'Use compact timeline layout',
label: _td('Use compact timeline layout'),
},
{
id: 'hideRedactions',
label: 'Hide removed messages',
label: _td('Hide removed messages'),
},
{
id: 'enableSyntaxHighlightLanguageDetection',
label: 'Enable automatic language detection for syntax highlighting',
label: _td('Enable automatic language detection for syntax highlighting'),
},
{
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',
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 = [
{
id: 'analyticsOptOut',
label: 'Opt out of analytics',
label: _td('Opt out of analytics'),
fn: function(checked) {
Analytics[checked ? 'disable' : 'enable']();
},
@ -130,7 +139,7 @@ const ANALYTICS_SETTINGS_LABELS = [
const WEBRTC_SETTINGS_LABELS = [
{
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 = [
{
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) {
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
},
@ -162,16 +171,44 @@ const CRYPTO_SETTINGS_LABELS = [
const THEMES = [
{
id: 'theme',
label: 'Light theme',
label: _td('Light theme'),
value: 'light',
},
{
id: 'theme',
label: 'Dark theme',
label: _td('Dark theme'),
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({
displayName: 'UserSettings',
@ -180,9 +217,6 @@ module.exports = React.createClass({
// The brand string given when creating email pushers
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.
referralBaseUrl: React.PropTypes.string,
@ -194,7 +228,6 @@ module.exports = React.createClass({
getDefaultProps: function() {
return {
onClose: function() {},
enableLabs: true,
};
},
@ -207,6 +240,7 @@ module.exports = React.createClass({
vectorVersion: undefined,
rejectingInvites: false,
mediaDevices: null,
ignoredUsers: [],
};
},
@ -228,6 +262,7 @@ module.exports = React.createClass({
}
this._refreshMediaDevices();
this._refreshIgnoredUsers();
// Bulk rejecting invites:
// /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) {
if (payload.action === "notifier_enabled") {
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) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, {
@ -627,7 +680,7 @@ module.exports = React.createClass({
<div>
<h3>Referral</h3>
<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>
);
@ -646,7 +699,7 @@ module.exports = React.createClass({
_renderLanguageSetting: function() {
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
return <div>
<label htmlFor="languageSelector">{_t('Interface Language')}</label>
<label htmlFor="languageSelector">{ _t('Interface Language') }</label>
<LanguageDropdown ref="language" onOptionChange={this.onLanguageChange}
className="mx_UserSettings_language"
value={this.state.language}
@ -669,7 +722,7 @@ module.exports = React.createClass({
<table>
<tbody>
<tr>
<td><strong>{_t('Autocomplete Delay (ms):')}</strong></td>
<td><strong>{ _t('Autocomplete Delay (ms):') }</strong></td>
<td>
<input
type="number"
@ -690,8 +743,8 @@ module.exports = React.createClass({
return <div className="mx_UserSettings_toggle">
<input id="urlPreviewsDisabled"
type="checkbox"
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
onChange={ this._onPreviewsDisabledChanged }
defaultChecked={UserSettingsStore.getUrlPreviewsDisabled()}
onChange={this._onPreviewsDisabledChanged}
/>
<label htmlFor="urlPreviewsDisabled">
{ _t("Disable inline URL previews by default") }
@ -712,13 +765,13 @@ module.exports = React.createClass({
if (setting.fn) setting.fn(e.target.checked);
};
return <div className="mx_UserSettings_toggle" key={ setting.id }>
<input id={ setting.id }
return <div className="mx_UserSettings_toggle" key={setting.id}>
<input id={setting.id}
type="checkbox"
defaultChecked={ this._syncedSettings[setting.id] }
onChange={ onChange }
defaultChecked={this._syncedSettings[setting.id]}
onChange={onChange}
/>
<label htmlFor={ setting.id }>
<label htmlFor={setting.id}>
{ _t(setting.label) }
</label>
</div>;
@ -729,6 +782,7 @@ module.exports = React.createClass({
// to rebind the onChange each time we render
const onChange = (e) => {
if (e.target.checked) {
this._syncedSettings[setting.id] = setting.value;
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
}
dis.dispatch({
@ -736,16 +790,16 @@ module.exports = React.createClass({
value: setting.value,
});
};
return <div className="mx_UserSettings_toggle" key={ setting.id + "_" + setting.value }>
<input id={ setting.id + "_" + setting.value }
return <div className="mx_UserSettings_toggle" key={setting.id + "_" + setting.value}>
<input id={setting.id + "_" + setting.value}
type="radio"
name={ setting.id }
value={ setting.value }
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
onChange={ onChange }
name={setting.id}
value={setting.value}
checked={this._syncedSettings[setting.id] === setting.value}
onChange={onChange}
/>
<label htmlFor={ setting.id + "_" + setting.value }>
{ setting.label }
<label htmlFor={setting.id + "_" + setting.value}>
{ _t(setting.label) }
</label>
</div>;
},
@ -781,10 +835,10 @@ module.exports = React.createClass({
<h3>{ _t("Cryptography") }</h3>
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
<ul>
<li><label>{_t("Device ID:")}</label>
<span><code>{deviceId}</code></span></li>
<li><label>{_t("Device key:")}</label>
<span><code><b>{identityKey}</b></code></span></li>
<li><label>{ _t("Device ID:") }</label>
<span><code>{ deviceId }</code></span></li>
<li><label>{ _t("Device key:") }</label>
<span><code><b>{ identityKey }</b></code></span></li>
</ul>
{ importExportButtons }
</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) {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
@ -803,13 +877,13 @@ module.exports = React.createClass({
if (setting.fn) setting.fn(e.target.checked);
};
return <div className="mx_UserSettings_toggle" key={ setting.id }>
<input id={ setting.id }
return <div className="mx_UserSettings_toggle" key={setting.id}>
<input id={setting.id}
type="checkbox"
defaultChecked={ this._localSettings[setting.id] }
onChange={ onChange }
defaultChecked={this._localSettings[setting.id]}
onChange={onChange}
/>
<label htmlFor={ setting.id }>
<label htmlFor={setting.id}>
{ _t(setting.label) }
</label>
</div>;
@ -819,8 +893,8 @@ module.exports = React.createClass({
const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
return (
<div>
<h3>{_t("Devices")}</h3>
<DevicesPanel className="mx_UserSettings_section"/>
<h3>{ _t("Devices") }</h3>
<DevicesPanel className="mx_UserSettings_section" />
</div>
);
},
@ -835,7 +909,7 @@ module.exports = React.createClass({
<div className="mx_UserSettings_section">
<p>{ _t("Found a bug?") }</p>
<button className="mx_UserSettings_button danger"
onClick={this._onBugReportClicked}>{_t('Report it')}
onClick={this._onBugReportClicked}>{ _t('Report it') }
</button>
</div>
</div>
@ -843,46 +917,37 @@ module.exports = React.createClass({
},
_renderAnalyticsControl: function() {
if (!SdkConfig.get().piwik) return <div/>;
if (!SdkConfig.get().piwik) return <div />;
return <div>
<h3>{ _t('Analytics') }</h3>
<div className="mx_UserSettings_section">
{_t('Riot collects anonymous analytics to allow us to improve the application.')}
{ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting )}
{ _t('Riot collects anonymous analytics to allow us to improve the application.') }
{ ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting ) }
</div>
</div>;
},
_renderLabs: function() {
// default to enabled if undefined
if (this.props.enableLabs === false) return null;
UserSettingsStore.doTranslations();
const features = [];
UserSettingsStore.LABS_FEATURES.forEach((feature) => {
// This feature has an override and will be set to the default, so do not
// show it here.
if (feature.override) {
return;
}
UserSettingsStore.getLabsFeatures().forEach((featureId) => {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
const onChange = (e) => {
UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked);
UserSettingsStore.setFeatureEnabled(featureId, e.target.checked);
this.forceUpdate();
};
features.push(
<div key={feature.id} className="mx_UserSettings_toggle">
<div key={featureId} className="mx_UserSettings_toggle">
<input
type="checkbox"
id={feature.id}
name={feature.id}
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
onChange={ onChange }
id={featureId}
name={featureId}
defaultChecked={UserSettingsStore.isFeatureEnabled(featureId)}
onChange={onChange}
/>
<label htmlFor={feature.id}>{feature.name}</label>
<label htmlFor={featureId}>{ UserSettingsStore.translatedNameForFeature(featureId) }</label>
</div>);
});
@ -896,7 +961,7 @@ module.exports = React.createClass({
<h3>{ _t("Labs") }</h3>
<div className="mx_UserSettings_section">
<p>{ _t("These are experimental features that may break in unexpected ways") }. { _t("Use with caution") }.</p>
{features}
{ features }
</div>
</div>
);
@ -929,10 +994,10 @@ module.exports = React.createClass({
const platform = PlatformPeg.get();
if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
return <div>
<h3>{_t('Updates')}</h3>
<h3>{ _t('Updates') }</h3>
<div className="mx_UserSettings_section">
<AccessibleButton className="mx_UserSettings_button" onClick={platform.startUpdateCheck}>
{_t('Check for update')}
{ _t('Check for update') }
</AccessibleButton>
</div>
</div>;
@ -958,7 +1023,7 @@ module.exports = React.createClass({
reject = (
<AccessibleButton className="mx_UserSettings_button danger"
onClick={onClick}>
{_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})}
{ _t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length}) }
</AccessibleButton>
);
}
@ -966,7 +1031,7 @@ module.exports = React.createClass({
return <div>
<h3>{ _t("Bulk Options") }</h3>
<div className="mx_UserSettings_section">
{reject}
{ reject }
</div>
</div>;
},
@ -984,7 +1049,7 @@ module.exports = React.createClass({
defaultChecked={settings['auto-launch']}
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>;
@ -996,7 +1061,7 @@ module.exports = React.createClass({
},
_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) {
@ -1032,15 +1097,15 @@ module.exports = React.createClass({
if (this.state.mediaDevices === false) {
return (
<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>
);
} else if (!this.state.mediaDevices) return;
const Dropdown = sdk.getComponent('elements.Dropdown');
let microphoneDropdown = <p>{_t('No Microphones detected')}</p>;
let webcamDropdown = <p>{_t('No Webcams detected')}</p>;
let microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
let webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
const defaultOption = {
deviceId: '',
@ -1057,12 +1122,12 @@ module.exports = React.createClass({
}
microphoneDropdown = <div>
<h4>{_t('Microphone')}</h4>
<h4>{ _t('Microphone') }</h4>
<Dropdown
className="mx_UserSettings_webRtcDevices_dropdown"
value={this.state.activeAudioInput || defaultInput}
onOptionChange={this._setAudioInput}>
{this._mapWebRtcDevicesToSpans(audioInputs)}
{ this._mapWebRtcDevicesToSpans(audioInputs) }
</Dropdown>
</div>;
}
@ -1077,25 +1142,25 @@ module.exports = React.createClass({
}
webcamDropdown = <div>
<h4>{_t('Camera')}</h4>
<h4>{ _t('Camera') }</h4>
<Dropdown
className="mx_UserSettings_webRtcDevices_dropdown"
value={this.state.activeVideoInput || defaultInput}
onOptionChange={this._setVideoInput}>
{this._mapWebRtcDevicesToSpans(videoInputs)}
{ this._mapWebRtcDevicesToSpans(videoInputs) }
</Dropdown>
</div>;
}
return <div>
{microphoneDropdown}
{webcamDropdown}
{ microphoneDropdown }
{ webcamDropdown }
</div>;
},
_renderWebRtcSettings: function() {
return <div>
<h3>{_t('VoIP')}</h3>
<h3>{ _t('VoIP') }</h3>
<div className="mx_UserSettings_section">
{ WEBRTC_SETTINGS_LABELS.map(this._renderLocalSetting) }
{ this._renderWebRtcDeviceSettings() }
@ -1161,7 +1226,7 @@ module.exports = React.createClass({
return (
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
<div className="mx_UserSettings_profileLabelCell">
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
<label htmlFor={id}>{ this.nameForMedium(val.medium) }</label>
</div>
<div className="mx_UserSettings_profileInputCell">
<input type="text" key={val.address} id={id}
@ -1169,7 +1234,7 @@ module.exports = React.createClass({
/>
</div>
<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} />
</div>
</div>
@ -1182,16 +1247,16 @@ module.exports = React.createClass({
addEmailSection = (
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
<div className="mx_UserSettings_profileLabelCell">
<label>{_t('Email')}</label>
<label>{ _t('Email') }</label>
</div>
<div className="mx_UserSettings_profileInputCell">
<EditableText
ref="add_email_input"
className="mx_UserSettings_editable"
placeholderClassName="mx_UserSettings_threepidPlaceholder"
placeholder={ _t("Add email address") }
blurToCancel={ false }
onValueChanged={ this._onAddEmailEditFinished } />
placeholder={_t("Add email address")}
blurToCancel={false}
onValueChanged={this._onAddEmailEditFinished} />
</div>
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
<img src="img/plus.svg" width="14" height="14" alt={_t("Add")} onClick={this._addEmail} />
@ -1239,8 +1304,8 @@ module.exports = React.createClass({
return (
<div className="mx_UserSettings">
<SimpleRoomHeader
title={ _t("Settings") }
onCancelClick={ this.props.onClose }
title={_t("Settings")}
onCancelClick={this.props.onClose}
/>
<GeminiScrollbar className="mx_UserSettings_body"
@ -1258,21 +1323,25 @@ module.exports = React.createClass({
<ChangeDisplayName />
</div>
</div>
{threepidsSection}
{ threepidsSection }
</div>
<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}
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
showUploadSection={false} className="mx_UserSettings_avatarPicker_img" />
</div>
<div className="mx_UserSettings_avatarPicker_edit">
<label htmlFor="avatarInput" ref="file_label">
<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" />
</label>
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
</div>
</div>
</div>
@ -1289,36 +1358,37 @@ module.exports = React.createClass({
</div> : null
}
{accountJsx}
{ accountJsx }
</div>
{this._renderReferral()}
{ this._renderReferral() }
{notificationArea}
{ notificationArea }
{this._renderUserInterfaceSettings()}
{this._renderLabs()}
{this._renderWebRtcSettings()}
{this._renderDevicesPanel()}
{this._renderCryptoInfo()}
{this._renderBulkOptions()}
{this._renderBugReport()}
{ this._renderUserInterfaceSettings() }
{ this._renderLabs() }
{ this._renderWebRtcSettings() }
{ this._renderDevicesPanel() }
{ this._renderCryptoInfo() }
{ this._renderIgnoredUsers() }
{ this._renderBulkOptions() }
{ this._renderBugReport() }
{PlatformPeg.get().isElectron() && this._renderElectronSettings()}
{ PlatformPeg.get().isElectron() && this._renderElectronSettings() }
{this._renderAnalyticsControl()}
{ this._renderAnalyticsControl() }
<h3>{ _t("Advanced") }</h3>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_advanced">
{ _t("Logged in as:") } {this._me}
{ _t("Logged in as:") } { this._me }
</div>
<div className="mx_UserSettings_advanced">
{_t('Access Token:')}
{ _t('Access Token:') }
<span className="mx_UserSettings_advanced_spoiler"
onClick={this._showSpoiler}
data-spoiler={ MatrixClientPeg.get().getAccessToken() }>
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
&lt;{ _t("click to reveal") }&gt;
</span>
</div>
@ -1329,23 +1399,23 @@ module.exports = React.createClass({
{ _t("Identity Server is") } { MatrixClientPeg.get().getIdentityServerUrl() }
</div>
<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)
: REACT_SDK_VERSION
}<br/>
{_t('riot-web version:')} {(this.state.vectorVersion !== undefined)
}<br />
{ _t('riot-web version:') } { (this.state.vectorVersion !== undefined)
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
: 'unknown'
}<br/>
{ _t("olm version:") } {olmVersionString}<br/>
}<br />
{ _t("olm version:") } { olmVersionString }<br />
</div>
</div>
{this._renderCheckUpdate()}
{ this._renderCheckUpdate() }
{this._renderClearCache()}
{ this._renderClearCache() }
{this._renderDeactivateAccount()}
{ this._renderDeactivateAccount() }
</GeminiScrollbar>
</div>

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
@ -16,13 +17,13 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
import { _t } from '../../../languageHandler';
var sdk = require('../../../index');
var Modal = require("../../../Modal");
var MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require('../../../index');
const Modal = require("../../../Modal");
const MatrixClientPeg = require('../../../MatrixClientPeg');
var PasswordReset = require("../../../PasswordReset");
const PasswordReset = require("../../../PasswordReset");
module.exports = React.createClass({
displayName: 'ForgotPassword',
@ -34,30 +35,30 @@ module.exports = React.createClass({
customIsUrl: React.PropTypes.string,
onLoginClick: React.PropTypes.func,
onRegisterClick: React.PropTypes.func,
onComplete: React.PropTypes.func.isRequired
onComplete: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
progress: null
progress: null,
};
},
submitPasswordReset: function(hsUrl, identityUrl, email, password) {
this.setState({
progress: "sending_email"
progress: "sending_email",
});
this.reset = new PasswordReset(hsUrl, identityUrl);
this.reset.resetPassword(email, password).done(() => {
this.setState({
progress: "sent_email"
progress: "sent_email",
});
}, (err) => {
this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
this.setState({
progress: null
progress: null,
});
});
},
@ -80,15 +81,12 @@ module.exports = React.createClass({
if (!this.state.email) {
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.'));
}
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.'));
}
else {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
} else {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
title: _t('Warning!'),
description:
@ -98,7 +96,7 @@ module.exports = React.createClass({
'end-to-end encryption keys on all devices, ' +
'making encrypted chat history unreadable, ' +
'unless you first export your room keys and re-import ' +
'them afterwards. In future this will be improved.'
'them afterwards. In future this will be improved.',
) }
</div>,
button: _t('Continue'),
@ -106,13 +104,13 @@ module.exports = React.createClass({
<button className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}>
{ _t('Export E2E room keys') }
</button>
</button>,
],
onFinished: (confirmed) => {
if (confirmed) {
this.submitPasswordReset(
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) {
this.setState({
[stateKey]: ev.target.value
[stateKey]: ev.target.value,
});
},
onHsUrlChanged: function(newHsUrl) {
this.setState({
enteredHomeserverUrl: newHsUrl
});
},
onIsUrlChanged: function(newIsUrl) {
this.setState({
enteredIdentityServerUrl: newIsUrl
});
onServerConfigChange: function(config) {
const newState = {};
if (config.hsUrl !== undefined) {
newState.enteredHomeserverUrl = config.hsUrl;
}
if (config.isUrl !== undefined) {
newState.enteredIdentityServerUrl = config.isUrl;
}
this.setState(newState);
},
showErrorDialog: function(body, title) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
title: title,
description: body,
@ -157,37 +154,34 @@ module.exports = React.createClass({
},
render: function() {
var LoginHeader = sdk.getComponent("login.LoginHeader");
var LoginFooter = sdk.getComponent("login.LoginFooter");
var ServerConfig = sdk.getComponent("login.ServerConfig");
var Spinner = sdk.getComponent("elements.Spinner");
const LoginHeader = sdk.getComponent("login.LoginHeader");
const LoginFooter = sdk.getComponent("login.LoginFooter");
const ServerConfig = sdk.getComponent("login.ServerConfig");
const Spinner = sdk.getComponent("elements.Spinner");
var resetPasswordJsx;
let resetPasswordJsx;
if (this.state.progress === "sending_email") {
resetPasswordJsx = <Spinner />;
}
else if (this.state.progress === "sent_email") {
} else if (this.state.progress === "sent_email") {
resetPasswordJsx = (
<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 />
<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>
);
}
else if (this.state.progress === "complete") {
} else if (this.state.progress === "complete") {
resetPasswordJsx = (
<div>
<p>{ _t('Your password has been reset') }.</p>
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
value={ _t('Return to login screen') } />
value={_t('Return to login screen')} />
</div>
);
}
else {
} else {
resetPasswordJsx = (
<div>
<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
value={this.state.email}
onChange={this.onInputChanged.bind(this, "email")}
placeholder={ _t('Email address') } autoFocus />
placeholder={_t('Email address')} autoFocus />
<br />
<input className="mx_Login_field" ref="pass" type="password"
name="reset_password"
value={this.state.password}
onChange={this.onInputChanged.bind(this, "password")}
placeholder={ _t('New password') } />
placeholder={_t('New password')} />
<br />
<input className="mx_Login_field" ref="pass" type="password"
name="reset_password_confirm"
value={this.state.password2}
onChange={this.onInputChanged.bind(this, "password2")}
placeholder={ _t('Confirm your new password') } />
placeholder={_t('Confirm your new password')} />
<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>
<ServerConfig ref="serverConfig"
withToggleButton={true}
@ -221,13 +215,12 @@ module.exports = React.createClass({
defaultIsUrl={this.props.defaultIsUrl}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
onHsUrlChanged={this.onHsUrlChanged}
onIsUrlChanged={this.onIsUrlChanged}
delayTimeMs={0}/>
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={0} />
<div className="mx_Login_error">
</div>
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
{_t('Return to login screen')}
{ _t('Return to login screen') }
</a>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
{ _t('Create an account') }
@ -243,9 +236,9 @@ module.exports = React.createClass({
<div className="mx_Login">
<div className="mx_Login_box">
<LoginHeader />
{resetPasswordJsx}
{ resetPasswordJsx }
</div>
</div>
);
}
},
});

View file

@ -134,7 +134,7 @@ module.exports = React.createClass({
},
_onLoginAsGuestClick: function() {
var self = this;
const self = this;
self.setState({
busy: true,
errorText: null,
@ -156,7 +156,7 @@ module.exports = React.createClass({
});
}).finally(function() {
self.setState({
busy: false
busy: false,
});
}).done();
},
@ -183,8 +183,8 @@ module.exports = React.createClass({
},
onServerConfigChange: function(config) {
var self = this;
let newState = {
const self = this;
const newState = {
errorText: null, // reset err messages
};
if (config.hsUrl !== undefined) {
@ -199,13 +199,13 @@ module.exports = React.createClass({
},
_initLoginLogic: function(hsUrl, isUrl) {
var self = this;
const self = this;
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
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,
});
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. " +
"Either use HTTPS or <a>enable unsafe scripts</a>.",
/<a>(.*?)<\/a>/,
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; }
)}
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; },
) }
</span>;
} else {
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.",
/<a>(.*?)<\/a>/,
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; }
)}
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; },
) }
</span>;
}
}
@ -290,6 +290,7 @@ module.exports = React.createClass({
onPhoneNumberChanged={this.onPhoneNumberChanged}
onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect}
hsUrl={this.state.enteredHomeserverUrl}
/>
);
case 'm.login.cas':
@ -303,7 +304,7 @@ module.exports = React.createClass({
}
return (
<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>
);
}
@ -333,19 +334,19 @@ module.exports = React.createClass({
const ServerConfig = sdk.getComponent("login.ServerConfig");
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
var loginAsGuestJsx;
let loginAsGuestJsx;
if (this.props.enableGuest) {
loginAsGuestJsx =
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
{ _t('Login as guest')}
{ _t('Login as guest') }
</a>;
}
var returnToAppJsx;
let returnToAppJsx;
if (this.props.onCancelClick) {
returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
{ _t('Return to app')}
{ _t('Return to app') }
</a>;
}
@ -354,7 +355,7 @@ module.exports = React.createClass({
<div className="mx_Login_box">
<LoginHeader />
<div>
<h2>{ _t('Sign in')}
<h2>{ _t('Sign in') }
{ loader }
</h2>
{ this.componentForStep(this.state.currentFlow) }
@ -365,12 +366,12 @@ module.exports = React.createClass({
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}/>
delayTimeMs={1000} />
<div className="mx_Login_error">
{ this.state.errorText }
</div>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
{ _t('Create an account')}
{ _t('Create an account') }
</a>
{ loginAsGuestJsx }
{ returnToAppJsx }
@ -380,5 +381,5 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});

View file

@ -25,14 +25,14 @@ module.exports = React.createClass({
displayName: 'PostRegistration',
propTypes: {
onComplete: React.PropTypes.func.isRequired
onComplete: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
avatarUrl: null,
errorString: null,
busy: false
busy: false,
};
},
@ -40,26 +40,26 @@ module.exports = React.createClass({
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
// 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).
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
this.setState({busy: true});
var self = this;
const self = this;
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
self.setState({
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
busy: false
busy: false,
});
}, function(error) {
self.setState({
errorString: _t("Failed to fetch avatar URL"),
busy: false
busy: false,
});
});
},
render: function() {
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
var LoginHeader = sdk.getComponent('login.LoginHeader');
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
const LoginHeader = sdk.getComponent('login.LoginHeader');
return (
<div className="mx_Login">
<div className="mx_Login_box">
@ -71,10 +71,10 @@ module.exports = React.createClass({
<ChangeAvatar
initialAvatarUrl={this.state.avatarUrl} />
<button onClick={this.props.onComplete}>{ _t('Continue') }</button>
{this.state.errorString}
{ this.state.errorString }
</div>
</div>
</div>
);
}
},
});

View file

@ -57,7 +57,7 @@ module.exports = React.createClass({
// registration shouldn't know or care how login is done.
onLoginClick: React.PropTypes.func.isRequired,
onCancelClick: React.PropTypes.func
onCancelClick: React.PropTypes.func,
},
getInitialState: function() {
@ -121,7 +121,7 @@ module.exports = React.createClass({
},
onServerConfigChange: function(config) {
let newState = {};
const newState = {};
if (config.hsUrl !== undefined) {
newState.hsUrl = config.hsUrl;
}
@ -195,7 +195,7 @@ module.exports = React.createClass({
this._rtsClient.getTeam(teamToken).then((team) => {
console.log(
`User successfully registered with team ${team.name}`
`User successfully registered with team ${team.name}`,
);
if (!team.rooms) {
return;
@ -223,7 +223,7 @@ module.exports = React.createClass({
deviceId: response.device_id,
homeserverUrl: this._matrixClient.getHomeserverUrl(),
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token
accessToken: response.access_token,
}, teamToken);
}).then((cli) => {
return this._setupPushers(cli);
@ -253,7 +253,7 @@ module.exports = React.createClass({
},
onFormValidationFailed: function(errCode) {
var errMsg;
let errMsg;
switch (errCode) {
case "RegistrationForm.ERR_PASSWORD_MISSING":
errMsg = _t('Missing password.');
@ -282,7 +282,7 @@ module.exports = React.createClass({
break;
}
this.setState({
errorText: errMsg
errorText: errMsg,
});
},
@ -316,7 +316,7 @@ module.exports = React.createClass({
emailAddress: this.state.formVals.email,
phoneCountry: this.state.formVals.phoneCountry,
phoneNumber: this.state.formVals.phoneNumber,
}
};
},
render: function() {
@ -346,7 +346,7 @@ module.exports = React.createClass({
} else {
let errorSection;
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 = (
<div>
@ -362,7 +362,7 @@ module.exports = React.createClass({
onRegisterClick={this.onFormSubmit}
onTeamSelected={this.onTeamSelected}
/>
{errorSection}
{ errorSection }
<ServerConfig ref="serverConfig"
withToggleButton={true}
customHsUrl={this.props.customHsUrl}
@ -380,7 +380,7 @@ module.exports = React.createClass({
if (this.props.onCancelClick) {
returnToAppJsx = (
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
{_t('Return to app')}
{ _t('Return to app') }
</a>
);
}
@ -393,15 +393,15 @@ module.exports = React.createClass({
this.state.teamSelected.domain + "/icon.png" :
null}
/>
<h2>{_t('Create an account')}</h2>
{registerBody}
<h2>{ _t('Create an account') }</h2>
{ registerBody }
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
{_t('I already have an account')}
{ _t('I already have an account') }
</a>
{returnToAppJsx}
{ returnToAppJsx }
<LoginFooter />
</div>
</div>
);
}
},
});

View file

@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var React = require('react');
var AvatarLogic = require("../../../Avatar");
import React from 'react';
import AvatarLogic from '../../../Avatar';
import sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton';
@ -34,7 +32,7 @@ module.exports = React.createClass({
height: React.PropTypes.number,
// XXX resizeMethod not actually used.
resizeMethod: React.PropTypes.string,
defaultToInitialLetter: React.PropTypes.bool // true to add default url
defaultToInitialLetter: React.PropTypes.bool, // true to add default url
},
getDefaultProps: function() {
@ -42,7 +40,7 @@ module.exports = React.createClass({
width: 40,
height: 40,
resizeMethod: 'crop',
defaultToInitialLetter: true
defaultToInitialLetter: true,
};
},
@ -52,15 +50,14 @@ module.exports = React.createClass({
componentWillReceiveProps: function(nextProps) {
// work out if we need to call setState (if the image URLs array has changed)
var newState = this._getState(nextProps);
var newImageUrls = newState.imageUrls;
var oldImageUrls = this.state.imageUrls;
const newState = this._getState(nextProps);
const newImageUrls = newState.imageUrls;
const oldImageUrls = this.state.imageUrls;
if (newImageUrls.length !== oldImageUrls.length) {
this.setState(newState); // detected a new entry
}
else {
} else {
// 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]) {
this.setState(newState); // detected a diff
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:
// imageUrls: [ props.url, props.urls, default image ]
var urls = props.urls || [];
const urls = props.urls || [];
if (props.url) {
urls.unshift(props.url); // put in urls[0]
}
var defaultImageUrl = null;
let defaultImageUrl = null;
if (props.defaultToInitialLetter) {
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
props.idName || props.name
props.idName || props.name,
);
urls.push(defaultImageUrl); // lowest priority
}
return {
imageUrls: urls,
defaultImageUrl: defaultImageUrl,
urlsIndex: 0
urlsIndex: 0,
};
},
onError: function(ev) {
var nextIndex = this.state.urlsIndex + 1;
const nextIndex = this.state.urlsIndex + 1;
if (nextIndex < this.state.imageUrls.length) {
// try the next one
this.setState({
urlsIndex: nextIndex
urlsIndex: nextIndex,
});
}
},
@ -111,32 +108,32 @@ module.exports = React.createClass({
return undefined;
}
var idx = 0;
var initial = name[0];
if ((initial === '@' || initial === '#') && name[1]) {
let idx = 0;
const initial = name[0];
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
idx++;
}
// string.codePointAt(0) would do this, but that isn't supported by
// some browsers (notably PhantomJS).
var chars = 1;
var first = name.charCodeAt(idx);
let chars = 1;
const first = name.charCodeAt(idx);
// check if its the start of a surrogate pair
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) {
chars++;
}
}
var firstChar = name.substring(idx, idx+chars);
const firstChar = name.substring(idx, idx+chars);
return firstChar.toUpperCase();
},
render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
var imageUrl = this.state.imageUrls[this.state.urlsIndex];
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
const {
name, idName, title, url, urls, width, height, resizeMethod,
@ -152,7 +149,7 @@ module.exports = React.createClass({
width: width + "px",
lineHeight: height + "px" }}
>
{initialLetter}
{ initialLetter }
</EmojiText>
);
const imgNode = (
@ -165,15 +162,15 @@ module.exports = React.createClass({
<AccessibleButton element='span' className="mx_BaseAvatar"
onClick={onClick} {...otherProps}
>
{textNode}
{imgNode}
{ textNode }
{ imgNode }
</AccessibleButton>
);
} else {
return (
<span className="mx_BaseAvatar" {...otherProps}>
{textNode}
{imgNode}
{ textNode }
{ imgNode }
</span>
);
}
@ -198,5 +195,5 @@ module.exports = React.createClass({
{...otherProps} />
);
}
}
},
});

View file

@ -16,9 +16,9 @@ limitations under the License.
'use strict';
var React = require('react');
var Avatar = require('../../../Avatar');
var sdk = require("../../../index");
const React = require('react');
const Avatar = require('../../../Avatar');
const sdk = require("../../../index");
const dispatcher = require("../../../dispatcher");
module.exports = React.createClass({
@ -63,14 +63,14 @@ module.exports = React.createClass({
imageUrl: Avatar.avatarUrlForMember(props.member,
props.width,
props.height,
props.resizeMethod)
props.resizeMethod),
};
},
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) {
onClick = () => {
@ -83,7 +83,7 @@ module.exports = React.createClass({
return (
<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
limitations under the License.
*/
var React = require('react');
var ContentRepo = require("matrix-js-sdk").ContentRepo;
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Avatar = require('../../../Avatar');
var sdk = require("../../../index");
import React from "react";
import {ContentRepo} from "matrix-js-sdk";
import MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from "../../../index";
module.exports = React.createClass({
displayName: 'RoomAvatar',
@ -30,7 +29,7 @@ module.exports = React.createClass({
oobData: React.PropTypes.object,
width: React.PropTypes.number,
height: React.PropTypes.number,
resizeMethod: React.PropTypes.string
resizeMethod: React.PropTypes.string,
},
getDefaultProps: function() {
@ -44,13 +43,13 @@ module.exports = React.createClass({
getInitialState: function() {
return {
urls: this.getImageUrls(this.props)
urls: this.getImageUrls(this.props),
};
},
componentWillReceiveProps: function(newProps) {
this.setState({
urls: this.getImageUrls(newProps)
urls: this.getImageUrls(newProps),
});
},
@ -61,11 +60,10 @@ module.exports = React.createClass({
props.oobData.avatarUrl,
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod
props.resizeMethod,
), // highest priority
this.getRoomAvatarUrl(props),
this.getOneToOneAvatar(props),
this.getFallbackAvatar(props) // lowest priority
this.getOneToOneAvatar(props), // lowest priority
].filter(function(url) {
return (url != null && url != "");
});
@ -79,17 +77,17 @@ module.exports = React.createClass({
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false
false,
);
},
getOneToOneAvatar: function(props) {
if (!props.room) return null;
var mlist = props.room.currentState.members;
var userIds = [];
const mlist = props.room.currentState.members;
const userIds = [];
// 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)) {
userIds.push(uid);
}
@ -99,7 +97,7 @@ module.exports = React.createClass({
}
if (userIds.length == 2) {
var theOtherGuy = null;
let theOtherGuy = null;
if (mlist[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
theOtherGuy = mlist[userIds[1]];
} else {
@ -110,7 +108,7 @@ module.exports = React.createClass({
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false
false,
);
} else if (userIds.length == 1) {
return mlist[userIds[0]].getAvatarUrl(
@ -118,37 +116,24 @@ module.exports = React.createClass({
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false
false,
);
} else {
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() {
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 (
<BaseAvatar {...otherProps} name={roomName}
idName={room ? room.roomId : null}
urls={this.state.urls} />
);
}
},
});

View file

@ -36,7 +36,7 @@ module.exports = React.createClass({
render: function() {
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';
var React = require('react');
const React = require('react');
import { _t } from '../../../languageHandler';
var Presets = {
const Presets = {
PrivateChat: "private_chat",
PublicChat: "public_chat",
Custom: "custom",
@ -29,7 +29,7 @@ module.exports = React.createClass({
displayName: 'CreateRoomPresets',
propTypes: {
onChange: React.PropTypes.func,
preset: React.PropTypes.string
preset: React.PropTypes.string,
},
Presets: Presets,
@ -47,10 +47,10 @@ module.exports = React.createClass({
render: function() {
return (
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
<option value={this.Presets.PrivateChat}>{_t("Private Chat")}</option>
<option value={this.Presets.PublicChat}>{_t("Public Chat")}</option>
<option value={this.Presets.Custom}>{_t("Custom")}</option>
<option value={this.Presets.PrivateChat}>{ _t("Private Chat") }</option>
<option value={this.Presets.PublicChat}>{ _t("Public Chat") }</option>
<option value={this.Presets.Custom}>{ _t("Custom") }</option>
</select>
);
}
},
});

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
const React = require('react');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
@ -35,10 +35,10 @@ module.exports = React.createClass({
},
getAliasLocalpart: function() {
var room_alias = this.props.alias;
let room_alias = this.props.alias;
if (room_alias && this.props.homeserver) {
var suffix = ":" + this.props.homeserver;
const suffix = ":" + this.props.homeserver;
if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) {
room_alias = room_alias.slice(1, -suffix.length);
}
@ -52,22 +52,22 @@ module.exports = React.createClass({
},
onFocus: function(ev) {
var target = ev.target;
var curr_val = ev.target.value;
const target = ev.target;
const curr_val = ev.target.value;
if (this.props.homeserver) {
if (curr_val == "") {
var self = this;
const self = this;
setTimeout(function() {
target.value = "#:" + self.props.homeserver;
target.setSelectionRange(1, 1);
}, 0);
} else {
var suffix = ":" + this.props.homeserver;
const suffix = ":" + this.props.homeserver;
setTimeout(function() {
target.setSelectionRange(
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);
}
@ -75,7 +75,7 @@ module.exports = React.createClass({
},
onBlur: function(ev) {
var curr_val = ev.target.value;
const curr_val = ev.target.value;
if (this.props.homeserver) {
if (curr_val == "#:" + this.props.homeserver) {
@ -84,8 +84,8 @@ module.exports = React.createClass({
}
if (curr_val != "") {
var new_val = ev.target.value;
var suffix = ":" + this.props.homeserver;
let new_val = ev.target.value;
const suffix = ":" + this.props.homeserver;
if (!curr_val.startsWith("#")) new_val = "#" + new_val;
if (!curr_val.endsWith(suffix)) new_val = new_val + suffix;
ev.target.value = new_val;
@ -97,7 +97,7 @@ module.exports = React.createClass({
return (
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
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 Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStoreCache from '../../../stores/GroupStoreCache';
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
module.exports = React.createClass({
displayName: "UserPickerDialog",
displayName: "AddressPickerDialog",
propTypes: {
title: PropTypes.string.isRequired,
@ -40,6 +41,12 @@ module.exports = React.createClass({
focus: PropTypes.bool,
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)),
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() {
@ -47,6 +54,8 @@ module.exports = React.createClass({
value: "",
focus: true,
validAddressTypes: addressTypes,
pickerType: 'user',
includeSelf: false,
};
},
@ -140,11 +149,23 @@ module.exports = React.createClass({
// Only do search if there is something to search
if (query.length > 0 && query != '@' && query.length >= 2) {
this.queryChangedDebouncer = setTimeout(() => {
if (this.state.serverSupportsUserDirectory) {
if (this.props.pickerType === 'user') {
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 {
console.error('Unknown pickerType', this.props.pickerType);
}
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
} else {
this.setState({
@ -185,6 +206,94 @@ module.exports = React.createClass({
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) {
this.setState({
busy: true,
@ -245,17 +354,30 @@ module.exports = React.createClass({
_processResults: function(results, query) {
const queryList = [];
results.forEach((user) => {
if (user.user_id === MatrixClientPeg.get().credentials.userId) {
results.forEach((result) => {
if (result.room_id) {
queryList.push({
addressType: 'mx-room-id',
address: result.room_id,
displayName: result.name,
avatarMxc: result.avatar_url,
isKnown: true,
});
return;
}
if (!this.props.includeSelf &&
result.user_id === MatrixClientPeg.get().credentials.userId
) {
return;
}
// Return objects, structure of which is defined
// by UserAddressType
queryList.push({
addressType: 'mx',
address: user.user_id,
displayName: user.display_name,
avatarMxc: user.avatar_url,
addressType: 'mx-user-id',
address: result.user_id,
displayName: result.display_name,
avatarMxc: result.avatar_url,
isKnown: true,
});
});
@ -291,16 +413,23 @@ module.exports = React.createClass({
address: addressText,
isKnown: false,
};
if (addrType == null) {
if (!this.props.validAddressTypes.includes(addrType)) {
this.setState({ error: true });
return null;
} else if (addrType == 'mx') {
} else if (addrType == 'mx-user-id') {
const user = MatrixClientPeg.get().getUser(addrObj.address);
if (user) {
addrObj.displayName = user.displayName;
addrObj.avatarMxc = user.avatarUrl;
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();
@ -360,7 +489,12 @@ module.exports = React.createClass({
const AddressTile = sdk.getComponent("elements.AddressTile");
for (let i = 0; i < this.state.userList.length; i++) {
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 addressSelector;
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">
{_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>;
} 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 (
this.state.query.length > 0 &&
this.state.queryList.length === 0 &&
!this.state.busy
) {
error = <div className="mx_ChatInviteDialog_error">{_t("No results")}</div>;
error = <div className="mx_ChatInviteDialog_error">{ _t("No results") }</div>;
} else {
addressSelector = (
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
addressList={ this.state.queryList }
onSelected={ this.onSelected }
truncateAt={ TRUNCATE_QUERY_LIST }
addressList={this.state.queryList}
showAddress={this.props.pickerType === 'user'}
onSelected={this.onSelected}
truncateAt={TRUNCATE_QUERY_LIST}
/>
);
}
@ -406,7 +554,7 @@ module.exports = React.createClass({
return (
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
<div className="mx_Dialog_title">
{this.props.title}
{ this.props.title }
</div>
<AccessibleButton className="mx_ChatInviteDialog_cancel"
onClick={this.onCancel} >
@ -422,7 +570,7 @@ module.exports = React.createClass({
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
{this.props.button}
{ this.props.button }
</button>
</div>
</div>

View file

@ -155,7 +155,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
width={48} height={48}
/>
<div className="mx_ChatCreateOrReuseDialog_profile_name">
{this.state.profile.displayName || this.props.userId}
{ this.state.profile.displayName || this.props.userId }
</div>
</div>;
}
@ -177,7 +177,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className='mx_ChatCreateOrReuseDialog'
onFinished={ this.props.onFinished.bind(false) }
onFinished={this.props.onFinished.bind(false)}
title={title}
>
{ content }

View file

@ -52,20 +52,20 @@ export default React.createClass({
return (
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
onEnterPressed={ this.onOk }
onEnterPressed={this.onOk}
title={title}
>
<div className="mx_Dialog_content">
{_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.")}
{ _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.") }
</div>
<div className="mx_Dialog_buttons">
<button className={confirmButtonClass} onClick={this.onOk}>
{_t("Remove")}
{ _t("Remove") }
</button>
<button onClick={this.onCancel}>
{_t("Cancel")}
{ _t("Cancel") }
</button>
</div>
</BaseDialog>

View file

@ -18,6 +18,7 @@ import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classnames from 'classnames';
import { GroupMemberType } from '../../../groups';
/*
* A dialog for confirming an operation on another user.
@ -30,7 +31,10 @@ import classnames from 'classnames';
export default React.createClass({
displayName: 'ConfirmUserActionDialog',
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'
// Whether to display a text field for a reason
@ -69,6 +73,7 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
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 confirmButtonClass = classnames({
@ -83,7 +88,7 @@ export default React.createClass({
<form onSubmit={this.onOk}>
<input className="mx_ConfirmUserActionDialog_reasonField"
ref={this._collectReasonField}
placeholder={ _t("Reason") }
placeholder={_t("Reason")}
autoFocus={true}
/>
</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 (
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
onEnterPressed={ this.onOk }
onEnterPressed={this.onOk}
title={title}
>
<div className="mx_Dialog_content">
<div className="mx_ConfirmUserActionDialog_avatar">
<MemberAvatar member={this.props.member} width={48} height={48} />
{ avatar }
</div>
<div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div>
<div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div>
<div className="mx_ConfirmUserActionDialog_name">{ name }</div>
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
</div>
{reasonBox}
{ reasonBox }
<div className="mx_Dialog_buttons">
<button className={confirmButtonClass}
onClick={this.onOk} autoFocus={!this.props.askReason}
>
{this.props.action}
{ this.props.action }
</button>
<button onClick={this.onCancel}>

View file

@ -62,15 +62,15 @@ export default React.createClass({
let error = null;
if (parsedGroupId === null) {
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()},
);
} else {
const domain = parsedGroupId[1];
if (domain !== MatrixClientPeg.get().getDomain()) {
error = _t(
"It is currently only possible to create groups on your own home server: "+
"use a group ID ending with %(domain)s",
"It is currently only possible to create communities on your own home server: "+
"use a community ID ending with %(domain)s",
{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
// any yet.
createErrorNode = <div className="error">
<div>{_t('Room creation failed')}</div>
<div>{this.state.createError.message}</div>
<div>{ _t('Room creation failed') }</div>
<div>{ this.state.createError.message }</div>
</div>;
}
return (
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
onEnterPressed={this._onFormSubmit}
title={_t('Create Group')}
title={_t('Create Community')}
>
<form onSubmit={this._onFormSubmit}>
<div className="mx_Dialog_content">
<div className="mx_CreateGroupDialog_inputRow">
<div className="mx_CreateGroupDialog_label">
<label htmlFor="groupname">{_t('Group Name')}</label>
<label htmlFor="groupname">{ _t('Community Name') }</label>
</div>
<div>
<input id="groupname" className="mx_CreateGroupDialog_input"
@ -169,7 +169,7 @@ export default React.createClass({
</div>
<div className="mx_CreateGroupDialog_inputRow">
<div className="mx_CreateGroupDialog_label">
<label htmlFor="groupid">{_t('Group ID')}</label>
<label htmlFor="groupid">{ _t('Community ID') }</label>
</div>
<div>
<input id="groupid" className="mx_CreateGroupDialog_input"
@ -182,9 +182,9 @@ export default React.createClass({
</div>
</div>
<div className="error">
{this.state.groupIdError}
{ this.state.groupIdError }
</div>
{createErrorNode}
{ createErrorNode }
</div>
<div className="mx_Dialog_buttons">
<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;
if (this.state.errStr) {
error = <div className="error">
{this.state.errStr}
{ this.state.errStr }
</div>;
passwordBoxClass = 'error';
}
@ -94,30 +94,30 @@ export default class DeactivateAccountDialog extends React.Component {
let cancelButton = null;
if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
{_t("Cancel")}
{ _t("Cancel") }
</button>;
}
return (
<div className="mx_DeactivateAccountDialog">
<div className="mx_Dialog_title danger">
{_t("Deactivate Account")}
{ _t("Deactivate Account") }
</div>
<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
type="password"
onChange={this._onPasswordFieldChange}
ref={(e) => {this._passwordField = e;}}
className={passwordBoxClass}
/>
{error}
{ error }
</div>
<div className="mx_Dialog_buttons">
<button
@ -125,10 +125,10 @@ export default class DeactivateAccountDialog extends React.Component {
onClick={this._onOk}
disabled={!okEnabled}
>
{okLabel}
{ okLabel }
</button>
{cancelButton}
{ cancelButton }
</div>
</div>
);

View file

@ -28,25 +28,25 @@ export default function DeviceVerifyDialog(props) {
const body = (
<div>
<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) " +
"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>
<div className="mx_UserSettings_cryptoSection">
<ul>
<li><label>{_t("Device name")}:</label> <span>{ props.device.getDisplayName() }</span></li>
<li><label>{_t("Device ID")}:</label> <span><code>{ props.device.deviceId}</code></span></li>
<li><label>{_t("Device key")}:</label> <span><code><b>{ key }</b></code></span></li>
<li><label>{ _t("Device name") }:</label> <span>{ props.device.getDisplayName() }</span></li>
<li><label>{ _t("Device ID") }:</label> <span><code>{ props.device.deviceId }</code></span></li>
<li><label>{ _t("Device key") }:</label> <span><code><b>{ key }</b></code></span></li>
</ul>
</div>
<p>
{_t("If it matches, press the verify button below. " +
{ _t("If it matches, press the verify button below. " +
"If it doesn't, then someone else is intercepting this device " +
"and you probably want to press the blacklist button instead.")}
"and you probably want to press the blacklist button instead.") }
</p>
<p>
{_t("In future this verification process will be more sophisticated.")}
{ _t("In future this verification process will be more sophisticated.") }
</p>
</div>
);

View file

@ -63,11 +63,11 @@ export default React.createClass({
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={this.props.title || _t('Error')}>
<div className="mx_Dialog_content">
{this.props.description || _t('An error has occurred.')}
{ this.props.description || _t('An error has occurred.') }
</div>
<div className="mx_Dialog_buttons">
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
{this.props.button || _t('OK')}
{ this.props.button || _t('OK') }
</button>
</div>
</BaseDialog>

View file

@ -48,7 +48,7 @@ export default React.createClass({
getInitialState: function() {
return {
authError: null,
}
};
},
_onAuthFinished: function(success, result) {
@ -73,12 +73,12 @@ export default React.createClass({
if (this.state.authError) {
content = (
<div>
<div>{this.state.authError.message || this.state.authError.toString()}</div>
<div>{ this.state.authError.message || this.state.authError.toString() }</div>
<br />
<AccessibleButton onClick={this._onDismissClick}
className="mx_UserSettings_button"
>
{_t("Dismiss")}
{ _t("Dismiss") }
</AccessibleButton>
</div>
);
@ -100,7 +100,7 @@ export default React.createClass({
onFinished={this.props.onFinished}
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
>
{content}
{ content }
</BaseDialog>
);
},

View file

@ -18,7 +18,7 @@ import Modal from '../../../Modal';
import React from 'react';
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
@ -116,27 +116,27 @@ export default React.createClass({
let text;
if (this.state.wasNewDevice) {
text = "You added a new device '%(displayName)s', which is"
+ " requesting encryption keys.";
text = _td("You added a new device '%(displayName)s', which is"
+ " requesting encryption keys.");
} else {
text = "Your unverified device '%(displayName)s' is requesting"
+ " encryption keys.";
text = _td("Your unverified device '%(displayName)s' is requesting"
+ " encryption keys.");
}
text = _t(text, {displayName: displayName});
return (
<div>
<p>{text}</p>
<p>{ text }</p>
<div className="mx_Dialog_buttons">
<button onClick={this._onVerifyClicked}>
{_t('Start verification')}
{ _t('Start verification') }
</button>
<button onClick={this._onShareClicked}>
{_t('Share without verifying')}
{ _t('Share without verifying') }
</button>
<button onClick={this._onIgnoreClicked}>
{_t('Ignore request')}
{ _t('Ignore request') }
</button>
</div>
</div>
@ -154,7 +154,7 @@ export default React.createClass({
} else {
content = (
<div>
<p>{_t('Loading device info...')}</p>
<p>{ _t('Loading device info...') }</p>
<Spinner />
</div>
);
@ -165,7 +165,7 @@ export default React.createClass({
onFinished={this.props.onFinished}
title={_t('Encryption key request')}
>
{content}
{ content }
</BaseDialog>
);
},

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
@ -17,6 +18,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classnames from 'classnames';
export default React.createClass({
displayName: 'QuestionDialog',
@ -25,6 +27,7 @@ export default React.createClass({
description: React.PropTypes.node,
extraButtons: React.PropTypes.node,
button: React.PropTypes.string,
danger: React.PropTypes.bool,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
},
@ -36,6 +39,7 @@ export default React.createClass({
extraButtons: null,
focus: true,
hasCancelButton: true,
danger: false,
};
},
@ -51,23 +55,27 @@ export default React.createClass({
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const cancelButton = this.props.hasCancelButton ? (
<button onClick={this.onCancel}>
{_t("Cancel")}
{ _t("Cancel") }
</button>
) : null;
const buttonClasses = classnames({
mx_Dialog_primary: true,
danger: this.props.danger,
});
return (
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
onEnterPressed={ this.onOk }
onEnterPressed={this.onOk}
title={this.props.title}
>
<div className="mx_Dialog_content">
{this.props.description}
{ this.props.description }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
{this.props.button || _t('OK')}
<button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}>
{ this.props.button || _t('OK') }
</button>
{this.props.extraButtons}
{cancelButton}
{ this.props.extraButtons }
{ cancelButton }
</div>
</BaseDialog>
);

View file

@ -45,10 +45,10 @@ export default React.createClass({
if (SdkConfig.get().bug_report_endpoint_url) {
bugreport = (
<p>
{_tJsx(
{ _tJsx(
"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>
);
}
@ -57,19 +57,19 @@ export default React.createClass({
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={_t('Unable to restore session')}>
<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 " +
"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 " +
"to the more recent version.")}</p>
"to the more recent version.") }</p>
{bugreport}
{ bugreport }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
{_t("Continue anyway")}
{ _t("Continue anyway") }
</button>
</div>
</BaseDialog>

View file

@ -130,10 +130,10 @@ export default React.createClass({
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
className="mx_SetEmailDialog_email_input"
placeholder={ _t("Email address") }
placeholder={_t("Email address")}
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
blurToCancel={ false }
onValueChanged={ this.onEmailAddressChanged } />;
blurToCancel={false}
onValueChanged={this.onEmailAddressChanged} />;
return (
<BaseDialog className="mx_SetEmailDialog"

View file

@ -226,7 +226,7 @@ export default React.createClass({
let usernameIndicator = null;
let usernameBusyIndicator = null;
if (this.state.usernameBusy) {
usernameBusyIndicator = <Spinner w="24" h="24"/>;
usernameBusyIndicator = <Spinner w="24" h="24" />;
} else {
const usernameAvailable = this.state.username &&
this.state.usernameCheckSupport && !this.state.usernameError;
@ -275,17 +275,17 @@ export default React.createClass({
/<a>(.*?)<\/a>/,
],
[
(sub) => <span>{this.props.homeserverUrl}</span>,
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{sub}</a>,
(sub) => <span>{ this.props.homeserverUrl }</span>,
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
],
)}
) }
</p>
<p>
{ _tJsx(
'If you already have a Matrix account you can <a>log in</a> instead.',
/<a>(.*?)<\/a>/,
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{sub}</a>],
)}
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a>],
) }
</p>
{ auth }
{ authErrorIndicator }

View file

@ -65,10 +65,10 @@ export default React.createClass({
>
<div className="mx_Dialog_content">
<div className="mx_TextInputDialog_label">
<label htmlFor="textinput"> {this.props.description} </label>
<label htmlFor="textinput"> { this.props.description } </label>
</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 className="mx_Dialog_buttons">
@ -76,7 +76,7 @@ export default React.createClass({
{ _t("Cancel") }
</button>
<button className="mx_Dialog_primary" onClick={this.onOk}>
{this.props.button}
{ this.props.button }
</button>
</div>
</BaseDialog>

View file

@ -28,9 +28,9 @@ function DeviceListEntry(props) {
return (
<li>
<DeviceVerifyButtons device={ device } userId={ userId } />
<DeviceVerifyButtons device={device} userId={userId} />
{ device.deviceId }
<br/>
<br />
{ device.getDisplayName() }
</li>
);
@ -48,13 +48,13 @@ function UserUnknownDeviceList(props) {
const {userId, userDevices} = props;
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
<DeviceListEntry key={ deviceId } userId={ userId }
device={ userDevices[deviceId] } />,
<DeviceListEntry key={deviceId} userId={userId}
device={userDevices[deviceId]} />,
);
return (
<ul className="mx_UnknownDeviceDialog_deviceList">
{deviceListEntries}
{ deviceListEntries }
</ul>
);
}
@ -71,13 +71,13 @@ function UnknownDeviceList(props) {
const {devices} = props;
const userListEntries = Object.keys(devices).map((userId) =>
<li key={ userId }>
<li key={userId}>
<p>{ userId }:</p>
<UserUnknownDeviceList userId={ userId } userDevices={ devices[userId] } />
<UserUnknownDeviceList userId={userId} userDevices={devices[userId]} />
</li>,
);
return <ul>{userListEntries}</ul>;
return <ul>{ userListEntries }</ul>;
}
UnknownDeviceList.propTypes = {
@ -120,17 +120,17 @@ export default React.createClass({
if (blacklistUnverified) {
warning = (
<h4>
{_t("You are currently blacklisting unverified devices; to send " +
"messages to these devices you must verify them.")}
{ _t("You are currently blacklisting unverified devices; to send " +
"messages to these devices you must verify them.") }
</h4>
);
} else {
warning = (
<div>
<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, " +
"but you can resend the message without verifying if you prefer.")}
"but you can resend the message without verifying if you prefer.") }
</p>
</div>
);
@ -149,22 +149,22 @@ export default React.createClass({
>
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<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>
{ warning }
{_t("Unknown devices")}:
{ _t("Unknown devices") }:
<UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" autoFocus={ true }
<button className="mx_Dialog_primary" autoFocus={true}
onClick={() => {
this.props.onFinished();
Resend.resendUnsentEvents(this.props.room);
}}>
{_t("Send anyway")}
{ _t("Send anyway") }
</button>
<button className="mx_Dialog_primary" autoFocus={ true }
<button className="mx_Dialog_primary" autoFocus={true}
onClick={() => {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148

View file

@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
import AccessibleButton from './AccessibleButton';
import dis from '../../../dispatcher';
import sdk from '../../../index';
import Analytics from '../../../Analytics';
export default React.createClass({
displayName: 'RoleButton',
@ -47,6 +48,7 @@ export default React.createClass({
_onClick: function(ev) {
ev.stopPropagation();
Analytics.trackEvent('Action Button', 'click', this.props.action);
dis.dispatch({action: this.props.action});
},
@ -77,8 +79,8 @@ export default React.createClass({
onMouseLeave={this._onMouseLeave}
>
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
{tooltip}
{ tooltip }
</AccessibleButton>
);
}
},
});

View file

@ -30,6 +30,8 @@ export default React.createClass({
// List of the addresses to display
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
// Whether to show the address on the address tiles
showAddress: React.PropTypes.bool,
truncateAt: React.PropTypes.number.isRequired,
selected: React.PropTypes.number,
@ -46,8 +48,8 @@ export default React.createClass({
componentWillReceiveProps: function(props) {
// Make sure the selected item isn't outside the list bounds
var selected = this.state.selected;
var maxSelected = this._maxSelected(props.addressList);
const selected = this.state.selected;
const maxSelected = this._maxSelected(props.addressList);
if (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
// at the top of the window.
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;
}
},
@ -75,7 +77,7 @@ export default React.createClass({
if (this.state.selected > 0) {
this.setState({
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)) {
this.setState({
selected: this.state.selected + 1,
hover : false,
hover: false,
});
}
},
@ -105,7 +107,7 @@ export default React.createClass({
},
onMouseLeave: function() {
this.setState({ hover : false });
this.setState({ hover: false });
},
selectAddress: function(index) {
@ -117,15 +119,15 @@ export default React.createClass({
},
createAddressListTiles: function() {
var self = this;
var AddressTile = sdk.getComponent("elements.AddressTile");
var maxSelected = this._maxSelected(this.props.addressList);
var addressList = [];
const self = this;
const AddressTile = sdk.getComponent("elements.AddressTile");
const maxSelected = this._maxSelected(this.props.addressList);
const addressList = [];
// Only create the address elements if there are address
if (this.props.addressList.length > 0) {
for (var i = 0; i <= maxSelected; i++) {
var classes = classNames({
for (let i = 0; i <= maxSelected; i++) {
const classes = classNames({
"mx_AddressSelector_addressListElement": true,
"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}
ref={(ref) => { this.addressListElement = ref; }}
>
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
</div>
<AddressTile
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) {
var listSize = list.length === 0 ? 0 : list.length - 1;
var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
const listSize = list.length === 0 ? 0 : list.length - 1;
const maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
return maxSelected;
},
render: function() {
var classes = classNames({
const classes = classNames({
"mx_AddressSelector": true,
"mx_AddressSelector_empty": this.props.addressList.length === 0,
});
@ -168,5 +176,5 @@ export default React.createClass({
{ this.createAddressListTiles() }
</div>
);
}
},
});

View file

@ -45,11 +45,12 @@ export default React.createClass({
const address = this.props.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(
address.avatarMxc, 25, 25, 'crop'
address.avatarMxc, 25, 25, 'crop',
));
} else if (address.addressType === 'email') {
imgUrls.push('img/icon-email-user.svg');
@ -77,7 +78,7 @@ export default React.createClass({
let info;
let error = false;
if (address.addressType === "mx" && address.isKnown) {
if (isMatrixAddress && address.isKnown) {
const idClasses = classNames({
"mx_AddressTile_id": true,
"mx_AddressTile_justified": this.props.justified,
@ -86,10 +87,13 @@ export default React.createClass({
info = (
<div className="mx_AddressTile_mx">
<div className={nameClasses}>{ name }</div>
<div className={idClasses}>{ address.address }</div>
{ this.props.showAddress ?
<div className={idClasses}>{ address.address }</div> :
<div />
}
</div>
);
} else if (address.addressType === "mx") {
} else if (isMatrixAddress) {
const unknownMxClasses = classNames({
"mx_AddressTile_unknownMx": true,
"mx_AddressTile_justified": this.props.justified,
@ -106,24 +110,24 @@ export default React.createClass({
let nameNode = null;
if (address.displayName) {
nameNode = <div className={nameClasses}>{ address.displayName }</div>
nameNode = <div className={nameClasses}>{ address.displayName }</div>;
}
info = (
<div className="mx_AddressTile_mx">
<div className={emailClasses}>{ address.address }</div>
{nameNode}
{ nameNode }
</div>
);
} else {
error = true;
var unknownClasses = classNames({
const unknownClasses = classNames({
"mx_AddressTile_unknown": true,
"mx_AddressTile_justified": this.props.justified,
});
info = (
<div className={unknownClasses}>{_t("Unknown Address")}</div>
<div className={unknownClasses}>{ _t("Unknown Address") }</div>
);
}
@ -150,5 +154,5 @@ export default React.createClass({
{ dismiss }
</div>
);
}
},
});

View file

@ -50,16 +50,16 @@ export default class AppPermission extends React.Component {
let e2eWarningText;
if (this.props.isRoomEncrypted) {
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 (
<div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'>
<img src='img/warning.svg' alt={_t('Warning!')}/>
<img src='img/warning.svg' alt={_t('Warning!')} />
</div>
<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>
{e2eWarningText}
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
{ e2eWarningText }
</div>
<input
className='mx_AppPermissionButton'

View file

@ -19,10 +19,11 @@ limitations under the License.
import url from 'url';
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import { _t, _td } from '../../../languageHandler';
import sdk from '../../../index';
import AppPermission from './AppPermission';
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
isScalarUrl: function() {
const scalarUrl = SdkConfig.get().integrations_rest_url;
return scalarUrl && this.props.url.startsWith(scalarUrl);
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
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() {
@ -118,6 +128,30 @@ export default React.createClass({
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() {
@ -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
_deleteWidgetLabel() {
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 */
@ -227,7 +261,7 @@ export default React.createClass({
if (this.state.loading) {
appTileBody = (
<div className='mx_AppTileBody mx_AppLoading'>
<MessageSpinner msg='Loading...'/>
<MessageSpinner msg='Loading...' />
</div>
);
} else if (this.state.hasPermissionToLoad == true) {
@ -278,19 +312,19 @@ export default React.createClass({
return (
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
{this.formatAppTileName()}
{ this.formatAppTileName() }
<span className="mx_AppTileMenuBarWidgets">
{/* Edit widget */}
{showEditButton && <img
{ /* Edit widget */ }
{ showEditButton && <img
src="img/edit.svg"
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
width="8" height="8"
alt={_t('Edit')}
title={_t('Edit')}
onClick={this._onEditClick}
/>}
/> }
{/* Delete widget */}
{ /* Delete widget */ }
<img src={deleteIcon}
className={deleteClasses}
width="8" height="8"
@ -300,7 +334,7 @@ export default React.createClass({
/>
</span>
</div>
{appTileBody}
{ appTileBody }
</div>
);
},

View file

@ -6,10 +6,10 @@ const AppWarning = (props) => {
return (
<div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'>
<img src='img/warning.svg' alt={_t('Warning!')}/>
<img src='img/warning.svg' alt={_t('Warning!')} />
</div>
<div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>{props.errorMsg}</span>
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span>
</div>
</div>
);

View file

@ -24,7 +24,7 @@ const CreateRoomButton = function(props) {
return (
<ActionButton action="view_create_room"
mouseOverAction={props.callout ? "callout_create_room" : null}
label={ _t("Create new room") }
label={_t("Create new room")}
iconPath="img/icons-create-room.svg"
size={props.size}
tooltip={props.tooltip}

View file

@ -30,7 +30,7 @@ export default React.createClass({
getInitialState: function() {
return {
device: this.props.device
device: this.props.device,
};
},
@ -60,37 +60,37 @@ export default React.createClass({
onUnverifyClick: function() {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.state.device.deviceId, false
this.props.userId, this.state.device.deviceId, false,
);
},
onBlacklistClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.state.device.deviceId, true
this.props.userId, this.state.device.deviceId, true,
);
},
onUnblacklistClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.state.device.deviceId, false
this.props.userId, this.state.device.deviceId, false,
);
},
render: function() {
var blacklistButton = null, verifyButton = null;
let blacklistButton = null, verifyButton = null;
if (this.state.device.isBlocked()) {
blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
onClick={this.onUnblacklistClick}>
{_t("Unblacklist")}
{ _t("Unblacklist") }
</button>
);
} else {
blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
onClick={this.onBlacklistClick}>
{_t("Blacklist")}
{ _t("Blacklist") }
</button>
);
}
@ -99,14 +99,14 @@ export default React.createClass({
verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}>
{_t("Unverify")}
{ _t("Unverify") }
</button>
);
} else {
verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
onClick={this.onVerifyClick}>
{_t("Verify...")}
{ _t("Verify...") }
</button>
);
}

View file

@ -95,7 +95,7 @@ export default class DirectorySearchBox extends React.Component {
onChange={this._onChange} onKeyUp={this._onKeyUp}
placeholder={this.props.placeholder} autoFocus
/>
{join_button}
{ join_button }
<span className="mx_DirectorySearchBox_clear_wrapper">
<span className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} />
</span>

View file

@ -26,6 +26,12 @@ class MenuOption extends React.Component {
this._onClick = this._onClick.bind(this);
}
getDefaultProps() {
return {
disabled: false,
};
}
_onMouseEnter() {
this.props.onMouseEnter(this.props.dropdownKey);
}
@ -46,15 +52,15 @@ class MenuOption extends React.Component {
onClick={this._onClick} onKeyPress={this._onKeyPress}
onMouseEnter={this._onMouseEnter}
>
{this.props.children}
</div>
{ this.props.children }
</div>;
}
};
}
MenuOption.propTypes = {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.node),
React.PropTypes.node
React.PropTypes.node,
]),
highlighted: React.PropTypes.bool,
dropdownKey: React.PropTypes.string,
@ -153,6 +159,8 @@ export default class Dropdown extends React.Component {
}
_onInputClick(ev) {
if (this.props.disabled) return;
if (!this.state.expanded) {
this.setState({
expanded: true,
@ -250,13 +258,13 @@ export default class Dropdown extends React.Component {
onMouseEnter={this._setHighlightedOption}
onClick={this._onMenuOptionClick}
>
{child}
{ child }
</MenuOption>
);
});
if (options.length === 0) {
return [<div key="0" className="mx_Dropdown_option">
{_t("No results")}
{ _t("No results") }
</div>];
}
return options;
@ -279,7 +287,7 @@ export default class Dropdown extends React.Component {
/>;
}
menu = <div className="mx_Dropdown_menu" style={menuStyle}>
{this._getMenuOptions()}
{ this._getMenuOptions() }
</div>;
}
@ -288,12 +296,13 @@ export default class Dropdown extends React.Component {
this.props.getShortOption(this.props.value) :
this.childrenByKey[this.props.value];
currentValue = <div className="mx_Dropdown_option">
{selectedChild}
</div>
{ selectedChild }
</div>;
}
const dropdownClasses = {
mx_Dropdown: true,
mx_Dropdown_disabled: this.props.disabled,
};
if (this.props.className) {
dropdownClasses[this.props.className] = true;
@ -303,9 +312,9 @@ export default class Dropdown extends React.Component {
// to the input, but overflows below it. The root contains both.
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
<AccessibleButton className="mx_Dropdown_input" onClick={this._onInputClick}>
{currentValue}
{ currentValue }
<span className="mx_Dropdown_arrow"></span>
{menu}
{ menu }
</AccessibleButton>
</div>;
}
@ -329,4 +338,6 @@ Dropdown.propTypes = {
// in the dropped-down menu.
getShortOption: React.PropTypes.func,
value: React.PropTypes.string,
}
// negative for consistency with HTML
disabled: React.PropTypes.bool,
};

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