diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 498dfb8818..f66d2c69d3 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -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 diff --git a/.eslintrc.js b/.eslintrc.js index 74790a2964..429aa24993 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -40,6 +40,19 @@ module.exports = { }], "react/jsx-key": ["error"], + // Assert no spacing in JSX curly brackets + // + // + // 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, diff --git a/CHANGELOG.md b/CHANGELOG.md index b5e596144e..2c2af18e43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,313 @@ +Changes in [0.10.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7) (2017-10-16) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7-rc.3...v0.10.7) + + * Update to latest js-sdk + +Changes in [0.10.7-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7-rc.3) (2017-10-13) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7-rc.2...v0.10.7-rc.3) + + * Fix the enableLabs flag, again + [\#1474](https://github.com/matrix-org/matrix-react-sdk/pull/1474) + +Changes in [0.10.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7-rc.2) (2017-10-13) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7-rc.1...v0.10.7-rc.2) + + * Honour the (now legacy) enableLabs flag + [\#1473](https://github.com/matrix-org/matrix-react-sdk/pull/1473) + * Don't show labs features by default + [\#1472](https://github.com/matrix-org/matrix-react-sdk/pull/1472) + * Make features disabled by default + [\#1470](https://github.com/matrix-org/matrix-react-sdk/pull/1470) + +Changes in [0.10.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7-rc.1) (2017-10-13) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.6...v0.10.7-rc.1) + + * Add warm fuzzy dialog for inviting users to a group + [\#1459](https://github.com/matrix-org/matrix-react-sdk/pull/1459) + * enable/disable features in config.json + [\#1468](https://github.com/matrix-org/matrix-react-sdk/pull/1468) + * Update from Weblate. + [\#1469](https://github.com/matrix-org/matrix-react-sdk/pull/1469) + * Don't send RR or RM when peeking at a room + [\#1463](https://github.com/matrix-org/matrix-react-sdk/pull/1463) + * Fix bug that inserted emoji when typing + [\#1467](https://github.com/matrix-org/matrix-react-sdk/pull/1467) + * Ignore VS16 char in RTE + [\#1458](https://github.com/matrix-org/matrix-react-sdk/pull/1458) + * Show failures when sending messages + [\#1460](https://github.com/matrix-org/matrix-react-sdk/pull/1460) + * Run eslint --fix + [\#1461](https://github.com/matrix-org/matrix-react-sdk/pull/1461) + * Show who banned the user on hover + [\#1441](https://github.com/matrix-org/matrix-react-sdk/pull/1441) + * Enhancements to room power level settings + [\#1440](https://github.com/matrix-org/matrix-react-sdk/pull/1440) + * Added TextInputWithCheckbox dialog + [\#868](https://github.com/matrix-org/matrix-react-sdk/pull/868) + * Make it clearer which HS you're logging into + [\#1456](https://github.com/matrix-org/matrix-react-sdk/pull/1456) + * Remove redundant stale onKeyDown + [\#1451](https://github.com/matrix-org/matrix-react-sdk/pull/1451) + * Only allow event state event handlers on state events + [\#1453](https://github.com/matrix-org/matrix-react-sdk/pull/1453) + * Modify the group store to include group rooms + [\#1452](https://github.com/matrix-org/matrix-react-sdk/pull/1452) + * Factor-out GroupStore and create GroupStoreCache + [\#1449](https://github.com/matrix-org/matrix-react-sdk/pull/1449) + * Put related groups UI behind groups labs flag + [\#1448](https://github.com/matrix-org/matrix-react-sdk/pull/1448) + * Restrict Flair in the timeline to related groups of the room + [\#1447](https://github.com/matrix-org/matrix-react-sdk/pull/1447) + * Implement UI for editing related groups of a room + [\#1446](https://github.com/matrix-org/matrix-react-sdk/pull/1446) + * Fix a couple of bugs with EditableItemList + [\#1445](https://github.com/matrix-org/matrix-react-sdk/pull/1445) + * Factor out EditableItemList from AliasSettings + [\#1444](https://github.com/matrix-org/matrix-react-sdk/pull/1444) + * Add dummy translation function to mark translatable strings + [\#1421](https://github.com/matrix-org/matrix-react-sdk/pull/1421) + * Implement button to remove a room from a group + [\#1438](https://github.com/matrix-org/matrix-react-sdk/pull/1438) + * Fix showing 3pid invites in member list + [\#1443](https://github.com/matrix-org/matrix-react-sdk/pull/1443) + * Add button to get to MyGroups (view_my_groups or path #/groups) + [\#1435](https://github.com/matrix-org/matrix-react-sdk/pull/1435) + * Add eslint rule to disallow spaces inside of curly braces + [\#1436](https://github.com/matrix-org/matrix-react-sdk/pull/1436) + * Fix ability to invite existing mx users + [\#1437](https://github.com/matrix-org/matrix-react-sdk/pull/1437) + * Construct address picker message using provided `validAddressTypes` + [\#1434](https://github.com/matrix-org/matrix-react-sdk/pull/1434) + * Fix GroupView summary rooms displaying without avatars + [\#1433](https://github.com/matrix-org/matrix-react-sdk/pull/1433) + * Implement adding rooms to a group (or group summary) by room ID + [\#1432](https://github.com/matrix-org/matrix-react-sdk/pull/1432) + * Give flair avatars a tooltip = the group ID + [\#1431](https://github.com/matrix-org/matrix-react-sdk/pull/1431) + * Fix ability to feature self in a group summary + [\#1430](https://github.com/matrix-org/matrix-react-sdk/pull/1430) + * Implement "Add room to group" feature + [\#1429](https://github.com/matrix-org/matrix-react-sdk/pull/1429) + * Fix group membership publicity + [\#1428](https://github.com/matrix-org/matrix-react-sdk/pull/1428) + * Add support for Jitsi screensharing in electron app + [\#1355](https://github.com/matrix-org/matrix-react-sdk/pull/1355) + * Delint and DRY TextForEvent + [\#1424](https://github.com/matrix-org/matrix-react-sdk/pull/1424) + * Bust the flair caches after 30mins + [\#1427](https://github.com/matrix-org/matrix-react-sdk/pull/1427) + * Show displayname / avatar in group member info + [\#1426](https://github.com/matrix-org/matrix-react-sdk/pull/1426) + * Create GroupSummaryStore for storing group summary stuff + [\#1418](https://github.com/matrix-org/matrix-react-sdk/pull/1418) + * Add status & toggle for publicity + [\#1419](https://github.com/matrix-org/matrix-react-sdk/pull/1419) + * MemberList: show 100 more on overflow tile click + [\#1417](https://github.com/matrix-org/matrix-react-sdk/pull/1417) + * Fix NPE in MemberList + [\#1425](https://github.com/matrix-org/matrix-react-sdk/pull/1425) + * Fix incorrect variable in string + [\#1422](https://github.com/matrix-org/matrix-react-sdk/pull/1422) + * apply i18n _t to string which has already been translated + [\#1420](https://github.com/matrix-org/matrix-react-sdk/pull/1420) + * Make the invite section a truncatedlist too + [\#1416](https://github.com/matrix-org/matrix-react-sdk/pull/1416) + * Implement removal function of features users/rooms + [\#1415](https://github.com/matrix-org/matrix-react-sdk/pull/1415) + * Allow TruncatedList to get children via a callback + [\#1412](https://github.com/matrix-org/matrix-react-sdk/pull/1412) + * Experimental: Lazy load user autocomplete entries + [\#1413](https://github.com/matrix-org/matrix-react-sdk/pull/1413) + * Show displayname & avatar url in group member list + [\#1414](https://github.com/matrix-org/matrix-react-sdk/pull/1414) + * De-lint TruncatedList + [\#1411](https://github.com/matrix-org/matrix-react-sdk/pull/1411) + * Remove unneeded strings + [\#1409](https://github.com/matrix-org/matrix-react-sdk/pull/1409) + * Clean on prerelease + [\#1410](https://github.com/matrix-org/matrix-react-sdk/pull/1410) + * Redesign membership section in GroupView + [\#1408](https://github.com/matrix-org/matrix-react-sdk/pull/1408) + * Implement adding rooms to the group summary + [\#1406](https://github.com/matrix-org/matrix-react-sdk/pull/1406) + * Honour the is_privileged flag in GroupView + [\#1407](https://github.com/matrix-org/matrix-react-sdk/pull/1407) + * Update when a group arrives + [\#1405](https://github.com/matrix-org/matrix-react-sdk/pull/1405) + * Implement `view_group` dispatch when clicking flair + [\#1404](https://github.com/matrix-org/matrix-react-sdk/pull/1404) + * GroupView: Add a User + [\#1402](https://github.com/matrix-org/matrix-react-sdk/pull/1402) + * Track action button click event + [\#1403](https://github.com/matrix-org/matrix-react-sdk/pull/1403) + * Separate sender profile into elements with classes + [\#1401](https://github.com/matrix-org/matrix-react-sdk/pull/1401) + * Fix ugly integration button, use hover to show error + [\#1399](https://github.com/matrix-org/matrix-react-sdk/pull/1399) + * Fix promise error in flair + [\#1400](https://github.com/matrix-org/matrix-react-sdk/pull/1400) + * Flair! + [\#1351](https://github.com/matrix-org/matrix-react-sdk/pull/1351) + * Group Membership UI + [\#1328](https://github.com/matrix-org/matrix-react-sdk/pull/1328) + +Changes in [0.10.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.6) (2017-09-21) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.5...v0.10.6) + + * New version of js-sdk with fixed build + +Changes in [0.10.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.5) (2017-09-21) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.4...v0.10.5) + + * Fix build error (https://github.com/vector-im/riot-web/issues/5091) + +Changes in [0.10.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.4) (2017-09-20) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.4-rc.1...v0.10.4) + + * No changes + +Changes in [0.10.4-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.4-rc.1) (2017-09-19) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.3...v0.10.4-rc.1) + + * Fix RoomView stuck in 'accept invite' state + [\#1396](https://github.com/matrix-org/matrix-react-sdk/pull/1396) + * Only show the integ management button if user is joined + [\#1398](https://github.com/matrix-org/matrix-react-sdk/pull/1398) + * suppressOnHover for member entity tiles which have no onClick + [\#1273](https://github.com/matrix-org/matrix-react-sdk/pull/1273) + * add /devtools command + [\#1268](https://github.com/matrix-org/matrix-react-sdk/pull/1268) + * Fix broken Link + [\#1359](https://github.com/matrix-org/matrix-react-sdk/pull/1359) + * Show who redacted an event on hover + [\#1387](https://github.com/matrix-org/matrix-react-sdk/pull/1387) + * start MELS expanded if it contains a highlighted/permalinked event. + [\#1388](https://github.com/matrix-org/matrix-react-sdk/pull/1388) + * Add ignore user API support + [\#1389](https://github.com/matrix-org/matrix-react-sdk/pull/1389) + * Add option to disable Emoji suggestions + [\#1392](https://github.com/matrix-org/matrix-react-sdk/pull/1392) + * sanitize the i18n for fn:textForHistoryVisibilityEvent + [\#1397](https://github.com/matrix-org/matrix-react-sdk/pull/1397) + * Don't check for only-emoji if there were none + [\#1394](https://github.com/matrix-org/matrix-react-sdk/pull/1394) + * Fix emojification of symbol characters + [\#1393](https://github.com/matrix-org/matrix-react-sdk/pull/1393) + * Update from Weblate. + [\#1395](https://github.com/matrix-org/matrix-react-sdk/pull/1395) + * Make /join join again + [\#1391](https://github.com/matrix-org/matrix-react-sdk/pull/1391) + * Display spinner not room preview after room create + [\#1390](https://github.com/matrix-org/matrix-react-sdk/pull/1390) + * Fix the avatar / room name in room preview + [\#1384](https://github.com/matrix-org/matrix-react-sdk/pull/1384) + * Remove spurious cancel button + [\#1381](https://github.com/matrix-org/matrix-react-sdk/pull/1381) + * Fix starting a chat by email address + [\#1386](https://github.com/matrix-org/matrix-react-sdk/pull/1386) + * respond on copy code block + [\#1363](https://github.com/matrix-org/matrix-react-sdk/pull/1363) + * fix DateUtils inconsistency with 12/24h + [\#1383](https://github.com/matrix-org/matrix-react-sdk/pull/1383) + * allow sending sub,sup and whitelist them on receive + [\#1382](https://github.com/matrix-org/matrix-react-sdk/pull/1382) + * Update roomlist when an event is decrypted + [\#1380](https://github.com/matrix-org/matrix-react-sdk/pull/1380) + * Update from Weblate. + [\#1379](https://github.com/matrix-org/matrix-react-sdk/pull/1379) + * fix radio for theme selection + [\#1368](https://github.com/matrix-org/matrix-react-sdk/pull/1368) + * fix some more zh_Hans - remove entirely broken lines + [\#1378](https://github.com/matrix-org/matrix-react-sdk/pull/1378) + * fix placeholder causing app to break when using zh + [\#1377](https://github.com/matrix-org/matrix-react-sdk/pull/1377) + * Avoid re-rendering RoomList on room switch + [\#1375](https://github.com/matrix-org/matrix-react-sdk/pull/1375) + * Fix 'Failed to load timeline position' regression + [\#1376](https://github.com/matrix-org/matrix-react-sdk/pull/1376) + * Fast path for emojifying strings + [\#1372](https://github.com/matrix-org/matrix-react-sdk/pull/1372) + * Consolidate the code copy button + [\#1374](https://github.com/matrix-org/matrix-react-sdk/pull/1374) + * Only add the code copy button for HTML messages + [\#1373](https://github.com/matrix-org/matrix-react-sdk/pull/1373) + * Don't re-render matrixchat unnecessarily + [\#1371](https://github.com/matrix-org/matrix-react-sdk/pull/1371) + * Don't wait for setState to run onHaveRoom + [\#1370](https://github.com/matrix-org/matrix-react-sdk/pull/1370) + * Introduce a RoomScrollStateStore + [\#1367](https://github.com/matrix-org/matrix-react-sdk/pull/1367) + * Don't always paginate when mounting a ScrollPanel + [\#1369](https://github.com/matrix-org/matrix-react-sdk/pull/1369) + * Remove unused scrollStateMap from LoggedinView + [\#1366](https://github.com/matrix-org/matrix-react-sdk/pull/1366) + * Revert "Implement sticky date separators" + [\#1365](https://github.com/matrix-org/matrix-react-sdk/pull/1365) + * Remove unused string "changing room on a RoomView is not supported" + [\#1361](https://github.com/matrix-org/matrix-react-sdk/pull/1361) + * Remove unused translation code translations + [\#1360](https://github.com/matrix-org/matrix-react-sdk/pull/1360) + * Implement sticky date separators + [\#1353](https://github.com/matrix-org/matrix-react-sdk/pull/1353) + +Changes in [0.10.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3) (2017-09-06) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.3-rc.2...v0.10.3) + + * No changes + +Changes in [0.10.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3-rc.2) (2017-09-05) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.3-rc.1...v0.10.3-rc.2) + + * Fix plurals in translations + [\#1358](https://github.com/matrix-org/matrix-react-sdk/pull/1358) + * Fix typo + [\#1357](https://github.com/matrix-org/matrix-react-sdk/pull/1357) + * Update from Weblate. + [\#1356](https://github.com/matrix-org/matrix-react-sdk/pull/1356) + +Changes in [0.10.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3-rc.1) (2017-09-01) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.2...v0.10.3-rc.1) + + * Fix room change sometimes being very slow + [\#1354](https://github.com/matrix-org/matrix-react-sdk/pull/1354) + * apply shouldHideEvent fn to onRoomTimeline for RoomStatusBar + [\#1346](https://github.com/matrix-org/matrix-react-sdk/pull/1346) + * text4event widget modified, used to show widget added each time. + [\#1345](https://github.com/matrix-org/matrix-react-sdk/pull/1345) + * separate concepts of showing and managing RRs to fix regression + [\#1352](https://github.com/matrix-org/matrix-react-sdk/pull/1352) + * Make staging widgets work with live and vice versa. + [\#1350](https://github.com/matrix-org/matrix-react-sdk/pull/1350) + * Avoid breaking /sync with uncaught exceptions + [\#1349](https://github.com/matrix-org/matrix-react-sdk/pull/1349) + * we need to pass whether it is an invite RoomSubList explicitly (i18n) + [\#1343](https://github.com/matrix-org/matrix-react-sdk/pull/1343) + * Percent encoding isn't a valid thing within _t + [\#1348](https://github.com/matrix-org/matrix-react-sdk/pull/1348) + * Fix spurious notifications + [\#1339](https://github.com/matrix-org/matrix-react-sdk/pull/1339) + * Unbreak password reset with a non-default HS + [\#1347](https://github.com/matrix-org/matrix-react-sdk/pull/1347) + * Remove unnecessary 'load' on notif audio element + [\#1341](https://github.com/matrix-org/matrix-react-sdk/pull/1341) + * _tJsx returns a React Object, the sub fn must return a React Object + [\#1340](https://github.com/matrix-org/matrix-react-sdk/pull/1340) + * Fix deprecation warning about promise.defer() + [\#1292](https://github.com/matrix-org/matrix-react-sdk/pull/1292) + * Fix click to insert completion + [\#1331](https://github.com/matrix-org/matrix-react-sdk/pull/1331) + Changes in [0.10.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.2) (2017-08-24) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.1...v0.10.2) diff --git a/README.md b/README.md index 144e89c938..c3106ccec7 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/jenkins.sh b/jenkins.sh index 0979edfa13..3a2d66739e 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -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 diff --git a/package.json b/package.json index 4e0476e493..85e0dc938a 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js new file mode 100755 index 0000000000..609c6b00c5 --- /dev/null +++ b/scripts/gen-i18n.js @@ -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" +); + diff --git a/scripts/travis.sh b/scripts/travis.sh index f349b06ad5..c4a06c1bd1 100755 --- a/scripts/travis.sh +++ b/scripts/travis.sh @@ -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 diff --git a/src/ActiveRoomObserver.js b/src/ActiveRoomObserver.js new file mode 100644 index 0000000000..d6fbb460b5 --- /dev/null +++ b/src/ActiveRoomObserver.js @@ -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; diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 5f8772c7aa..abc9aa0bed 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -107,6 +107,9 @@ export default class BasePlatform { isElectron(): boolean { return false; } + setupScreenSharingForIframe() { + } + /** * Restarts the application, without neccessarily reloading * any application code diff --git a/src/CallHandler.js b/src/CallHandler.js index 8331d579df..a9539d40e1 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -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 diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 93057fafed..00728061a2 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -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; diff --git a/src/DateUtils.js b/src/DateUtils.js index 78eef57eae..77f3644f6f 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.js @@ -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,13 +92,13 @@ module.exports = { monthName: months[date.getMonth()], day: date.getDate(), fullYear: date.getFullYear(), - time: showTwelveHour ? twelveHourTime(date) : this.formatTime(date), + time: this.formatTime(date, showTwelveHour), }); }, formatTime: function(date, showTwelveHour=false) { if (showTwelveHour) { - return twelveHourTime(date); + return twelveHourTime(date); } return pad(date.getHours()) + ':' + pad(date.getMinutes()); }, diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js new file mode 100644 index 0000000000..0b039074f0 --- /dev/null +++ b/src/GroupAddressPicker.js @@ -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 =
+
{ _t("Who would you like to add to this community?") }
+
+ { _t( + "Warning: any person you add to a community will be publicly "+ + "visible to anyone who knows the community ID", + ) } +
+
; + + 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 =
+
{ _t("Which rooms would you like to add to this community?") }
+
+ { _t( + "Warning: any room you add to a community will be publicly "+ + "visible to anyone who knows the community ID", + ) } +
+
; + + 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(", "), + }); + }); +} diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 87e714083b..b306eab23c 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -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]; @@ -127,7 +147,7 @@ export function processHtmlForSending(html: string): string { * of that HTML. */ export function sanitizedHtmlNode(insaneHtml) { - const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams); + const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams); return
; } @@ -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 = "" + let span = "" + snippet + ""; 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 = - + let node = + { snippet } ; if (highlight && this.highlightLink) { - node = {node}; + node = { node }; } 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. foobar 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; } - 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; + let emojiBody = false; + if (!opts.disableBigEmoji && bodyHasEmoji) { + EMOJI_REGEX.lastIndex = 0; + 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 ; } -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)), diff --git a/src/ImageUtils.js b/src/ImageUtils.js index 3744241874..a83d94a633 100644 --- a/src/ImageUtils.js +++ b/src/ImageUtils.js @@ -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); } diff --git a/src/Login.js b/src/Login.js index 049b79c2f4..0eff94ce60 100644 --- a/src/Login.js +++ b/src/Login.js @@ -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 diff --git a/src/Markdown.js b/src/Markdown.js index 6e735c6f0e..e05f163ba5 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -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); } diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 4264828c7b..0c3d5b3775 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -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); diff --git a/src/Modal.js b/src/Modal.js index 056b6d8bf2..68d75d1ff1 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -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 = ( + 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 = ( -
+ const modal = this._modals[0]; + const dialog = ( +
- {modal.elem} + { modal.elem }
-
+
); diff --git a/src/Notifier.js b/src/Notifier.js index 0a3b346cf4..93ef192fe0 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -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) { diff --git a/src/Presence.js b/src/Presence.js index c45d571217..fab518e1cb 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -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); diff --git a/src/RichText.js b/src/RichText.js index cbd3b9ae18..b61ba0b9a4 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -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 ({props.children}); + return ({ props.children }); }, }; @@ -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) => ( - {props.children} + { props.children } - ) + ), })); markdownDecorators.push({ @@ -136,9 +142,9 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator { }, component: (props) => ( - {props.children} + { props.children } - ) + ), }); // 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): {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( diff --git a/src/Invite.js b/src/RoomInvite.js similarity index 92% rename from src/Invite.js rename to src/RoomInvite.js index b8e33d318a..ceb3dd0fda 100644 --- a/src/Invite.js +++ b/src/RoomInvite.js @@ -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; diff --git a/src/Rooms.js b/src/Rooms.js index 2e3f4457f0..6cc2d867a6 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -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 diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index 0b753cf3ab..c9d056f88e 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -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) { diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index d14d439d66..7698829647 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -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); } diff --git a/src/Skinner.js b/src/Skinner.js index f47572ba01..1fe12f85ab 100644 --- a/src/Skinner.js +++ b/src/Skinner.js @@ -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(); } diff --git a/src/SlashCommands.js b/src/SlashCommands.js index e5378d4347..82665cc2f3 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -240,6 +240,59 @@ const commands = { return reject(this.getUsage()); }), + ignore: new Command("ignore", "", 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: ( +
+

{ _t("You are now ignoring %(userId)s", {userId: userId}) }

+
+ ), + hasCancelButton: false, + }); + }), + ); + } + } + return reject(this.getUsage()); + }), + + unignore: new Command("unignore", "", 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: ( +
+

{ _t("You are no longer ignoring %(userId)s", {userId: userId}) }

+
+ ), + hasCancelButton: false, + }); + }), + ); + } + } + return reject(this.getUsage()); + }), + // Define the power level of a user op: new Command("op", " []", 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", " ", function(roomId, args) { if (args) { diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 95066912ac..51e3eb8dc9 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -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}); - } - else { - return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName}); + return _t('%(senderName)s requested a VoIP conference.', {senderName}); + } else { + return _t('%(senderName)s invited %(targetName)s.', {senderName, 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 @@ -231,11 +242,11 @@ function textForPowerEvent(event) { const to = event.getContent().users[userId]; if (to !== from) { diff.push( - _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { - userId: userId, + _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { + 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) { - return _t('%(widgetName)s widget added by %(senderName)s', { - widgetName, senderName, - }); + 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, + '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 ''; + }, }; diff --git a/src/Tinter.js b/src/Tinter.js index 5bf13e6d4a..6b23df8c9b 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -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"); - } + }, }; diff --git a/src/Unread.js b/src/Unread.js index 8fffc2a429..20e876ad88 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -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; - } + }, }; diff --git a/src/UserAddress.js b/src/UserAddress.js index 9eee48629d..e7501a0d91 100644 --- a/src/UserAddress.js +++ b/src/UserAddress.js @@ -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; } diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 68a1ba229f..68f463c373 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -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. */ +const FEATURES = [ + { + id: 'feature_groups', + name: _td("Communities"), + }, + { + id: 'feature_pinning', + name: _td("Message Pinning"), + }, +]; + export default { - LABS_FEATURES: [ - { - name: "-", - id: 'matrix_apps', - default: true, + getLabsFeatures() { + const featuresConfig = SdkConfig.get()['features'] || {}; - // XXX: Always use default, ignore localStorage and remove from labs - override: true, - }, - ], + // The old flag: honourned for backwards compat + const enableLabs = SdkConfig.get()['enableLabs']; - // horrible but it works. The locality makes this somewhat more palatable. - doTranslations: function() { - this.LABS_FEATURES[0].name = _t("Matrix Apps"); + 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) { diff --git a/src/Velociraptor.js b/src/Velociraptor.js index 9c85bafca0..9a674d4f09 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -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 ( - {Object.values(this.children)} + { Object.values(this.children) } ); }, diff --git a/src/VelocityBounce.js b/src/VelocityBounce.js index 3ad7d207a9..2141b05325 100644 --- a/src/VelocityBounce.js +++ b/src/VelocityBounce.js @@ -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 ) { diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index f3d89f0ff2..6bea2cbb92 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -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}); } - } + }, }; diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index cec2f05de2..a8f588d39a 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -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 ({ _t('unknown device') }); } - var verificationStatus = ({ _t('NOT verified') }); + let verificationStatus = ({ _t('NOT verified') }); if (device.isBlocked()) { verificationStatus = ({ _t('Blacklisted') }); } else if (device.isVerified()) { @@ -118,7 +118,7 @@ module.exports = React.createClass({ { _t('Ed25519 fingerprint') } - {device.getFingerprint()} + { device.getFingerprint() } @@ -126,7 +126,7 @@ module.exports = React.createClass({ }, _renderEventInfo: function() { - var event = this.props.event; + const event = this.props.event; return ( @@ -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 = ( - ); } return ( -
+
{ _t('End-to-end encryption information') }

{ _t('Event information') }

- {this._renderEventInfo()} + { this._renderEventInfo() }

{ _t('Sender device information') }

- {this._renderDeviceInfo()} + { this._renderDeviceInfo() }
- - {buttons} + { buttons }
); - } + }, }); diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 8f113353d9..04274442c2 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -136,13 +136,13 @@ export default React.createClass({ ) }

- {this.state.errStr} + { this.state.errStr }
@@ -155,7 +155,7 @@ export default React.createClass({
@@ -172,7 +172,7 @@ export default React.createClass({ disabled={disableForm} />
diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index 9eac7f78b2..a01b6580f1 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -134,13 +134,13 @@ export default React.createClass({ ) }

- {this.state.errStr} + { this.state.errStr }
@@ -153,14 +153,14 @@ export default React.createClass({
+ disabled={disableForm} />
@@ -170,7 +170,7 @@ export default React.createClass({ disabled={!this.state.enableSubmit || disableForm} />
diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 7a64fb154c..5b10110f04 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -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; diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 6f2f68b121..e85457e6aa 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -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: '', - description: 'Displays action', + description: _td('Displays action'), }, { command: '/ban', args: ' [reason]', - description: 'Bans user with given id', + description: _td('Bans user with given id'), }, { command: '/unban', args: '', - description: 'Unbans user with given id', + description: _td('Unbans user with given id'), }, { command: '/op', args: ' []', - description: 'Define the power level of a user', + description: _td('Define the power level of a user'), }, { command: '/deop', args: '', - description: 'Deops user with given id', + description: _td('Deops user with given id'), }, { command: '/invite', args: '', - description: 'Invites user with given id to current room', + description: _td('Invites user with given id to current room'), }, { command: '/join', args: '', - description: 'Joins room with given alias', + description: _td('Joins room with given alias'), }, { command: '/part', args: '[]', - description: 'Leave room', + description: _td('Leave room'), }, { command: '/topic', args: '', - description: 'Sets the room topic', + description: _td('Sets the room topic'), }, { command: '/kick', args: ' [reason]', - description: 'Kicks user with given id', + description: _td('Kicks user with given id'), }, { command: '/nick', args: '', - description: 'Changes your display nickname', + description: _td('Changes your display nickname'), }, { command: '/ddg', args: '', - description: 'Searches DuckDuckGo for results', + description: _td('Searches DuckDuckGo for results'), }, { command: '/tint', args: ' []', - description: 'Changes colour scheme of current room', + description: _td('Changes colour scheme of current room'), }, { command: '/verify', args: ' ', - description: 'Verifies a user, device, and pubkey tuple', + description: _td('Verifies a user, device, and pubkey tuple'), + }, + { + command: '/ignore', + args: '', + description: _td('Ignores a user, hiding their messages from you'), + }, + { + command: '/unignore', + args: '', + 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: (), range, }; @@ -140,7 +150,7 @@ export default class CommandProvider extends AutocompleteProvider { renderCompletions(completions: [React.Component]): ?React.Component { return
- {completions} + { completions }
; } } diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index 0f0399cf7d..a27533f7c2 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -30,13 +30,13 @@ export class TextualCompletion extends React.Component { subtitle, description, className, - ...restProps, + ...restProps } = this.props; return (
- {title} - {subtitle} - {description} + { title } + { subtitle } + { description }
); } @@ -56,14 +56,14 @@ export class PillCompletion extends React.Component { description, initialComponent, className, - ...restProps, + ...restProps } = this.props; return (
- {initialComponent} - {title} - {subtitle} - {description} + { initialComponent } + { title } + { subtitle } + { description }
); } diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index 9c996bb1cc..b2e85c4668 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -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
- {completions} + { completions }
; } } diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 16e0347a5b..a5b80e3b0e 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -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: ( - {unicode}} /> + { unicode }} /> ), 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
- {completions} + { completions }
; } } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 1770089eb2..cc04f54dda 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -106,7 +106,7 @@ export default class RoomProvider extends AutocompleteProvider { renderCompletions(completions: [React.Component]): ?React.Component { return
- {completions} + { completions }
; } } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 017491a07e..296399c06c 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -33,7 +33,8 @@ const USER_REGEX = /@\S*/g; let instance = null; export default class UserProvider extends AutocompleteProvider { - users: Array = []; + users: Array = 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: ( } + initialComponent={} 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
- {completions} + { completions }
; } diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index e5a62b8345..c3ad7f9cd1 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -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 =
; 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 = (
- {chevron} - + { chevron } +
- +
); diff --git a/src/components/structures/CreateRoom.js b/src/components/structures/CreateRoom.js index 7ecc315ba7..26454c5ea6 100644 --- a/src/components/structures/CreateRoom.js +++ b/src/components/structures/CreateRoom.js @@ -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 ( - + ); } else { - var error_box = ""; + let error_box = ""; if (curr_phase == this.phases.ERROR) { error_box = (
- {_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}) }
); } - 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 (
- +
-
-