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..c6aeb0d1be 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,10 +29,16 @@ module.exports = { // so we replace it with a version that is class property aware "babel/no-invalid-this": "error", + // We appear to follow this most of the time, so let's enforce it instead + // of occasionally following it (or catching it in review) + "keyword-spacing": "error", + /** react **/ // This just uses the react plugin to help eslint known when // variables have been used in JSX "react/jsx-uses-vars": "error", + // Don't mark React as unused if we're using JSX + "react/jsx-uses-react": "error", // bind or arrow function in props causes performance issues "react/jsx-no-bind": ["error", { @@ -40,6 +46,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..87459882c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,597 @@ +Changes in [0.11.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.3) (2017-12-04) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.2...v0.11.3) + + * Bump js-sdk version to pull in fix for [setting room publicity in a group](https://github.com/matrix-org/matrix-js-sdk/commit/aa3201ebb0fff5af2fb733080aa65ed1f7213de6). + +Changes in [0.11.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.2) (2017-11-28) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.1...v0.11.2) + + * Ignore unrecognised login flows + [\#1633](https://github.com/matrix-org/matrix-react-sdk/pull/1633) + +Changes in [0.11.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.1) (2017-11-17) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0...v0.11.1) + + * Fix the force TURN option + [\#1621](https://github.com/matrix-org/matrix-react-sdk/pull/1621) + +Changes in [0.11.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0) (2017-11-15) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0-rc.3...v0.11.0) + + +Changes in [0.11.0-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0-rc.3) (2017-11-14) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0-rc.2...v0.11.0-rc.3) + + +Changes in [0.11.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0-rc.2) (2017-11-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0-rc.1...v0.11.0-rc.2) + + * Make groups a fully-fleged baked-in feature + [\#1603](https://github.com/matrix-org/matrix-react-sdk/pull/1603) + +Changes in [0.11.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0-rc.1) (2017-11-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7...v0.11.0-rc.1) + + * Improve widget rendering on prop updates + [\#1548](https://github.com/matrix-org/matrix-react-sdk/pull/1548) + * Display group member profile (avatar/displayname) in ConfirmUserActionDialog + [\#1595](https://github.com/matrix-org/matrix-react-sdk/pull/1595) + * Don't crash if there isn't a room notif rule + [\#1602](https://github.com/matrix-org/matrix-react-sdk/pull/1602) + * Show group name in flair tooltip if one is set + [\#1596](https://github.com/matrix-org/matrix-react-sdk/pull/1596) + * Convert group avatar URL to HTTP before handing to BaseAvatar + [\#1597](https://github.com/matrix-org/matrix-react-sdk/pull/1597) + * Add group features as starting points for ILAG + [\#1601](https://github.com/matrix-org/matrix-react-sdk/pull/1601) + * Modify the group room visibility API to reflect the js-sdk changes + [\#1598](https://github.com/matrix-org/matrix-react-sdk/pull/1598) + * Update from Weblate. + [\#1599](https://github.com/matrix-org/matrix-react-sdk/pull/1599) + * Revert "UnknownDeviceDialog: get devices from SDK" + [\#1594](https://github.com/matrix-org/matrix-react-sdk/pull/1594) + * Order users in the group member list with admins first + [\#1591](https://github.com/matrix-org/matrix-react-sdk/pull/1591) + * Fetch group members after accepting an invite + [\#1592](https://github.com/matrix-org/matrix-react-sdk/pull/1592) + * Improve address picker for rooms + [\#1589](https://github.com/matrix-org/matrix-react-sdk/pull/1589) + * Fix FlairStore getPublicisedGroupsCached to give the correct, existing + promise + [\#1590](https://github.com/matrix-org/matrix-react-sdk/pull/1590) + * Use the getProfileInfo API for group inviter profile + [\#1585](https://github.com/matrix-org/matrix-react-sdk/pull/1585) + * Add checkbox to GroupAddressPicker for determining visibility of group rooms + [\#1587](https://github.com/matrix-org/matrix-react-sdk/pull/1587) + * Alter group member api + [\#1581](https://github.com/matrix-org/matrix-react-sdk/pull/1581) + * Improve group creation UX + [\#1580](https://github.com/matrix-org/matrix-react-sdk/pull/1580) + * Disable RoomDetailList in GroupView when editing + [\#1583](https://github.com/matrix-org/matrix-react-sdk/pull/1583) + * Default to no read pins if there is no applicable account data + [\#1586](https://github.com/matrix-org/matrix-react-sdk/pull/1586) + * UnknownDeviceDialog: get devices from SDK + [\#1584](https://github.com/matrix-org/matrix-react-sdk/pull/1584) + * Add a small indicator for when a new event is pinned + [\#1486](https://github.com/matrix-org/matrix-react-sdk/pull/1486) + * Implement tooltip for group rooms + [\#1582](https://github.com/matrix-org/matrix-react-sdk/pull/1582) + * Room notifs in autocomplete & composer + [\#1577](https://github.com/matrix-org/matrix-react-sdk/pull/1577) + * Ignore img tags in HTML if src is not specified + [\#1579](https://github.com/matrix-org/matrix-react-sdk/pull/1579) + * Indicate admins in the group member list with a sheriff badge + [\#1578](https://github.com/matrix-org/matrix-react-sdk/pull/1578) + * Remember whether widget drawer was hidden per-room + [\#1533](https://github.com/matrix-org/matrix-react-sdk/pull/1533) + * Throw an error when trying to create a group store with falsey groupId + [\#1576](https://github.com/matrix-org/matrix-react-sdk/pull/1576) + * Fixes React warning + [\#1571](https://github.com/matrix-org/matrix-react-sdk/pull/1571) + * Fix Flair not appearing due to missing this._usersInFlight + [\#1575](https://github.com/matrix-org/matrix-react-sdk/pull/1575) + * Use, if possible, a room's canonical or first alias when viewing the … + [\#1574](https://github.com/matrix-org/matrix-react-sdk/pull/1574) + * Add CSS classes to group ID input in CreateGroupDialog + [\#1573](https://github.com/matrix-org/matrix-react-sdk/pull/1573) + * Give autocomplete providers the room they're in + [\#1568](https://github.com/matrix-org/matrix-react-sdk/pull/1568) + * Fix multiple pills on one line + [\#1572](https://github.com/matrix-org/matrix-react-sdk/pull/1572) + * Fix group invites such that they look similar to room invites + [\#1570](https://github.com/matrix-org/matrix-react-sdk/pull/1570) + * Add a GeminiScrollbar to Your Communities + [\#1569](https://github.com/matrix-org/matrix-react-sdk/pull/1569) + * Fix multiple requests for publicised groups of given user + [\#1567](https://github.com/matrix-org/matrix-react-sdk/pull/1567) + * Add toggle to alter visibility of a room-group association + [\#1566](https://github.com/matrix-org/matrix-react-sdk/pull/1566) + * Pillify room notifs in the timeline + [\#1564](https://github.com/matrix-org/matrix-react-sdk/pull/1564) + * Implement simple GroupRoomInfo + [\#1563](https://github.com/matrix-org/matrix-react-sdk/pull/1563) + * turn NPE on flair resolution errors into a logged error + [\#1565](https://github.com/matrix-org/matrix-react-sdk/pull/1565) + * Less translation in parts + [\#1484](https://github.com/matrix-org/matrix-react-sdk/pull/1484) + * Redact group IDs from analytics + [\#1562](https://github.com/matrix-org/matrix-react-sdk/pull/1562) + * Display whether the group summary/room list is loading + [\#1560](https://github.com/matrix-org/matrix-react-sdk/pull/1560) + * Change client-side validation of group IDs to match synapse + [\#1558](https://github.com/matrix-org/matrix-react-sdk/pull/1558) + * Prevent non-members from opening group settings + [\#1559](https://github.com/matrix-org/matrix-react-sdk/pull/1559) + * Alter UI for disinviting a group member + [\#1556](https://github.com/matrix-org/matrix-react-sdk/pull/1556) + * Only show admin tools to privileged users + [\#1555](https://github.com/matrix-org/matrix-react-sdk/pull/1555) + * Try lowercase username on login + [\#1550](https://github.com/matrix-org/matrix-react-sdk/pull/1550) + * Don't refresh page on password change prompt + [\#1554](https://github.com/matrix-org/matrix-react-sdk/pull/1554) + * Fix initial in GroupAvatar in GroupView + [\#1553](https://github.com/matrix-org/matrix-react-sdk/pull/1553) + * Use "crop" method to scale group avatars in MyGroups + [\#1549](https://github.com/matrix-org/matrix-react-sdk/pull/1549) + * Lowercase all usernames + [\#1547](https://github.com/matrix-org/matrix-react-sdk/pull/1547) + * Add sensible missing entry generator for MELS tests + [\#1546](https://github.com/matrix-org/matrix-react-sdk/pull/1546) + * Fix prompt to re-use chat room + [\#1545](https://github.com/matrix-org/matrix-react-sdk/pull/1545) + * Add unregiseterListener to GroupStore + [\#1544](https://github.com/matrix-org/matrix-react-sdk/pull/1544) + * Fix groups invited users err for non members + [\#1543](https://github.com/matrix-org/matrix-react-sdk/pull/1543) + * Add Mention button to MemberInfo + [\#1532](https://github.com/matrix-org/matrix-react-sdk/pull/1532) + * Only show group settings cog to members + [\#1541](https://github.com/matrix-org/matrix-react-sdk/pull/1541) + * Use correct icon for group room deletion and make themeable + [\#1540](https://github.com/matrix-org/matrix-react-sdk/pull/1540) + * Add invite button to MemberInfo if user has left or wasn't in room + [\#1534](https://github.com/matrix-org/matrix-react-sdk/pull/1534) + * Add option to mirror local video feed + [\#1539](https://github.com/matrix-org/matrix-react-sdk/pull/1539) + * Use the correct userId when displaying who redacted a message + [\#1538](https://github.com/matrix-org/matrix-react-sdk/pull/1538) + * Only show editing UI for aliases/related_groups for users /w power + [\#1529](https://github.com/matrix-org/matrix-react-sdk/pull/1529) + * Swap from `ui_opacity` to `panel_disabled` + [\#1535](https://github.com/matrix-org/matrix-react-sdk/pull/1535) + * Fix room address picker tiles default name + [\#1536](https://github.com/matrix-org/matrix-react-sdk/pull/1536) + * T3chguy/hide level change on 50 + [\#1531](https://github.com/matrix-org/matrix-react-sdk/pull/1531) + * fix missing date sep caused by hidden event at start of day + [\#1537](https://github.com/matrix-org/matrix-react-sdk/pull/1537) + * Add a delete confirmation dialog for widgets + [\#1520](https://github.com/matrix-org/matrix-react-sdk/pull/1520) + * When dispatching view_[my_]group[s], reset RoomViewStore + [\#1530](https://github.com/matrix-org/matrix-react-sdk/pull/1530) + * Prevent editing of UI requiring user privilege if user unprivileged + [\#1528](https://github.com/matrix-org/matrix-react-sdk/pull/1528) + * Use the correct property of the API room objects + [\#1526](https://github.com/matrix-org/matrix-react-sdk/pull/1526) + * Don't include the |other in the translation value + [\#1527](https://github.com/matrix-org/matrix-react-sdk/pull/1527) + * Re-run gen-i18n after fixing https://github.com/matrix-org/matrix-react- + sdk/pull/1521 + [\#1525](https://github.com/matrix-org/matrix-react-sdk/pull/1525) + * Fix some react warnings in GroupMemberList + [\#1522](https://github.com/matrix-org/matrix-react-sdk/pull/1522) + * Fix bug with gen-i18n/js when adding new plurals + [\#1521](https://github.com/matrix-org/matrix-react-sdk/pull/1521) + * Make GroupStoreCache global for cross-package access + [\#1524](https://github.com/matrix-org/matrix-react-sdk/pull/1524) + * Add fields needed by RoomDetailList to groupRoomFromApiObject + [\#1523](https://github.com/matrix-org/matrix-react-sdk/pull/1523) + * Only show flair for groups with avatars set + [\#1519](https://github.com/matrix-org/matrix-react-sdk/pull/1519) + * Refresh group member lists after inviting users + [\#1518](https://github.com/matrix-org/matrix-react-sdk/pull/1518) + * Invalidate the user's public groups cache when changing group publicity + [\#1517](https://github.com/matrix-org/matrix-react-sdk/pull/1517) + * Make the gen-i18n script validate _t calls + [\#1515](https://github.com/matrix-org/matrix-react-sdk/pull/1515) + * Add placeholder to MyGroups page, adjust CSS classes + [\#1514](https://github.com/matrix-org/matrix-react-sdk/pull/1514) + * Rxl881/parallelshell + [\#1338](https://github.com/matrix-org/matrix-react-sdk/pull/1338) + * Run prunei18n + [\#1513](https://github.com/matrix-org/matrix-react-sdk/pull/1513) + * Update from Weblate. + [\#1512](https://github.com/matrix-org/matrix-react-sdk/pull/1512) + * Add script to prune unused translations + [\#1502](https://github.com/matrix-org/matrix-react-sdk/pull/1502) + * Fix creation of DM rooms + [\#1510](https://github.com/matrix-org/matrix-react-sdk/pull/1510) + * Group create dialog: only enter localpart + [\#1507](https://github.com/matrix-org/matrix-react-sdk/pull/1507) + * Improve MyGroups UI + [\#1509](https://github.com/matrix-org/matrix-react-sdk/pull/1509) + * Use object URLs to load Files in to images + [\#1508](https://github.com/matrix-org/matrix-react-sdk/pull/1508) + * Add clientside error for non-alphanumeric group ID + [\#1506](https://github.com/matrix-org/matrix-react-sdk/pull/1506) + * Fix invites to groups without names + [\#1505](https://github.com/matrix-org/matrix-react-sdk/pull/1505) + * Add warning when adding group rooms/users + [\#1504](https://github.com/matrix-org/matrix-react-sdk/pull/1504) + * More Groups->Communities + [\#1503](https://github.com/matrix-org/matrix-react-sdk/pull/1503) + * Groups -> Communities + [\#1501](https://github.com/matrix-org/matrix-react-sdk/pull/1501) + * Factor out Flair cache into FlairStore + [\#1500](https://github.com/matrix-org/matrix-react-sdk/pull/1500) + * Add i18n script to package.json + [\#1499](https://github.com/matrix-org/matrix-react-sdk/pull/1499) + * Make gen-i18n support 'HTML' + [\#1498](https://github.com/matrix-org/matrix-react-sdk/pull/1498) + * fix editing visuals on groupview header + [\#1497](https://github.com/matrix-org/matrix-react-sdk/pull/1497) + * Script to generate the translations base file + [\#1493](https://github.com/matrix-org/matrix-react-sdk/pull/1493) + * Update from Weblate. + [\#1495](https://github.com/matrix-org/matrix-react-sdk/pull/1495) + * Attempt to relate a group to a room when adding it + [\#1494](https://github.com/matrix-org/matrix-react-sdk/pull/1494) + * Shuffle GroupView UI + [\#1490](https://github.com/matrix-org/matrix-react-sdk/pull/1490) + * Fix bug preventing partial group profile + [\#1491](https://github.com/matrix-org/matrix-react-sdk/pull/1491) + * Don't show room IDs when picking rooms + [\#1492](https://github.com/matrix-org/matrix-react-sdk/pull/1492) + * Only show invited section if there are invited group members + [\#1489](https://github.com/matrix-org/matrix-react-sdk/pull/1489) + * Show "Invited" section in the user list + [\#1488](https://github.com/matrix-org/matrix-react-sdk/pull/1488) + * Refactor class names for an entity tile being hovered over + [\#1487](https://github.com/matrix-org/matrix-react-sdk/pull/1487) + * Modify GroupView UI + [\#1475](https://github.com/matrix-org/matrix-react-sdk/pull/1475) + * Message/event pinning + [\#1439](https://github.com/matrix-org/matrix-react-sdk/pull/1439) + * Remove duplicate declaration that breaks the build + [\#1483](https://github.com/matrix-org/matrix-react-sdk/pull/1483) + * Include magnet scheme in sanitize HTML params + [\#1301](https://github.com/matrix-org/matrix-react-sdk/pull/1301) + * Add a way to jump to a user's Read Receipt from MemberInfo + [\#1454](https://github.com/matrix-org/matrix-react-sdk/pull/1454) + * Use standard subsitution syntax in _tJsx + [\#1462](https://github.com/matrix-org/matrix-react-sdk/pull/1462) + * Don't suggest grey as a color scheme for a room + [\#1442](https://github.com/matrix-org/matrix-react-sdk/pull/1442) + * allow hiding of notification body for privacy reasons + [\#1362](https://github.com/matrix-org/matrix-react-sdk/pull/1362) + * Suggest to invite people when speaking in an empty room + [\#1466](https://github.com/matrix-org/matrix-react-sdk/pull/1466) + * Buttons to remove room/self avatar + [\#1478](https://github.com/matrix-org/matrix-react-sdk/pull/1478) + * T3chguy/fix memberlist + [\#1480](https://github.com/matrix-org/matrix-react-sdk/pull/1480) + * add option to disable BigEmoji + [\#1481](https://github.com/matrix-org/matrix-react-sdk/pull/1481) + +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/docs/settings.md b/docs/settings.md new file mode 100644 index 0000000000..d41aebad3c --- /dev/null +++ b/docs/settings.md @@ -0,0 +1,151 @@ +# Settings Reference + +This document serves as developer documentation for using "Granular Settings". Granular Settings allow users to specify different values for a setting at particular levels of interest. For example, a user may say that in a particular room they want URL previews off, but in all other rooms they want them enabled. The `SettingsStore` helps mask the complexity of dealing with the different levels and exposes easy to use getters and setters. + + +## Levels + +Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in order of prioirty, are: +* `device` - The current user's device +* `room-device` - The current user's device, but only when in a specific room +* `room-account` - The current user's account, but only when in a specific room +* `account` - The current user's account +* `room` - A specific room (setting for all members of the room) +* `config` - Values are defined by `config.json` +* `default` - The hardcoded default for the settings + +Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure that room administrators cannot force account-only settings upon participants. + + +## Settings + +Settings are the different options a user may set or experience in the application. These are pre-defined in `src/settings/Settings.js` under the `SETTINGS` constant and have the following minimum requirements: +``` +// The ID is used to reference the setting throughout the application. This must be unique. +"theSettingId": { + // The levels this setting supports is required. In `src/settings/Settings.js` there are various pre-set arrays + // for this option - they should be used where possible to avoid copy/pasting arrays across settings. + supportedLevels: [...], + + // The default for this setting serves two purposes: It provides a value if the setting is not defined at other + // levels, and it serves to demonstrate the expected type to other developers. The value isn't enforced, but it + // should be respected throughout the code. The default may be any data type. + default: false, + + // The display name has two notations: string and object. The object notation allows for different translatable + // strings to be used for different levels, while the string notation represents the string for all levels. + + displayName: _td("Change something"), // effectively `displayName: { "default": _td("Change something") }` + displayName: { + "room": _td("Change something for participants of this room"), + + // Note: the default will be used if the level requested (such as `device`) does not have a string defined here. + "default": _td("Change something"), + } +} +``` + +### Getting values for a setting + +After importing `SettingsStore`, simply make a call to `SettingsStore.getValue`. The `roomId` parameter should always be supplied where possible, even if the setting does not have a per-room level value. This is to ensure that the value returned is best represented in the room, particularly if the setting ever gets a per-room level in the future. + +In settings pages it is often desired to have the value at a particular level instead of getting the calculated value. Call `SettingsStore.getValueAt` to get the value of a setting at a particular level, and optionally make it explicitly at that level. By default `getValueAt` will traverse the tree starting at the provided level; making it explicit means it will not go beyond the provided level. When using `getValueAt`, please be sure to use `SettingLevel` to represent the target level. + +### Setting values for a setting + +Values are defined at particular levels and should be done in a safe manner. There are two checks to perform to ensure a clean save: is the level supported and can the user actually set the value. In most cases, neither should be an issue although there are circumstances where this changes. An example of a safe call is: +```javascript +const isSupported = SettingsStore.isLevelSupported(SettingLevel.ROOM); +if (isSupported) { + const canSetValue = SettingsStore.canSetValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM); + if (canSetValue) { + SettingsStore.setValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM, newValue); + } +} +``` + +These checks may also be performed in different areas of the application to avoid the verbose example above. For instance, the component which allows changing the setting may be hidden conditionally on the above conditions. + +##### `SettingsFlag` component + +Where possible, the `SettingsFlag` component should be used to set simple "flip-a-bit" (true/false) settings. The `SettingsFlag` also supports simple radio button options, such as the theme the user would like to use. +```html + +``` + +### Getting the display name for a setting + +Simply call `SettingsStore.getDisplayName`. The appropriate display name will be returned and automatically translated for you. If a display name cannot be found, it will return `null`. + + +## Features + +Occasionally some parts of the application may be undergoing testing and are not quite production ready. These are commonly known to be behind a "labs flag". Features behind lab flags must go through the granular settings system, and look and act very much normal settings. The exception is that they must supply `isFeature: true` as part of the setting definition and should go through the helper functions on `SettingsStore`. + +### Determining if a feature is enabled + +A simple call to `SettingsStore.isFeatureEnabled` will tell you if the feature is enabled. This will perform all the required calculations to determine if the feature is enabled based upon the configuration and user selection. + +### Enabling a feature + +Features can only be enabled if the feature is in the `labs` state, otherwise this is a no-op. To find the current set of features in the `labs` state, call `SettingsStore.getLabsFeatures`. To set the value, call `SettingsStore.setFeatureEnabled`. + + +## Setting controllers + +Settings may have environmental factors that affect their value or need additional code to be called when they are modified. A setting controller is able to override the calculated value for a setting and react to changes in that setting. Controllers are not a replacement for the level handlers and should only be used to ensure the environment is kept up to date with the setting where it is otherwise not possible. An example of this is the notification settings: they can only be considered enabled if the platform supports notifications, and enabling notifications requires additional steps to actually enable notifications. + +For more information, see `src/settings/controllers/SettingController.js`. + + +## Local echo + +`SettingsStore` will perform local echo on all settings to ensure that immediately getting values does not cause a split-brain scenario. As mentioned in the "Setting values for a setting" section, the appropriate checks should be done to ensure that the user is allowed to set the value. The local echo system assumes that the user has permission and that the request will go through successfully. The local echo only takes effect until the request to save a setting has completed (either successfully or otherwise). + +```javascript +SettingsStore.setValue(...).then(() => { + // The value has actually been stored at this point. +}); +SettingsStore.getValue(...); // this will return the value set in `setValue` above. +``` + + + +# Maintainers Reference + +The granular settings system has a few complex parts to power it. This section is to document how the `SettingsStore` is supposed to work. + +### General information + +The `SettingsStore` uses the hardcoded `LEVEL_ORDER` constant to ensure that it is using the correct override procedure. The array is checked from left to right, simulating the behaviour of overriding values from the higher levels. Each level should be defined in this array, including `default`. + +Handlers (`src/settings/handlers/SettingsHandler.js`) represent a single level and are responsible for getting and setting values at that level. Handlers also provide additional information to the `SettingsStore` such as if the level is supported or if the current user may set values at the level. The `SettingsStore` will use the handler to enforce checks and manipulate settings. Handlers are also responsible for dealing with migration patterns or legacy settings for their level (for example, a setting being renamed or using a different key from other settings in the underlying store). Handlers are provided to the `SettingsStore` via the `LEVEL_HANDLERS` constant. `SettingsStore` will optimize lookups by only considering handlers that are supported on the platform. + +Local echo is achieved through `src/settings/handlers/LocalEchoWrapper.js` which acts as a wrapper around a given handler. This is automatically applied to all defined `LEVEL_HANDLERS` and proxies the calls to the wrapped handler where possible. The echo is achieved by a simple object cache stored within the class itself. The cache is invalidated immediately upon the proxied save call succeeding or failing. + +Controllers are notified of changes by the `SettingsStore`, and are given the opportunity to override values after the `SettingsStore` has deemed the value calculated. Controllers are invoked as the last possible step in the code. + +### Features + +Features automatically get considered as `disabled` if they are not listed in the `SdkConfig` or `enable_labs` is false/not set. Features are always checked against the configuration before going through the level order as they have the option of being forced-on or forced-off for the application. This is done by the `features` section and looks something like this: + +``` +"features": { + "feature_groups": "enable", + "feature_pinning": "disable", // the default + "feature_presence": "labs" +} +``` + +If `enableLabs` is true in the configuration, the default for features becomes `"labs"`. 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-lock.json b/package-lock.json new file mode 100644 index 0000000000..6dd02674be --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6551 @@ +{ + "name": "matrix-react-sdk", + "version": "0.10.7", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", + "dev": true, + "requires": { + "mime-types": "2.1.17", + "negotiator": "0.6.1" + } + }, + "acorn": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.2.tgz", + "integrity": "sha512-o96FZLJBPY1lvTuJylGA9Bk3t/GKPPJG8H0ydQQl01crzwJgspa4AEIq/pVTXigmK0PHVQhiAtn8WMBLL9D2WA==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "3.3.0" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", + "dev": true + }, + "ajv": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz", + "integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=", + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.0.0", + "json-schema-traverse": "0.3.1", + "json-stable-stringify": "1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "another-json": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz", + "integrity": "sha1-tfQBnJc7bdXGUGotk0acttMq7tw=" + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + } + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.9.0" + } + }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arraybuffer.slice": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", + "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "babel-cli": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-cli/-/babel-cli-6.26.0.tgz", + "integrity": "sha1-UCq1SHTX24itALiHoGODzgPQAvE=", + "dev": true, + "requires": { + "babel-core": "6.26.0", + "babel-polyfill": "6.26.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "chokidar": "1.7.0", + "commander": "2.11.0", + "convert-source-map": "1.5.0", + "fs-readdir-recursive": "1.0.0", + "glob": "7.1.2", + "lodash": "4.17.4", + "output-file-sync": "1.1.2", + "path-is-absolute": "1.0.1", + "slash": "1.0.0", + "source-map": "0.5.7", + "v8flags": "2.1.1" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-core": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz", + "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-generator": "6.26.0", + "babel-helpers": "6.24.1", + "babel-messages": "6.23.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "convert-source-map": "1.5.0", + "debug": "2.6.9", + "json5": "0.5.1", + "lodash": "4.17.4", + "minimatch": "3.0.4", + "path-is-absolute": "1.0.1", + "private": "0.1.8", + "slash": "1.0.0", + "source-map": "0.5.7" + } + }, + "babel-eslint": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-6.1.2.tgz", + "integrity": "sha1-UpNBn+NnLWZZjTJ9qWlFZ7pqXy8=", + "dev": true, + "requires": { + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash.assign": "4.2.0", + "lodash.pickby": "4.6.0" + } + }, + "babel-generator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz", + "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=", + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.4", + "source-map": "0.5.7", + "trim-right": "1.0.1" + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "dev": true, + "requires": { + "babel-helper-explode-assignable-expression": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-builder-react-jsx": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", + "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "esutils": "2.0.2" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.4" + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "dev": true, + "requires": { + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.4" + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "dev": true, + "requires": { + "babel-helper-optimise-call-expression": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-loader": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-6.4.1.tgz", + "integrity": "sha1-CzQRLVsHSKjc2/Uaz2+b1C1QuMo=", + "dev": true, + "requires": { + "find-cache-dir": "0.1.1", + "loader-utils": "0.2.17", + "mkdirp": "0.5.1", + "object-assign": "4.1.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-add-module-exports": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-0.2.1.tgz", + "integrity": "sha1-mumh9KjcZ/DN7E9K7aHkOl/2XiU=", + "dev": true + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", + "dev": true + }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", + "dev": true + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", + "dev": true + }, + "babel-plugin-syntax-flow": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", + "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=", + "dev": true + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=", + "dev": true + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", + "dev": true + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", + "dev": true + }, + "babel-plugin-transform-async-to-bluebird": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-bluebird/-/babel-plugin-transform-async-to-bluebird-1.1.1.tgz", + "integrity": "sha1-Ruo+fFr2KXgqyfHtG3zTj4Qlr9Q=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-plugin-syntax-async-functions": "6.13.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0" + } + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-functions": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-plugin-syntax-class-properties": "6.13.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.4" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "dev": true, + "requires": { + "babel-helper-define-map": "6.26.0", + "babel-helper-function-name": "6.24.1", + "babel-helper-optimise-call-expression": "6.24.1", + "babel-helper-replace-supers": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz", + "integrity": "sha1-DYOUApt9xqvhqX7xgeAHWN0uXYo=", + "dev": true, + "requires": { + "babel-plugin-transform-strict-mode": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "dev": true, + "requires": { + "babel-helper-replace-supers": "6.24.1", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "dev": true, + "requires": { + "babel-helper-call-delegate": "6.24.1", + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "dev": true, + "requires": { + "babel-helper-regex": "6.26.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "dev": true, + "requires": { + "babel-helper-regex": "6.26.0", + "babel-runtime": "6.26.0", + "regexpu-core": "2.0.0" + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "dev": true, + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "6.24.1", + "babel-plugin-syntax-exponentiation-operator": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-flow-strip-types": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", + "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", + "dev": true, + "requires": { + "babel-plugin-syntax-flow": "6.18.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-react-display-name": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", + "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-react-jsx": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", + "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", + "dev": true, + "requires": { + "babel-helper-builder-react-jsx": "6.26.0", + "babel-plugin-syntax-jsx": "6.18.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-react-jsx-self": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz", + "integrity": "sha1-322AqdomEqEh5t3XVYvL7PBuY24=", + "dev": true, + "requires": { + "babel-plugin-syntax-jsx": "6.18.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-react-jsx-source": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", + "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=", + "dev": true, + "requires": { + "babel-plugin-syntax-jsx": "6.18.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", + "dev": true, + "requires": { + "regenerator-transform": "0.10.1" + } + }, + "babel-plugin-transform-runtime": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", + "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "core-js": "2.5.1", + "regenerator-runtime": "0.10.5" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "dev": true + } + } + }, + "babel-preset-es2015": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoping": "6.26.0", + "babel-plugin-transform-es2015-classes": "6.24.1", + "babel-plugin-transform-es2015-computed-properties": "6.24.1", + "babel-plugin-transform-es2015-destructuring": "6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "6.24.1", + "babel-plugin-transform-es2015-for-of": "6.23.0", + "babel-plugin-transform-es2015-function-name": "6.24.1", + "babel-plugin-transform-es2015-literals": "6.22.0", + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", + "babel-plugin-transform-es2015-modules-systemjs": "6.24.1", + "babel-plugin-transform-es2015-modules-umd": "6.24.1", + "babel-plugin-transform-es2015-object-super": "6.24.1", + "babel-plugin-transform-es2015-parameters": "6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", + "babel-plugin-transform-es2015-spread": "6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "6.24.1", + "babel-plugin-transform-es2015-template-literals": "6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "6.24.1", + "babel-plugin-transform-regenerator": "6.26.0" + } + }, + "babel-preset-es2016": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2016/-/babel-preset-es2016-6.24.1.tgz", + "integrity": "sha1-+QC/k+LrwNJ235uKtZck6/2Vn4s=", + "dev": true, + "requires": { + "babel-plugin-transform-exponentiation-operator": "6.24.1" + } + }, + "babel-preset-es2017": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2017/-/babel-preset-es2017-6.24.1.tgz", + "integrity": "sha1-WXvq37n38gi8/YoS6bKym4svFNE=", + "dev": true, + "requires": { + "babel-plugin-syntax-trailing-function-commas": "6.22.0", + "babel-plugin-transform-async-to-generator": "6.24.1" + } + }, + "babel-preset-flow": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz", + "integrity": "sha1-5xIYiHCFrpoktb5Baa/7WZgWxJ0=", + "dev": true, + "requires": { + "babel-plugin-transform-flow-strip-types": "6.22.0" + } + }, + "babel-preset-react": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", + "integrity": "sha1-umnfrqRfw+xjm2pOzqbhdwLJE4A=", + "dev": true, + "requires": { + "babel-plugin-syntax-jsx": "6.18.0", + "babel-plugin-transform-react-display-name": "6.25.0", + "babel-plugin-transform-react-jsx": "6.24.1", + "babel-plugin-transform-react-jsx-self": "6.22.0", + "babel-plugin-transform-react-jsx-source": "6.22.0", + "babel-preset-flow": "6.23.0" + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "dev": true, + "requires": { + "babel-core": "6.26.0", + "babel-runtime": "6.26.0", + "core-js": "2.5.1", + "home-or-tmp": "2.0.0", + "lodash": "4.17.4", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "2.5.1", + "regenerator-runtime": "0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true + }, + "base64-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", + "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==", + "dev": true + }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dev": true, + "requires": { + "callsite": "1.0.0" + } + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "binary-extensions": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", + "integrity": "sha1-muuabF6IY4qtFx4Wf1kAq+JINdA=", + "dev": true + }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=", + "dev": true + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "blueimp-canvas-to-blob": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.14.0.tgz", + "integrity": "sha512-i6I2CiX1VR8YwUNYBo+dM8tg89ns4TTHxSpWjaDeHKcYS3yFalpLCwDaY21/EsJMufLy2tnG4j0JN5L8OVNkKQ==" + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "content-type": "1.0.4", + "debug": "2.6.9", + "depd": "1.1.1", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "1.6.15" + } + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.2.0" + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "browser-encrypt-attachment": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/browser-encrypt-attachment/-/browser-encrypt-attachment-0.3.0.tgz", + "integrity": "sha1-IFqUyq3w3H6BQTlBgS9lW9GQ/xw=" + }, + "browser-request": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz", + "integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc=" + }, + "browserify-aes": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-0.4.0.tgz", + "integrity": "sha1-BnFJtmjfMcS1hTPgLQHoBthgjiw=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "dev": true, + "requires": { + "pako": "0.2.9" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "1.2.1", + "ieee754": "1.1.8", + "isarray": "1.0.0" + } + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "0.2.0" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "fsevents": "1.1.2", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "classnames": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", + "integrity": "sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0=" + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "1.0.1" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true + } + } + }, + "clone": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", + "integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=", + "dev": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "combine-lists": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz", + "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "commonmark": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.27.0.tgz", + "integrity": "sha1-2GwmK5YoIelIPGnFR7xYhAwEezQ=", + "requires": { + "entities": "1.1.1", + "mdurl": "1.0.1", + "minimist": "1.2.0", + "string.prototype.repeat": "0.2.0" + } + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", + "dev": true + }, + "component-emitter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", + "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=", + "dev": true + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "typedarray": "0.0.6" + } + }, + "connect": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.5.tgz", + "integrity": "sha1-+43ee6B2OHfQ7J352sC0tA5yx9o=", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.0.6", + "parseurl": "1.3.2", + "utils-merge": "1.0.1" + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", + "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=", + "dev": true + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, + "core-js": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz", + "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "counterpart": { + "version": "0.18.3", + "resolved": "https://registry.npmjs.org/counterpart/-/counterpart-0.18.3.tgz", + "integrity": "sha512-tli4qPAFeYB34LvvCc/1xYRLCWjf4WsUt6sXfpggDfGDKoI8rhnabz0SljDoBpAK8z1u8GBCg0YDkbvWb16uUQ==", + "requires": { + "date-names": "0.1.10", + "except": "0.1.3", + "extend": "3.0.1", + "pluralizers": "0.1.6", + "sprintf-js": "1.1.1" + } + }, + "create-react-class": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.2.tgz", + "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.2.0" + } + } + } + }, + "crypto-browserify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.3.0.tgz", + "integrity": "sha1-ufx1u0oO1h3PHNXa6W6zDJw+UGw=", + "dev": true, + "requires": { + "browserify-aes": "0.4.0", + "pbkdf2-compat": "2.0.1", + "ripemd160": "0.2.0", + "sha.js": "2.2.6" + } + }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "0.10.35" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "date-names": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/date-names/-/date-names-0.1.10.tgz", + "integrity": "sha1-YvjZMyKVBEZX8852FtijQCH47ys=" + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "dev": true, + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.11" + } + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.0", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.6.2" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "diff": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", + "dev": true + }, + "doctrine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", + "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=", + "dev": true, + "requires": { + "esutils": "2.0.2", + "isarray": "1.0.0" + } + }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "requires": { + "custom-event": "1.0.1", + "ent": "2.2.0", + "extend": "3.0.1", + "void-elements": "2.0.1" + } + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "requires": { + "domelementtype": "1.1.3", + "entities": "1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" + } + } + }, + "domain-browser": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", + "dev": true + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" + }, + "domhandler": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", + "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", + "requires": { + "domelementtype": "1.3.0" + } + }, + "domutils": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.6.2.tgz", + "integrity": "sha1-GVjMC0yUJuntNn+xyOhUiRsPo/8=", + "requires": { + "dom-serializer": "0.1.0", + "domelementtype": "1.3.0" + } + }, + "draft-js": { + "version": "0.11.0-alpha", + "resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.11.0-alpha.tgz", + "integrity": "sha1-MtshCPkn6bhEbaH3nkR1wrf4aK4=", + "requires": { + "fbjs": "0.8.16", + "immutable": "3.7.6", + "object-assign": "4.1.1" + } + }, + "draft-js-export-html": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/draft-js-export-html/-/draft-js-export-html-0.6.0.tgz", + "integrity": "sha1-zIDwVExD0Kf+28U8DLCRToCQ92k=", + "requires": { + "draft-js-utils": "1.2.0" + } + }, + "draft-js-export-markdown": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/draft-js-export-markdown/-/draft-js-export-markdown-0.3.0.tgz", + "integrity": "sha1-hjkOA86vHTR/xhaGerf1Net2v0I=", + "requires": { + "draft-js-utils": "1.2.0" + } + }, + "draft-js-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/draft-js-utils/-/draft-js-utils-1.2.0.tgz", + "integrity": "sha1-9csj6xZzJf/tPXmIL9wxdyHS/RI=" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "emojione": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/emojione/-/emojione-2.2.7.tgz", + "integrity": "sha1-RkV89rmy+NoTroouTlR94G7hXpY=" + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encodeurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=", + "dev": true + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "0.4.19" + } + }, + "engine.io": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.3.tgz", + "integrity": "sha1-jef5eJXSDTm4X4ju7nd7K9QrE9Q=", + "dev": true, + "requires": { + "accepts": "1.3.3", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "ws": "1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "engine.io-client": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.3.tgz", + "integrity": "sha1-F5jtk0USRkU9TG9jXXogH+lA1as=", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parsejson": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "1.1.2", + "xmlhttprequest-ssl": "1.5.3", + "yeast": "0.1.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "engine.io-parser": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.2.tgz", + "integrity": "sha1-k3sHnwAH0Ik+xW1GyyILjLQ1Igo=", + "dev": true, + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "0.0.6", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.4", + "has-binary": "0.1.7", + "wtf-8": "1.0.0" + } + }, + "enhanced-resolve": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz", + "integrity": "sha1-TW5omzcl+GCQknzMhs2fFjW4ni4=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "memory-fs": "0.2.0", + "tapable": "0.1.10" + }, + "dependencies": { + "memory-fs": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", + "integrity": "sha1-8rslNovBIeORwlIN6Slpyu4KApA=", + "dev": true + } + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" + }, + "errno": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", + "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", + "dev": true, + "requires": { + "prr": "0.0.0" + } + }, + "es-abstract": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.9.0.tgz", + "integrity": "sha512-kk3IJoKo7A3pWJc0OV8yZ/VEX2oSUytfekrJiqoxBlKJMFAJVJVpGdHClCCTdv+Fn2zHfpDHHIelMFhZVfef3Q==", + "dev": true, + "requires": { + "es-to-primitive": "1.1.1", + "function-bind": "1.1.1", + "has": "1.0.1", + "is-callable": "1.1.3", + "is-regex": "1.0.4" + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "dev": true, + "requires": { + "is-callable": "1.1.3", + "is-date-object": "1.0.1", + "is-symbol": "1.0.1" + } + }, + "es5-ext": { + "version": "0.10.35", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.35.tgz", + "integrity": "sha1-GO6FjOajxFx9eekcFfzKnsVoSU8=", + "dev": true, + "requires": { + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.35", + "es6-symbol": "3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.35", + "es6-iterator": "2.0.3", + "es6-set": "0.1.5", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.35", + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.35" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.35", + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true, + "requires": { + "es6-map": "0.1.5", + "es6-weak-map": "2.0.2", + "esrecurse": "4.2.0", + "estraverse": "4.2.0" + } + }, + "eslint": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", + "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "chalk": "1.1.3", + "concat-stream": "1.6.0", + "debug": "2.6.9", + "doctrine": "2.0.0", + "escope": "3.6.0", + "espree": "3.5.1", + "esquery": "1.0.0", + "estraverse": "4.2.0", + "esutils": "2.0.2", + "file-entry-cache": "2.0.0", + "glob": "7.1.2", + "globals": "9.18.0", + "ignore": "3.3.5", + "imurmurhash": "0.1.4", + "inquirer": "0.12.0", + "is-my-json-valid": "2.16.1", + "is-resolvable": "1.0.0", + "js-yaml": "3.10.0", + "json-stable-stringify": "1.0.1", + "levn": "0.3.0", + "lodash": "4.17.4", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "optionator": "0.8.2", + "path-is-inside": "1.0.2", + "pluralize": "1.2.1", + "progress": "1.1.8", + "require-uncached": "1.0.3", + "shelljs": "0.7.8", + "strip-bom": "3.0.0", + "strip-json-comments": "2.0.1", + "table": "3.8.3", + "text-table": "0.2.0", + "user-home": "2.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true, + "requires": { + "os-homedir": "1.0.2" + } + } + } + }, + "eslint-config-google": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.7.1.tgz", + "integrity": "sha1-VZj4SY6eB4Qg80uASVuNlZ9lH7I=", + "dev": true + }, + "eslint-plugin-babel": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-babel/-/eslint-plugin-babel-4.1.2.tgz", + "integrity": "sha1-eSAqDjV1fdkngJGbIzbx+i/lPB4=", + "dev": true + }, + "eslint-plugin-flowtype": { + "version": "2.39.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.39.1.tgz", + "integrity": "sha512-RiQv+7Z9QDJuzt+NO8sYgkLGT+h+WeCrxP7y8lI7wpU41x3x/2o3PGtHk9ck8QnA9/mlbNcy/hG0eKvmd7npaA==", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "eslint-plugin-react": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.4.0.tgz", + "integrity": "sha512-tvjU9u3VqmW2vVuYnE8Qptq+6ji4JltjOjJ9u7VAOxVYkUkyBZWRvNYKbDv5fN+L6wiA+4we9+qQahZ0m63XEA==", + "dev": true, + "requires": { + "doctrine": "2.0.0", + "has": "1.0.1", + "jsx-ast-utils": "2.0.1", + "prop-types": "15.6.0" + } + }, + "espree": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.1.tgz", + "integrity": "sha1-DJiLirRttTEAoZVK5LqZXd0n2H4=", + "dev": true, + "requires": { + "acorn": "5.1.2", + "acorn-jsx": "3.0.1" + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "esquery": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", + "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "dev": true, + "requires": { + "estraverse": "4.2.0" + } + }, + "esrecurse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", + "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", + "dev": true, + "requires": { + "estraverse": "4.2.0", + "object-assign": "4.1.1" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "estree-walker": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.5.0.tgz", + "integrity": "sha512-/bEAy+yKAZQrEWUhGmS3H9XpGqSDBtRzX0I2PgMw9kA2n1jN22uV5B5p7MFdZdvWdXCRJztXAfx6ZeRfgkEETg==", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.35" + } + }, + "eventemitter3": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", + "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "except": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/except/-/except-0.1.3.tgz", + "integrity": "sha1-mCYckZWFUVNrREgiOOl4P7c9KSo=", + "requires": { + "indexof": "0.0.1" + } + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "expand-braces": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", + "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=", + "dev": true, + "requires": { + "array-slice": "0.2.3", + "array-unique": "0.2.1", + "braces": "0.1.5" + }, + "dependencies": { + "braces": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz", + "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=", + "dev": true, + "requires": { + "expand-range": "0.1.1" + } + }, + "expand-range": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", + "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", + "dev": true, + "requires": { + "is-number": "0.1.1", + "repeat-string": "0.2.2" + } + }, + "is-number": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", + "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", + "dev": true + }, + "repeat-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz", + "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=", + "dev": true + } + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, + "expect": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-1.20.2.tgz", + "integrity": "sha1-1Fj+TFYAQDa64yMkFqP2Nh8E+WU=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "has": "1.0.1", + "is-equal": "1.5.5", + "is-regex": "1.0.4", + "object-inspect": "1.3.0", + "object-keys": "1.0.11", + "tmatch": "2.0.1" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fbemitter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-2.1.1.tgz", + "integrity": "sha1-Uj4U/a9SSIBbsC9i78M75wP1GGU=", + "requires": { + "fbjs": "0.8.16" + } + }, + "fbjs": { + "version": "0.8.16", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", + "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=", + "requires": { + "core-js": "1.2.7", + "isomorphic-fetch": "2.2.1", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "promise": "7.3.1", + "setimmediate": "1.0.5", + "ua-parser-js": "0.7.17" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + } + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5", + "object-assign": "4.1.1" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "1.3.0", + "object-assign": "4.1.1" + } + }, + "file-saver": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-1.3.3.tgz", + "integrity": "sha1-zdTETTqiZOrC9o7BZbx5HDSvEjI=" + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "filesize": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.5.6.tgz", + "integrity": "sha1-X9mPPqyU7JUW747VeC+thKAaCho=" + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "finalhandler": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.6.tgz", + "integrity": "sha1-AHrqM9Gk0+QgF/YkhIrVjSEvgU8=", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.3.1", + "unpipe": "1.0.0" + } + }, + "find-cache-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", + "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", + "dev": true, + "requires": { + "commondir": "1.0.1", + "mkdirp": "0.5.1", + "pkg-dir": "1.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "dev": true, + "requires": { + "circular-json": "0.3.3", + "del": "2.2.2", + "graceful-fs": "4.1.11", + "write": "0.2.1" + } + }, + "flow-parser": { + "version": "0.57.3", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.57.3.tgz", + "integrity": "sha1-uNJBobHLrgQ6+nl2458mmYjY/jQ=", + "dev": true + }, + "flux": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/flux/-/flux-2.1.1.tgz", + "integrity": "sha1-LGrGUtQzdIiWhInGWG86/yajjqQ=", + "requires": { + "fbemitter": "2.1.1", + "fbjs": "0.1.0-alpha.7", + "immutable": "3.7.6" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + }, + "fbjs": { + "version": "0.1.0-alpha.7", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.1.0-alpha.7.tgz", + "integrity": "sha1-rUMIuPIy+zxzYDNJ6nJdHpw5Mjw=", + "requires": { + "core-js": "1.2.7", + "promise": "7.3.1", + "whatwg-fetch": "0.9.0" + } + }, + "whatwg-fetch": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz", + "integrity": "sha1-DjaExsuZlbQ+/J3wPkw2XZX9nMA=" + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, + "foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", + "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "formatio": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", + "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", + "dev": true, + "requires": { + "samsam": "1.1.2" + } + }, + "fs-access": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", + "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", + "dev": true, + "requires": { + "null-check": "1.0.0" + } + }, + "fs-readdir-recursive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz", + "integrity": "sha1-jNF0XItPiinIyuw5JHaSG6GV9WA=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", + "integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.7.0", + "node-pre-gyp": "0.6.36" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.9" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true, + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.7", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true, + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "bundled": true, + "dev": true + }, + "mime-types": { + "version": "2.1.15", + "bundled": true, + "dev": true, + "requires": { + "mime-db": "1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.36", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true, + "dev": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "bundled": true, + "dev": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.0.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.0.1", + "bundled": true, + "dev": true + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.2.9", + "rimraf": "2.6.1", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "dev": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "uuid": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "fuse.js": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-2.7.4.tgz", + "integrity": "sha1-luQg/efvARrEnCWKYhMU/ldlNvk=" + }, + "gemini-scrollbar": { + "version": "github:matrix-org/gemini-scrollbar#b302279810d05319ac5ff1bd34910bff32325c7b" + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "1.0.2" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "5.2.3", + "har-schema": "2.0.0" + } + }, + "has": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "dev": true, + "requires": { + "function-bind": "1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-binary": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", + "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.0", + "sntp": "2.0.2" + } + }, + "highlight.js": { + "version": "8.9.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-8.9.1.tgz", + "integrity": "sha1-uKnFSTISqTkvAiK2SclhFJfr+4g=" + }, + "hoek": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "htmlparser2": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", + "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.4.1", + "domutils": "1.6.2", + "entities": "1.1.1", + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "dev": true, + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.3.1" + } + }, + "http-proxy": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.16.2.tgz", + "integrity": "sha1-Bt/ykpUr9k2+hHH6nfcwZtTzd0I=", + "dev": true, + "requires": { + "eventemitter3": "1.2.0", + "requires-port": "1.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "https-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "dev": true + }, + "ignore": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.5.tgz", + "integrity": "sha512-JLH93mL8amZQhh/p6mfQgVBH3M6epNq3DfsXsTSuSrInVjwyYlFE1nv2AgfRCC8PoOhM0jwQ5v8s9LgbK7yGDw==", + "dev": true + }, + "immutable": { + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", + "integrity": "sha1-E7TTyxK++hVIKib+Gy665kAHHks=" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "inquirer": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true, + "requires": { + "ansi-escapes": "1.4.0", + "ansi-regex": "2.1.1", + "chalk": "1.1.3", + "cli-cursor": "1.0.2", + "cli-width": "2.2.0", + "figures": "1.7.0", + "lodash": "4.17.4", + "readline2": "1.0.1", + "run-async": "0.1.0", + "rx-lite": "3.1.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "through": "2.3.8" + } + }, + "interpret": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.4.tgz", + "integrity": "sha1-ggzdWIuGj/sZGoCVBtbJyPISsbA=", + "dev": true + }, + "invariant": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "is-arrow-function": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-arrow-function/-/is-arrow-function-2.0.3.tgz", + "integrity": "sha1-Kb4sLY2UUIUri7r7Y1unuNjofsI=", + "dev": true, + "requires": { + "is-callable": "1.1.3" + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "1.10.0" + } + }, + "is-boolean-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.0.tgz", + "integrity": "sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=", + "dev": true + }, + "is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", + "dev": true + }, + "is-callable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", + "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", + "dev": true + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/is-equal/-/is-equal-1.5.5.tgz", + "integrity": "sha1-XoXxlX4FKIMkf+s4aWWju6Ffuz0=", + "dev": true, + "requires": { + "has": "1.0.1", + "is-arrow-function": "2.0.3", + "is-boolean-object": "1.0.0", + "is-callable": "1.1.3", + "is-date-object": "1.0.1", + "is-generator-function": "1.0.6", + "is-number-object": "1.0.3", + "is-regex": "1.0.4", + "is-string": "1.0.4", + "is-symbol": "1.0.1", + "object.entries": "1.0.4" + } + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-generator-function": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.6.tgz", + "integrity": "sha1-nnFlPNFf/zQcecQVFGChMdMen8Q=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-my-json-valid": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz", + "integrity": "sha512-ochPsqWS1WXj8ZnMIV0vnNXooaMhp7cyL4FMSIPKTtnV0Ha/T19G2b9kkhcNsabV9bxYkze7/aLZJb/bYuFduQ==", + "dev": true, + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-number-object": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.3.tgz", + "integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=", + "dev": true + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true, + "requires": { + "is-path-inside": "1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", + "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "1.0.1" + } + }, + "is-resolvable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", + "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", + "dev": true, + "requires": { + "tryit": "1.0.3" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-string": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.4.tgz", + "integrity": "sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isbinaryfile": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.2.tgz", + "integrity": "sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "1.7.3", + "whatwg-fetch": "1.1.1" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jade": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "dev": true, + "requires": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "dev": true + }, + "mkdirp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", + "dev": true + } + } + }, + "jquery": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz", + "integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c=" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "json-loader": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", + "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jsx-ast-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz", + "integrity": "sha1-6AGxs5mF4g//yHtA43SAgOLcrH8=", + "dev": true, + "requires": { + "array-includes": "3.0.3" + } + }, + "karma": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz", + "integrity": "sha512-k5pBjHDhmkdaUccnC7gE3mBzZjcxyxYsYVaqiL2G5AqlfLyBO5nw2VdNK+O16cveEPd/gIOWULH7gkiYYwVNHg==", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "body-parser": "1.18.2", + "chokidar": "1.7.0", + "colors": "1.1.2", + "combine-lists": "1.0.1", + "connect": "3.6.5", + "core-js": "2.5.1", + "di": "0.0.1", + "dom-serialize": "2.2.1", + "expand-braces": "0.1.2", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "http-proxy": "1.16.2", + "isbinaryfile": "3.0.2", + "lodash": "3.10.1", + "log4js": "0.6.38", + "mime": "1.4.1", + "minimatch": "3.0.4", + "optimist": "0.6.1", + "qjobs": "1.1.5", + "range-parser": "1.2.0", + "rimraf": "2.6.2", + "safe-buffer": "5.1.1", + "socket.io": "1.7.3", + "source-map": "0.5.7", + "tmp": "0.0.31", + "useragent": "2.2.1" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + } + } + }, + "karma-chrome-launcher": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-0.2.3.tgz", + "integrity": "sha1-TG1wDRY6nTTGGO/YeRi+SeekqMk=", + "dev": true, + "requires": { + "fs-access": "1.0.1", + "which": "1.3.0" + } + }, + "karma-cli": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-0.1.2.tgz", + "integrity": "sha1-ys6oQ3Hs4Zh2JlyPoQLru5/uSow=", + "dev": true, + "requires": { + "resolve": "1.4.0" + } + }, + "karma-junit-reporter": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/karma-junit-reporter/-/karma-junit-reporter-0.4.2.tgz", + "integrity": "sha1-SSojZyj+TJKqz0GfzQEQpDJ+nX8=", + "dev": true, + "requires": { + "path-is-absolute": "1.0.1", + "xmlbuilder": "3.1.0" + } + }, + "karma-logcapture-reporter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/karma-logcapture-reporter/-/karma-logcapture-reporter-0.0.1.tgz", + "integrity": "sha1-vxsLHJFeDeKVoV/i8BedQoG6zdw=", + "dev": true + }, + "karma-mocha": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-0.2.2.tgz", + "integrity": "sha1-OI7ZF9oV3LGW0bkVwZNO+AMZP44=", + "dev": true + }, + "karma-sourcemap-loader": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz", + "integrity": "sha1-kTIsd/jxPUb+0GKwQuEAnUxFBdg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "karma-spec-reporter": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/karma-spec-reporter/-/karma-spec-reporter-0.0.31.tgz", + "integrity": "sha1-SDDccUihVcfXoYbmMjOaDYD63sM=", + "dev": true, + "requires": { + "colors": "1.1.2" + } + }, + "karma-summary-reporter": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/karma-summary-reporter/-/karma-summary-reporter-1.3.3.tgz", + "integrity": "sha1-nHQKJLYL+RNes59acylsTM0Q2Zs=", + "dev": true, + "requires": { + "chalk": "1.1.3" + } + }, + "karma-webpack": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-1.8.1.tgz", + "integrity": "sha1-OdX9Lt7qPMPvW0BZibN9Ww5qO04=", + "dev": true, + "requires": { + "async": "0.9.2", + "loader-utils": "0.2.17", + "lodash": "3.10.1", + "source-map": "0.1.43", + "webpack-dev-middleware": "1.12.0" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "linkifyjs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-2.1.5.tgz", + "integrity": "sha512-8FqxPXQDLjI2nNHlM7eGewxE6DHvMbtiW0AiXzm0s4RkTwVZYRDTeVXkiRxLHTd4CuRBQY/JPtvtqJWdS7gHyA==", + "requires": { + "jquery": "3.2.1", + "react": "15.6.2", + "react-dom": "15.6.2" + } + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1", + "object-assign": "4.1.1" + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", + "dev": true + }, + "lodash.pickby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", + "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8=", + "dev": true + }, + "log4js": { + "version": "0.6.38", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz", + "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "semver": "4.3.6" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "lolex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", + "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "3.0.2" + } + }, + "lru-cache": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.2.4.tgz", + "integrity": "sha1-bGWGGb7PFAMdDQtZSxYELOTcBj0=", + "dev": true + }, + "matrix-js-sdk": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-0.8.5.tgz", + "integrity": "sha1-1ZAVTx53ADVyZw+p28rH5APnbk8=", + "requires": { + "another-json": "0.2.0", + "bluebird": "3.5.1", + "browser-request": "0.3.3", + "content-type": "1.0.4", + "request": "2.83.0" + } + }, + "matrix-react-test-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/matrix-react-test-utils/-/matrix-react-test-utils-0.1.1.tgz", + "integrity": "sha1-tUiETQ6+M46hucjxZHTDDRfDvfQ=", + "dev": true, + "requires": { + "react": "15.6.2", + "react-dom": "15.6.2" + } + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "0.1.4", + "readable-stream": "2.3.3" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "1.30.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "mocha": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz", + "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=", + "dev": true, + "requires": { + "commander": "2.3.0", + "debug": "2.2.0", + "diff": "1.4.0", + "escape-string-regexp": "1.0.2", + "glob": "3.2.11", + "growl": "1.9.2", + "jade": "0.26.3", + "mkdirp": "0.5.1", + "supports-color": "1.2.0", + "to-iso-string": "0.0.2" + }, + "dependencies": { + "commander": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", + "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=", + "dev": true + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "escape-string-regexp": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", + "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=", + "dev": true + }, + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimatch": "0.3.0" + } + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2.2.4", + "sigmund": "1.0.1" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "supports-color": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz", + "integrity": "sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4=", + "dev": true + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + }, + "nan": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", + "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=", + "dev": true, + "optional": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "dev": true + }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "0.1.12", + "is-stream": "1.1.0" + } + }, + "node-libs-browser": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-0.7.0.tgz", + "integrity": "sha1-PicsCBnjCJNeJmdECNevDhSRuDs=", + "dev": true, + "requires": { + "assert": "1.4.1", + "browserify-zlib": "0.1.4", + "buffer": "4.9.1", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.3.0", + "domain-browser": "1.1.7", + "events": "1.1.1", + "https-browserify": "0.0.1", + "os-browserify": "0.2.1", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "readable-stream": "2.3.3", + "stream-browserify": "2.0.1", + "stream-http": "2.7.2", + "string_decoder": "0.10.31", + "timers-browserify": "2.0.4", + "tty-browserify": "0.0.0", + "url": "0.11.0", + "util": "0.10.3", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "null-check": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", + "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", + "dev": true + }, + "object-inspect": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.3.0.tgz", + "integrity": "sha512-OHHnLgLNXpM++GnJRyyhbr2bwl3pPVm4YvaraHrRvDt/N3r+s/gDVHciA7EJBTkijKXj61ssgSAikq1fb0IBRg==", + "dev": true + }, + "object-keys": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", + "dev": true + }, + "object.entries": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", + "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.9.0", + "function-bind": "1.1.1", + "has": "1.0.1" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "0.0.10", + "wordwrap": "0.0.3" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=", + "dev": true + }, + "os-browserify": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.2.1.tgz", + "integrity": "sha1-Y/xMzuXS13Y9Jrv4YBB45sLgBE8=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "output-file-sync": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz", + "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "mkdirp": "0.5.1", + "object-assign": "4.1.1" + } + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", + "dev": true + }, + "parallelshell": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/parallelshell/-/parallelshell-3.0.2.tgz", + "integrity": "sha512-aW73W8tmYiFZtQi41pweV3WWT6o/EvSxAVQHbumOhN53H47OuWQwrRc11xQ2i44GFvR5AjtzhD92r8Kv9X+7Iw==", + "dev": true + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parsejson": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz", + "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "pbkdf2-compat": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz", + "integrity": "sha1-tuDI+plJTZTgURV1gCpZpcFC8og=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "requires": { + "find-up": "1.1.2" + } + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true + }, + "pluralizers": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/pluralizers/-/pluralizers-0.1.6.tgz", + "integrity": "sha1-GrOLbnYOb5f5hGElCXuGK8l0yB4=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "2.0.6" + } + }, + "prop-types": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", + "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + }, + "prr": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", + "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qjobs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.1.5.tgz", + "integrity": "sha1-ZZ3p8s+NzCehSBJ28gU3cnI4LnM=", + "dev": true + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + } + }, + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", + "requires": { + "create-react-class": "15.6.2", + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "prop-types": "15.6.0" + } + }, + "react-addons-css-transition-group": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/react-addons-css-transition-group/-/react-addons-css-transition-group-15.3.2.tgz", + "integrity": "sha1-2PpSvsm7Yb396LnkZSuAKXy/9mc=" + }, + "react-addons-test-utils": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz", + "integrity": "sha1-wStu/cIkfBDae4dw0YUICnsEcVY=", + "dev": true + }, + "react-dom": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", + "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "prop-types": "15.6.0" + } + }, + "react-gemini-scrollbar": { + "version": "github:matrix-org/react-gemini-scrollbar#5e97aef7e034efc8db1431f4b0efe3b26e249ae9", + "requires": { + "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279810d05319ac5ff1bd34910bff32325c7b" + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.3", + "set-immediate-shim": "1.0.1" + } + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "mute-stream": "0.0.5" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "1.4.0" + } + }, + "regenerate": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", + "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz", + "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==" + }, + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "private": "0.1.8" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regexp-quote": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/regexp-quote/-/regexp-quote-0.0.0.tgz", + "integrity": "sha1-Hg9GUMhi3L/tVP1CsUjpuxch/PI=" + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "dev": true, + "requires": { + "regenerate": "1.3.3", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "request": { + "version": "2.83.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", + "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.1", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "require-json": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/require-json/-/require-json-0.0.1.tgz", + "integrity": "sha1-PIkU+T10Qt6Mv05oGsYqcqozZ/4=", + "dev": true + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "0.1.0", + "resolve-from": "1.0.1" + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", + "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "1.1.1", + "onetime": "1.1.0" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "ripemd160": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-0.2.0.tgz", + "integrity": "sha1-K/GYveFnys+lHAqSjoS2i74XH84=", + "dev": true + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true, + "requires": { + "once": "1.4.0" + } + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "samsam": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", + "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=", + "dev": true + }, + "sanitize-html": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.14.1.tgz", + "integrity": "sha1-cw/6Ikm98YMz7/5FsoYXPJxa0Lg=", + "requires": { + "htmlparser2": "3.9.2", + "regexp-quote": "0.0.0", + "xtend": "4.0.1" + } + }, + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", + "dev": true + }, + "sha.js": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.2.6.tgz", + "integrity": "sha1-F93t3F9yL7ZlAWWIlUYZd4ZzFbo=", + "dev": true + }, + "shelljs": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", + "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", + "dev": true, + "requires": { + "glob": "7.1.2", + "interpret": "1.0.4", + "rechoir": "0.6.2" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "sinon": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", + "integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=", + "dev": true, + "requires": { + "formatio": "1.1.1", + "lolex": "1.3.2", + "samsam": "1.1.2", + "util": "0.10.3" + } + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "sntp": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz", + "integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=", + "requires": { + "hoek": "4.2.0" + } + }, + "socket.io": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz", + "integrity": "sha1-uK+cq6AJSeVo42nxMn6pvp6iRhs=", + "dev": true, + "requires": { + "debug": "2.3.3", + "engine.io": "1.8.3", + "has-binary": "0.1.7", + "object-assign": "4.1.0", + "socket.io-adapter": "0.5.0", + "socket.io-client": "1.7.3", + "socket.io-parser": "2.3.1" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "object-assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", + "dev": true + } + } + }, + "socket.io-adapter": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz", + "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=", + "dev": true, + "requires": { + "debug": "2.3.3", + "socket.io-parser": "2.3.1" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "socket.io-client": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.7.3.tgz", + "integrity": "sha1-sw6GqhDV7zVGYBwJzeR2Xjgdo3c=", + "dev": true, + "requires": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "2.3.3", + "engine.io-client": "1.8.3", + "has-binary": "0.1.7", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseuri": "0.0.5", + "socket.io-parser": "2.3.1", + "to-array": "0.1.4" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "socket.io-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz", + "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=", + "dev": true, + "requires": { + "component-emitter": "1.1.2", + "debug": "2.2.0", + "isarray": "0.0.1", + "json3": "3.3.2" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "source-list-map": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz", + "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-loader": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.1.6.tgz", + "integrity": "sha1-wJkD2m1zueU7ftjuUkVZcFHpjpE=", + "dev": true, + "requires": { + "async": "0.9.2", + "loader-utils": "0.2.17", + "source-map": "0.1.43" + }, + "dependencies": { + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, + "sprintf-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", + "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "stream-http": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", + "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", + "dev": true, + "requires": { + "builtin-status-codes": "3.0.0", + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string.prototype.repeat": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz", + "integrity": "sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8=" + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "table": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "requires": { + "ajv": "4.11.8", + "ajv-keywords": "1.5.1", + "chalk": "1.1.3", + "lodash": "4.17.4", + "slice-ansi": "0.0.4", + "string-width": "2.1.1" + }, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "tapable": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", + "integrity": "sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=", + "dev": true + }, + "text-encoding-utf-8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.1.tgz", + "integrity": "sha1-Uepsen6y+09nRnt2NzVmH1YDSS0=" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "time-stamp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-2.0.0.tgz", + "integrity": "sha1-lcakRTDhW6jW9KPsuMOj+sRto1c=", + "dev": true + }, + "timers-browserify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.4.tgz", + "integrity": "sha512-uZYhyU3EX8O7HQP+J9fTVYwsq90Vr68xPEFo7yrVImIxYvHgukBEgOB/SgGoorWVTzGM/3Z+wUNnboA4M8jWrg==", + "dev": true, + "requires": { + "setimmediate": "1.0.5" + } + }, + "tmatch": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-2.0.1.tgz", + "integrity": "sha1-DFYkbzPzDaG409colauvFmYPOM8=", + "dev": true + }, + "tmp": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", + "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "to-iso-string": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz", + "integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "requires": { + "punycode": "1.4.1" + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tryit": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", + "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", + "dev": true + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.17" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "ua-parser-js": { + "version": "0.7.17", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz", + "integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==" + }, + "uglify-js": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.7.5.tgz", + "integrity": "sha1-RhLAx7qu4rp8SH3kkErhIgefLKg=", + "dev": true, + "requires": { + "async": "0.2.10", + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true + }, + "ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", + "dev": true + }, + "useragent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.2.1.tgz", + "integrity": "sha1-z1k+9PLRdYdei7ZY6pLhik/QbY4=", + "dev": true, + "requires": { + "lru-cache": "2.2.4", + "tmp": "0.0.31" + } + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "v8flags": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "dev": true, + "requires": { + "user-home": "1.1.1" + } + }, + "velocity-vector": { + "version": "github:vector-im/velocity#059e3b2348f1110888d033974d3109fd5a3af00f", + "requires": { + "jquery": "3.2.1" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true + }, + "walk": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.9.tgz", + "integrity": "sha1-MbTbZnjyrgHDnqn7hyWpAx5Vins=", + "dev": true, + "requires": { + "foreachasync": "3.0.0" + } + }, + "watchpack": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-0.2.9.tgz", + "integrity": "sha1-Yuqkq15bo1/fwBgnVibjwPXj+ws=", + "dev": true, + "requires": { + "async": "0.9.2", + "chokidar": "1.7.0", + "graceful-fs": "4.1.11" + } + }, + "webpack": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-1.15.0.tgz", + "integrity": "sha1-T/MfU9sDM55VFkqdRo7gMklo/pg=", + "dev": true, + "requires": { + "acorn": "3.3.0", + "async": "1.5.2", + "clone": "1.0.2", + "enhanced-resolve": "0.9.1", + "interpret": "0.6.6", + "loader-utils": "0.2.17", + "memory-fs": "0.3.0", + "mkdirp": "0.5.1", + "node-libs-browser": "0.7.0", + "optimist": "0.6.1", + "supports-color": "3.2.3", + "tapable": "0.1.10", + "uglify-js": "2.7.5", + "watchpack": "0.2.9", + "webpack-core": "0.6.9" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "interpret": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-0.6.6.tgz", + "integrity": "sha1-/s16GOfOXKar+5U+H4YhOknxYls=", + "dev": true + }, + "memory-fs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.3.0.tgz", + "integrity": "sha1-e8xrYp46Q+hx1+Kaymrop/FcuyA=", + "dev": true, + "requires": { + "errno": "0.1.4", + "readable-stream": "2.3.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "webpack-core": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz", + "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=", + "dev": true, + "requires": { + "source-list-map": "0.1.8", + "source-map": "0.4.4" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "webpack-dev-middleware": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz", + "integrity": "sha1-007++y7dp+HTtdvgcolRMhllFwk=", + "dev": true, + "requires": { + "memory-fs": "0.4.1", + "mime": "1.4.1", + "path-is-absolute": "1.0.1", + "range-parser": "1.2.0", + "time-stamp": "2.0.0" + } + }, + "whatwg-fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-1.1.1.tgz", + "integrity": "sha1-rDydOfMgxtzlM5lp0FTvQ90zMxk=" + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "0.5.1" + } + }, + "ws": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.2.tgz", + "integrity": "sha1-iiRPoFJAHgjJiGz0SoUYnh/UBn8=", + "dev": true, + "requires": { + "options": "0.0.6", + "ultron": "1.0.2" + } + }, + "wtf-8": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz", + "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=", + "dev": true + }, + "xmlbuilder": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-3.1.0.tgz", + "integrity": "sha1-LIaIjy1OrehQ+jjKf3Ij9yCVFuE=", + "dev": true, + "requires": { + "lodash": "3.10.1" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + } + } + }, + "xmlhttprequest-ssl": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz", + "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + } + } +} diff --git a/package.json b/package.json index cc352cb8db..943c443c59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.10.2", + "version": "0.11.3", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -28,19 +28,24 @@ "test" ], "bin": { - "reskindex": "scripts/reskindex.js" + "reskindex": "scripts/reskindex.js", + "matrix-gen-i18n": "scripts/gen-i18n.js", + "matrix-prune-i18n": "scripts/prune-i18n.js" }, "scripts": { "reskindex": "node scripts/reskindex.js -h header", "reskindex:watch": "node scripts/reskindex.js -h header -w", + "i18n": "matrix-gen-i18n", + "prunei18n": "matrix-prune-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,11 +71,14 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "0.8.2", + "matrix-js-sdk": "0.9.2", "optimist": "^0.6.1", "prop-types": "^15.5.8", + "querystring": "^0.2.0", "react": "^15.4.0", "react-addons-css-transition-group": "15.3.2", + "react-dnd": "^2.1.4", + "react-dnd-html5-backend": "^2.1.2", "react-dom": "^15.4.0", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "sanitize-html": "^1.14.1", @@ -99,8 +107,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", @@ -114,12 +124,13 @@ "karma-webpack": "^1.7.0", "matrix-react-test-utils": "^0.1.1", "mocha": "^2.4.5", - "parallelshell": "^1.2.0", + "parallelshell": "^3.0.2", "react-addons-test-utils": "^15.4.0", "require-json": "0.0.1", "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..fa9ccc8ed7 --- /dev/null +++ b/scripts/gen-i18n.js @@ -0,0 +1,268 @@ +#!/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']; + +const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json'; +const OUTPUT_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 getFormatStrings(str) { + // Match anything that starts with % + // We could make a regex that matched the full placeholder, but this + // would just not match invalid placeholders and so wouldn't help us + // detect the invalid ones. + // Also note that for simplicity, this just matches a % character and then + // anything up to the next % character (or a single %, or end of string). + const formatStringRe = /%([^%]+|%|$)/g; + const formatStrings = new Set(); + + let match; + while ( (match = formatStringRe.exec(str)) !== null ) { + const placeholder = match[1]; // Minus the leading '%' + if (placeholder === '%') continue; // Literal % is %% + + const placeholderMatch = placeholder.match(/^\((.*?)\)(.)/); + if (placeholderMatch === null) { + throw new Error("Invalid format specifier: '"+match[0]+"'"); + } + if (placeholderMatch.length < 3) { + throw new Error("Malformed format specifier"); + } + const placeholderName = placeholderMatch[1]; + const placeholderFormat = placeholderMatch[2]; + + if (placeholderFormat !== 's') { + throw new Error(`'${placeholderFormat}' used as format character: you probably meant 's'`); + } + + formatStrings.add(placeholderName); + } + + return formatStrings; +} + +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; + + // check the format string against the args + // We only check _t: _td has no args + if (node.callee.name === '_t') { + try { + const placeholders = getFormatStrings(tKey); + for (const placeholder of placeholders) { + if (node.arguments.length < 2) { + throw new Error(`Placeholder found ('${placeholder}') but no substitutions given`); + } + const value = getObjectValue(node.arguments[1], placeholder); + if (value === null) { + throw new Error(`No value found for placeholder '${placeholder}'`); + } + } + + // Validate tag replacements + if (node.arguments.length > 2) { + const tagMap = node.arguments[2]; + for (const prop of tagMap.properties) { + if (prop.key.type === 'Literal') { + const tag = prop.key.value; + // RegExp same as in src/languageHandler.js + const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`); + if (!tKey.match(regexp)) { + throw new Error(`No match for ${regexp} in ${tKey}`); + } + } + } + } + + } catch (e) { + console.log(); + console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); + process.exit(1); + } + } + + 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) { + if (tr.includes("|")) { + if (inputTranslationsRaw[tr]) { + trObj[tr] = inputTranslationsRaw[tr]; + } else { + trObj[tr] = tr.split("|")[0]; + } + } else { + trObj[tr] = tr; + } +} + +fs.writeFileSync( + OUTPUT_FILE, + JSON.stringify(trObj, translatables.values(), 4) + "\n" +); + +console.log(); +console.log(`Wrote ${translatables.size} strings to ${OUTPUT_FILE}`); diff --git a/scripts/prune-i18n.js b/scripts/prune-i18n.js new file mode 100755 index 0000000000..b4fe8d69f5 --- /dev/null +++ b/scripts/prune-i18n.js @@ -0,0 +1,68 @@ +#!/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. +*/ + +/* + * Looks through all the translation files and removes any strings + * which don't appear in en_EN.json. + * Use this if you remove a translation, but merge any outstanding changes + * from weblate first or you'll need to resolve the conflict in weblate. + */ + +const fs = require('fs'); +const path = require('path'); + +const I18NDIR = 'src/i18n/strings'; + +const enStringsRaw = JSON.parse(fs.readFileSync(path.join(I18NDIR, 'en_EN.json'))); + +const enStrings = new Set(); +for (const str of Object.keys(enStringsRaw)) { + const parts = str.split('|'); + if (parts.length > 1) { + enStrings.add(parts[0]); + } else { + enStrings.add(str); + } +} + +for (const filename of fs.readdirSync(I18NDIR)) { + if (filename === 'en_EN.json') continue; + if (filename === 'basefile.json') continue; + if (!filename.endsWith('.json')) continue; + + const trs = JSON.parse(fs.readFileSync(path.join(I18NDIR, filename))); + const oldLen = Object.keys(trs).length; + for (const tr of Object.keys(trs)) { + const parts = tr.split('|'); + const trKey = parts.length > 1 ? parts[0] : tr; + if (!enStrings.has(trKey)) { + delete trs[tr]; + } + } + + const removed = oldLen - Object.keys(trs).length; + if (removed > 0) { + console.log(`${filename}: removed ${removed} translations`); + // XXX: This is totally relying on the impl serialising the JSON object in the + // same order as they were parsed from the file. JSON.stringify() has a specific argument + // that can be used to control the order, but JSON.parse() lacks any kind of equivalent. + // Empirically this does maintain the order on my system, so I'm going to leave it like + // this for now. + fs.writeFileSync(path.join(I18NDIR, filename), JSON.stringify(trs, undefined, 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/Analytics.js b/src/Analytics.js index a82f57a144..1b4f45bc6b 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -19,7 +19,7 @@ import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; function getRedactedUrl() { - const redactedHash = window.location.hash.replace(/#\/(room|user)\/(.+)/, "#/$1/"); + const redactedHash = window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/"); // hardcoded url to make piwik happy return 'https://riot.im/app/' + redactedHash; } 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..fd56d7f1b1 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.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. @@ -52,34 +53,33 @@ limitations under the License. */ import MatrixClientPeg from './MatrixClientPeg'; -import UserSettingsStore from './UserSettingsStore'; import PlatformPeg from './PlatformPeg'; import Modal from './Modal'; import sdk from './index'; import { _t } from './languageHandler'; import Matrix from 'matrix-js-sdk'; import dis from './dispatcher'; +import { showUnknownDeviceDialogForCalls } from './cryptodevices'; 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,31 +88,65 @@ 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(); } } } +function _reAttemptCall(call) { + if (call.direction === 'outbound') { + dis.dispatch({ + action: 'place_call', + room_id: call.roomId, + type: call.type, + }); + } else { + call.answer(); + } +} + function _setCallListeners(call) { call.on("error", function(err) { console.error("Call error: %s", err); console.error(err.stack); - call.hangup(); - _setCallState(undefined, call.roomId, "ended"); - }); - call.on('send_event_error', function(err) { - if (err.name === "UnknownDeviceError") { - dis.dispatch({ - action: 'unknown_device_error', - err: err, - room: MatrixClientPeg.get().getRoom(call.roomId), + if (err.code === 'unknown_devices') { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + Modal.createTrackedDialog('Call Failed', '', QuestionDialog, { + title: _t('Call Failed'), + description: _t( + "There are unknown devices in this room: "+ + "if you proceed without verifying them, it will be "+ + "possible for someone to eavesdrop on your call." + ), + button: _t('Review Devices'), + onFinished: function(confirmed) { + if (confirmed) { + const room = MatrixClientPeg.get().getRoom(call.roomId); + showUnknownDeviceDialogForCalls( + MatrixClientPeg.get(), + room, + () => { + _reAttemptCall(call); + }, + call.direction === 'outbound' ? _t("Call Anyway") : _t("Answer Anyway"), + call.direction === 'outbound' ? _t("Call") : _t("Answer"), + ); + } + }, + }); + } else { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + + Modal.createTrackedDialog('Call Failed', '', ErrorDialog, { + title: _t('Call Failed'), + description: err.message, }); } }); @@ -125,38 +159,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 +193,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"); } @@ -189,17 +216,14 @@ function _setCallState(call, roomId, status) { function _onAction(payload) { function placeCall(newCall) { _setCallListeners(newCall); - _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 +237,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 +278,17 @@ 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), - }); + const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id); 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 +299,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 +316,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 +373,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 +384,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 +402,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 +418,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/CallMediaHandler.js b/src/CallMediaHandler.js index 839b496845..cdc5c61921 100644 --- a/src/CallMediaHandler.js +++ b/src/CallMediaHandler.js @@ -14,8 +14,8 @@ limitations under the License. */ -import UserSettingsStore from './UserSettingsStore'; import * as Matrix from 'matrix-js-sdk'; +import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; export default { getDevices: function() { @@ -43,22 +43,20 @@ export default { }, loadDevices: function() { - // this.getDevices().then((devices) => { - const localSettings = UserSettingsStore.getLocalSettings(); - // // if deviceId is not found, automatic fallback is in spec - // // recall previously stored inputs if any - Matrix.setMatrixCallAudioInput(localSettings['webrtc_audioinput']); - Matrix.setMatrixCallVideoInput(localSettings['webrtc_videoinput']); - // }); + const audioDeviceId = SettingsStore.getValue("webrtc_audioinput"); + const videoDeviceId = SettingsStore.getValue("webrtc_videoinput"); + + Matrix.setMatrixCallAudioInput(audioDeviceId); + Matrix.setMatrixCallVideoInput(videoDeviceId); }, setAudioInput: function(deviceId) { - UserSettingsStore.setLocalSetting('webrtc_audioinput', deviceId); + SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId); Matrix.setMatrixCallAudioInput(deviceId); }, setVideoInput: function(deviceId) { - UserSettingsStore.setLocalSetting('webrtc_videoinput', deviceId); + SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId); Matrix.setMatrixCallVideoInput(deviceId); }, }; diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js index 2fff3882b4..2757c5bd3d 100644 --- a/src/ComposerHistoryManager.js +++ b/src/ComposerHistoryManager.js @@ -61,7 +61,7 @@ export default class ComposerHistoryManager { // TODO: Performance issues? let item; - for(; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { + for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { this.history.push( Object.assign(new HistoryItem(), JSON.parse(item)), ); diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 93057fafed..8d40b65124 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); @@ -99,23 +99,17 @@ function loadImageElement(imageFile) { // Load the file into an html element const img = document.createElement("img"); + const objectUrl = URL.createObjectURL(imageFile); + img.src = objectUrl; - const reader = new FileReader(); - reader.onload = function(e) { - img.src = e.target.result; - - // Once ready, create a thumbnail - img.onload = function() { - deferred.resolve(img); - }; - img.onerror = function(e) { - deferred.reject(e); - }; + // Once ready, create a thumbnail + img.onload = function() { + URL.revokeObjectURL(objectUrl); + deferred.resolve(img); }; - reader.onerror = function(e) { + img.onerror = function(e) { deferred.reject(e); }; - reader.readAsDataURL(imageFile); return deferred.promise; } @@ -129,12 +123,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 +185,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 +280,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 +291,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 +304,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 +325,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 +349,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 +361,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 +370,7 @@ class ContentMessages { } if (error) { dis.dispatch({action: 'upload_failed', upload: upload}); - } - else { + } else { dis.dispatch({action: 'upload_finished', upload: upload}); } }); @@ -389,9 +382,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..ef9010cbf2 --- /dev/null +++ b/src/GroupAddressPicker.js @@ -0,0 +1,156 @@ +/* +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) => { + let addRoomsPublicly = false; + const onCheckboxClicked = (e) => { + addRoomsPublicly = e.target.checked; + }; + const description =
+
{ _t("Which rooms would you like to add to this community?") }
+
; + + const checkboxContainer = ; + + const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); + Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, { + title: _t("Add rooms to the community"), + description: description, + extraNode: checkboxContainer, + 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, addRoomsPublicly).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(", "), + }); + } + }).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, addRoomsPublicly) { + const matrixClient = MatrixClientPeg.get(); + const groupStore = GroupStoreCache.getGroupStore(groupId); + const errorList = []; + return Promise.all(addrs.map((addr) => { + return groupStore + .addRoomToGroup(addr.address, addRoomsPublicly) + .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..0c262fe89a 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,13 +202,13 @@ 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 // because transformTags is used _before_ we filter by allowedSchemesByTag and // we don't want to allow images with `https?` `src`s. - if (!attribs.src.startsWith('mxc://')) { + if (!attribs.src || !attribs.src.startsWith('mxc://')) { return { tagName, attribs: {}}; } attribs.src = MatrixClientPeg.get().mxcUrlToHttp( @@ -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/KeyCode.js b/src/Keyboard.js similarity index 77% rename from src/KeyCode.js rename to src/Keyboard.js index ec5595b71b..9c872e1c66 100644 --- a/src/KeyCode.js +++ b/src/Keyboard.js @@ -1,5 +1,6 @@ /* Copyright 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. @@ -15,7 +16,7 @@ limitations under the License. */ /* a selection of key codes, as used in KeyboardEvent.keyCode */ -module.exports = { +export const KeyCode = { BACKSPACE: 8, TAB: 9, ENTER: 13, @@ -58,3 +59,12 @@ module.exports = { KEY_Y: 89, KEY_Z: 90, }; + +export function isOnlyCtrlOrCmdKeyEvent(ev) { + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + if (isMac) { + return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey; + } else { + return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey; + } +} diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 4d8911f7a6..efd5c20d5c 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -389,6 +389,8 @@ function _persistCredentialsToLocalStorage(credentials) { * Logs the current session out and transitions to the logged-out state */ export function logout() { + if (!MatrixClientPeg.get()) return; + if (MatrixClientPeg.get().isGuest()) { // logout doesn't work for guest sessions // Also we sometimes want to re-log in a guest session @@ -436,6 +438,10 @@ function startMatrixClient() { DMRoomMap.makeShared().start(); MatrixClientPeg.start(); + + // dispatch that we finished starting up to wire up any other bits + // of the matrix client that cannot be set prior to starting up. + dis.dispatch({action: 'client_started'}); } /* diff --git a/src/Login.js b/src/Login.js index 049b79c2f4..61a14959d8 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; @@ -143,36 +143,84 @@ export default class Login { Object.assign(loginParams, legacyParams); const client = this._createTemporaryClient(); + + const tryFallbackHs = (originalError) => { + const fbClient = Matrix.createClient({ + baseUrl: self._fallbackHsUrl, + idBaseUrl: this._isUrl, + }); + + return fbClient.login('m.login.password', loginParams).then(function(data) { + return Promise.resolve({ + homeserverUrl: self._fallbackHsUrl, + identityServerUrl: self._isUrl, + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + }); + }).catch((fallback_error) => { + console.log("fallback HS login failed", fallback_error); + // throw the original error + throw originalError; + }); + }; + const tryLowercaseUsername = (originalError) => { + const loginParamsLowercase = Object.assign({}, loginParams, { + user: username.toLowerCase(), + identifier: { + user: username.toLowerCase(), + }, + }); + return client.login('m.login.password', loginParamsLowercase).then(function(data) { + return Promise.resolve({ + homeserverUrl: self._hsUrl, + identityServerUrl: self._isUrl, + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + }); + }).catch((fallback_error) => { + console.log("Lowercase username login failed", fallback_error); + // throw the original error + throw originalError; + }); + }; + + let originalLoginError = null; return client.login('m.login.password', loginParams).then(function(data) { return Promise.resolve({ homeserverUrl: self._hsUrl, identityServerUrl: self._isUrl, userId: data.user_id, deviceId: data.device_id, - accessToken: data.access_token + accessToken: data.access_token, }); - }, function(error) { + }).catch((error) => { + originalLoginError = error; if (error.httpStatus === 403) { if (self._fallbackHsUrl) { - var fbClient = Matrix.createClient({ - baseUrl: self._fallbackHsUrl, - idBaseUrl: this._isUrl, - }); - - return fbClient.login('m.login.password', loginParams).then(function(data) { - return Promise.resolve({ - homeserverUrl: self._fallbackHsUrl, - identityServerUrl: self._isUrl, - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token - }); - }, function(fallback_error) { - // throw the original error - throw error; - }); + return tryFallbackHs(originalLoginError); } } + throw originalLoginError; + }).catch((error) => { + // We apparently squash case at login serverside these days: + // https://github.com/matrix-org/synapse/blob/1189be43a2479f5adf034613e8d10e3f4f452eb9/synapse/handlers/auth.py#L475 + // so this wasn't needed after all. Keeping the code around in case the + // the situation changes... + + /* + if ( + error.httpStatus === 403 && + loginParams.identifier.type === 'm.id.user' && + username.search(/[A-Z]/) > -1 + ) { + return tryLowercaseUsername(originalLoginError); + } + */ + throw originalLoginError; + }).catch((error) => { + console.log("Login failed", error); throw 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..14dfa91fa4 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.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. @@ -21,6 +22,8 @@ import utils from 'matrix-js-sdk/lib/utils'; import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline'; import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set'; import createMatrixClient from './utils/createMatrixClient'; +import SettingsStore from './settings/SettingsStore'; +import MatrixActionCreators from './actions/MatrixActionCreators'; interface MatrixClientCreds { homeserverUrl: string, @@ -67,6 +70,8 @@ class MatrixClientPeg { unset() { this.matrixClient = null; + + MatrixActionCreators.stop(); } /** @@ -84,7 +89,7 @@ class MatrixClientPeg { if (this.matrixClient.initCrypto) { await this.matrixClient.initCrypto(); } - } catch(e) { + } catch (e) { // this can happen for a number of reasons, the most likely being // that the olm library was missing. It's not fatal. console.warn("Unable to initialise e2e: " + e); @@ -93,12 +98,13 @@ class MatrixClientPeg { const opts = utils.deepCopy(this.opts); // the react sdk doesn't work without this, so don't allow opts.pendingEventOrdering = "detached"; + opts.disablePresence = true; // we do this manually 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) { + } catch (err) { // log any errors when starting up the database (if one exists) console.error(`Error starting matrixclient store: ${err}`); } @@ -106,6 +112,9 @@ class MatrixClientPeg { // regardless of errors, start the client. If we did error out, we'll // just end up doing a full initial /sync. + // Connect the matrix client to the dispatcher + MatrixActionCreators.start(this.matrixClient); + console.log(`MatrixClientPeg: really starting MatrixClient`); this.get().startClient(opts); console.log(`MatrixClientPeg: MatrixClient started`); @@ -136,13 +145,14 @@ class MatrixClientPeg { } _createClient(creds: MatrixClientCreds) { - var opts = { + const opts = { baseUrl: creds.homeserverUrl, idBaseUrl: creds.identityServerUrl, accessToken: creds.accessToken, userId: creds.userId, deviceId: creds.deviceId, timelineSupport: true, + forceTURN: SettingsStore.getValue('webRtcForceTURN', false), }; this.matrixClient = createMatrixClient(opts, this.indexedDbWorkerScript); @@ -153,8 +163,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..75b698862c 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. @@ -24,6 +25,7 @@ import dis from './dispatcher'; import sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; +import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; /* * Dispatches: @@ -79,10 +81,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 +99,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 +118,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); @@ -137,10 +139,8 @@ const Notifier = { // make sure that we persist the current setting audio_enabled setting // before changing anything - if (global.localStorage) { - if (global.localStorage.getItem('audio_notifications_enabled') === null) { - this.setAudioEnabled(this.isEnabled()); - } + if (SettingsStore.isLevelSupported(SettingLevel.DEVICE)) { + SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, this.isEnabled()); } if (enable) { @@ -148,6 +148,7 @@ const Notifier = { plaf.requestNotificationPermission().done((result) => { if (result !== 'granted') { // The permission request was dismissed or denied + // TODO: Support alternative branding in messaging const description = result === 'denied' ? _t('Riot does not have permission to send you notifications - please check your browser settings') : _t('Riot was not given permission to send notifications - please try again'); @@ -159,10 +160,6 @@ const Notifier = { return; } - if (global.localStorage) { - global.localStorage.setItem('notifications_enabled', 'true'); - } - if (callback) callback(); dis.dispatch({ action: "notifier_enabled", @@ -173,8 +170,6 @@ const Notifier = { // disabled again in the future, we will show the banner again. this.setToolbarHidden(false); } else { - if (!global.localStorage) return; - global.localStorage.setItem('notifications_enabled', 'false'); dis.dispatch({ action: "notifier_enabled", value: false, @@ -183,31 +178,24 @@ const Notifier = { }, isEnabled: function() { + return this.isPossible() && SettingsStore.getValue("notificationsEnabled"); + }, + + isPossible: function() { const plaf = PlatformPeg.get(); if (!plaf) return false; if (!plaf.supportsNotifications()) return false; if (!plaf.maySendNotifications()) return false; - if (!global.localStorage) return true; - - const enabled = global.localStorage.getItem('notifications_enabled'); - if (enabled === null) return true; - return enabled === 'true'; + return true; // possible, but not necessarily enabled }, - setAudioEnabled: function(enable) { - if (!global.localStorage) return; - global.localStorage.setItem('audio_notifications_enabled', - enable ? 'true' : 'false'); + isBodyEnabled: function() { + return this.isEnabled() && SettingsStore.getValue("notificationBodyEnabled"); }, - isAudioEnabled: function(enable) { - if (!global.localStorage) return true; - const enabled = global.localStorage.getItem( - 'audio_notifications_enabled'); - // default to true if the popups are enabled - if (enabled === null) return this.isEnabled(); - return enabled === 'true'; + isAudioEnabled: function() { + return this.isEnabled() && SettingsStore.getValue("audioNotificationsEnabled"); }, setToolbarHidden: function(hidden, persistent = true) { @@ -224,16 +212,14 @@ const Notifier = { // update the info to localStorage for persistent settings if (persistent && global.localStorage) { - global.localStorage.setItem('notifications_hidden', hidden); + global.localStorage.setItem("notifications_hidden", hidden); } }, isToolbarHidden: function() { // Check localStorage for any such meta data if (global.localStorage) { - if (global.localStorage.getItem('notifications_hidden') === 'true') { - return true; - } + return global.localStorage.getItem("notifications_hidden") === "true"; } return this.toolbarHidden; @@ -247,12 +233,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 +289,7 @@ const Notifier = { this._playAudioNotification(ev, room); } } - } + }, }; if (!global.mxNotifier) { diff --git a/src/Presence.js b/src/Presence.js index c45d571217..2652c64c96 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 { @@ -56,13 +56,27 @@ class Presence { return this.state; } + /** + * Get the current status message. + * @returns {String} the status message, may be null + */ + getStatusMessage() { + return this.statusMessage; + } + /** * Set the presence state. * If the state has changed, the Home Server will be notified. * @param {string} newState the new presence state (see PRESENCE enum) + * @param {String} statusMessage an optional status message for the presence + * @param {boolean} maintain true to have this status maintained by this tracker */ - setState(newState) { - if (newState === this.state) { + setState(newState, statusMessage=null, maintain=false) { + if (this.maintain) { + // Don't update presence if we're maintaining a particular status + return; + } + if (newState === this.state && statusMessage === this.statusMessage) { return; } if (PRESENCE_STATES.indexOf(newState) === -1) { @@ -71,22 +85,38 @@ class Presence { if (!this.running) { return; } - var old_state = this.state; + const old_state = this.state; + const old_message = this.statusMessage; this.state = newState; + this.statusMessage = statusMessage; + this.maintain = maintain; if (MatrixClientPeg.get().isGuest()) { return; // don't try to set presence when a guest; it won't work. } - var self = this; - MatrixClientPeg.get().setPresence(this.state).done(function() { + const updateContent = { + presence: this.state, + status_msg: this.statusMessage ? this.statusMessage : '', + }; + + const self = this; + MatrixClientPeg.get().setPresence(updateContent).done(function() { console.log("Presence: %s", newState); + + // We have to dispatch because the js-sdk is unreliable at telling us about our own presence + dis.dispatch({action: "self_presence_updated", statusInfo: updateContent}); }, function(err) { console.error("Failed to set presence: %s", err); self.state = old_state; + self.statusMessage = old_message; }); } + stopMaintainingStatus() { + this.maintain = false; + } + /** * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms. * @private @@ -95,7 +125,8 @@ class Presence { this.setState("unavailable"); } - _onUserActivity() { + _onUserActivity(payload) { + if (payload.action === "sync_state" || payload.action === "self_presence_updated") return; this._resetTimer(); } @@ -104,7 +135,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/Resend.js b/src/Resend.js index 1fee5854ea..4eaee16d1b 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -44,13 +44,6 @@ module.exports = { // XXX: temporary logging to try to diagnose // https://github.com/vector-im/riot-web/issues/3148 console.log('Resend got send failure: ' + err.name + '('+err+')'); - if (err.name === "UnknownDeviceError") { - dis.dispatch({ - action: 'unknown_device_error', - err: err, - room: room, - }); - } dis.dispatch({ action: 'message_send_failed', @@ -60,9 +53,5 @@ module.exports = { }, removeFromQueue: function(event) { MatrixClientPeg.get().cancelPendingEvent(event); - dis.dispatch({ - action: 'message_send_cancelled', - event: event, - }); }, }; diff --git a/src/RichText.js b/src/RichText.js index cbd3b9ae18..12274ee9f3 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/Roles.js b/src/Roles.js index 83d8192c67..438b6c1236 100644 --- a/src/Roles.js +++ b/src/Roles.js @@ -15,19 +15,20 @@ limitations under the License. */ import { _t } from './languageHandler'; -export function levelRoleMap() { +export function levelRoleMap(usersDefault) { return { undefined: _t('Default'), - 0: _t('User'), + 0: _t('Restricted'), + [usersDefault]: _t('Default'), 50: _t('Moderator'), 100: _t('Admin'), }; } -export function textualPowerLevel(level, userDefault) { - const LEVEL_ROLE_MAP = this.levelRoleMap(); +export function textualPowerLevel(level, usersDefault) { + const LEVEL_ROLE_MAP = this.levelRoleMap(usersDefault); if (LEVEL_ROLE_MAP[level]) { - return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); + return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`); } else { return level; } diff --git a/src/Invite.js b/src/RoomInvite.js similarity index 64% rename from src/Invite.js rename to src/RoomInvite.js index b8e33d318a..1979c6d111 100644 --- a/src/Invite.js +++ b/src/RoomInvite.js @@ -21,6 +21,8 @@ import Modal from './Modal'; import { getAddressType } from './UserAddress'; import createRoom from './createRoom'; import sdk from './'; +import dis from './dispatcher'; +import DMRoomMap from './utils/DMRoomMap'; import { _t } from './languageHandler'; export function inviteToRoom(roomId, addr) { @@ -28,7 +30,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 +52,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 +63,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'), @@ -79,15 +81,40 @@ function _onStartChatFinished(shouldInvite, addrs) { const addrTexts = addrs.map((addr) => addr.address); if (_isDmChat(addrTexts)) { - // Start a new DM chat - createRoom({dmUserId: addrTexts[0]}).catch((err) => { - console.error(err.stack); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, { - title: _t("Failed to invite user"), - description: ((err && err.message) ? err.message : _t("Operation failed")), + const rooms = _getDirectMessageRooms(addrTexts[0]); + if (rooms.length > 0) { + // A Direct Message room already exists for this user, so select a + // room from a list that is similar to the one in MemberInfo panel + const ChatCreateOrReuseDialog = sdk.getComponent( + "views.dialogs.ChatCreateOrReuseDialog", + ); + const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, { + userId: addrTexts[0], + onNewDMClick: () => { + dis.dispatch({ + action: 'start_chat', + user_id: addrTexts[0], + }); + close(true); + }, + onExistingRoomSelected: (roomId) => { + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); + close(true); + }, + }).close; + } else { + // Start a new DM chat + createRoom({dmUserId: addrTexts[0]}).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, { + title: _t("Failed to invite user"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); }); - }); + } } else { // Start multi user chat let room; @@ -127,7 +154,7 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) { } function _isDmChat(addrTexts) { - if (addrTexts.length === 1 && getAddressType(addrTexts[0])) { + if (addrTexts.length === 1 && getAddressType(addrTexts[0]) === 'mx-user-id') { return true; } else { return false; @@ -153,3 +180,19 @@ function _showAnyInviteErrors(addrs, room) { return addrs; } +function _getDirectMessageRooms(addr) { + const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); + const dmRooms = dmRoomMap.getDMRoomsForUserId(addr); + const rooms = []; + dmRooms.forEach((dmRoom) => { + const room = MatrixClientPeg.get().getRoom(dmRoom); + if (room) { + const me = room.getMember(MatrixClientPeg.get().credentials.userId); + if (me.membership == 'join') { + rooms.push(room); + } + } + }); + return rooms; +} + 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..7bd8603264 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -15,10 +15,11 @@ limitations under the License. */ import Promise from 'bluebird'; -var request = require('browser-request'); +import SettingsStore from "./settings/SettingsStore"; +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 +39,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 +54,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', @@ -76,10 +77,40 @@ class ScalarAuthClient { return defer.promise; } + getScalarPageTitle(url) { + const defer = Promise.defer(); + + let scalarPageLookupUrl = SdkConfig.get().integrations_rest_url + '/widgets/title_lookup'; + scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl); + scalarPageLookupUrl += '&curl=' + encodeURIComponent(url); + request({ + method: 'GET', + uri: scalarPageLookupUrl, + json: true, + }, (err, response, body) => { + if (err) { + defer.reject(err); + } else if (response.statusCode / 100 !== 2) { + defer.reject({statusCode: response.statusCode}); + } else if (!body) { + defer.reject(new Error("Missing page title in response")); + } else { + let title = ""; + if (body.page_title_cache_item && body.page_title_cache_item.cached_title) { + title = body.page_title_cache_item.cached_title; + } + defer.resolve(title); + } + }); + + return defer.promise; + } + 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); + url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme")); if (id) { url += '&integ_id=' + encodeURIComponent(id); } diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index d14d439d66..3c164c6551 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -356,16 +356,32 @@ 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); } +function getRoomEncState(event, roomId) { + const client = MatrixClientPeg.get(); + if (!client) { + sendError(event, _t('You need to be logged in.')); + return; + } + const room = client.getRoom(roomId); + if (!room) { + sendError(event, _t('This room is not recognised.')); + return; + } + const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId); + + sendResponse(event, roomIsEncrypted); +} + function setPlumbingState(event, roomId, status) { if (typeof status !== 'string') { throw new Error('Plumbing state status should be a string'); @@ -376,7 +392,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 +431,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 +501,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 +532,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,8 +557,16 @@ 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; - if (event.origin.length === 0 || !url.startsWith(event.origin) || !event.data.action) { + // + // TODO -- Scalar postMessage API should be namespaced with event.data.api field + // Fix following "if" statement to respond only to specific API messages. + const url = SdkConfig.get().integrations_ui_url; + if ( + event.origin.length === 0 || + !url.startsWith(event.origin) || + !event.data.action || + event.data.api // Ignore messages with specific API set + ) { return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise } @@ -594,6 +617,9 @@ const onMessage = function(event) { } else if (event.data.action === "get_widgets") { getWidgets(event, roomId); return; + } else if (event.data.action === "get_room_enc_state") { + getRoomEncState(event, roomId); + return; } else if (event.data.action === "can_send_event") { canSendEvent(event, roomId); return; @@ -647,7 +673,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/SdkConfig.js b/src/SdkConfig.js index 48ebf011f2..8df725a913 100644 --- a/src/SdkConfig.js +++ b/src/SdkConfig.js @@ -26,7 +26,7 @@ const DEFAULTS = { class SdkConfig { static get() { - return global.mxReactSdkConfig; + return global.mxReactSdkConfig || {}; } static put(cfg) { 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..344bac1ddb 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -20,6 +20,7 @@ import Tinter from "./Tinter"; import sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; +import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; class Command { @@ -97,9 +98,7 @@ const commands = { colorScheme.secondary_color = matches[4]; } return success( - MatrixClientPeg.get().setRoomAccountData( - roomId, "org.matrix.room.color_scheme", colorScheme, - ), + SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme), ); } } @@ -240,6 +239,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 +344,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..1bdf5ad90c 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,82 +82,78 @@ 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) { const senderName = event.sender ? event.sender.name : _t('Someone'); const eventContent = event.getContent(); let reason = ""; - if(!MatrixClientPeg.get().supportsVoip()) { + if (!MatrixClientPeg.get().supportsVoip()) { reason = _t('(not supported by this browser)'); - } else if(eventContent.reason) { + } else if (eventContent.reason) { if (eventContent.reason === "ice_failed") { reason = _t('(could not connect media)'); } else if (eventContent.reason === "invite_timeout") { @@ -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..c7402c15be 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -1,5 +1,6 @@ /* Copyright 2015 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. @@ -14,148 +15,125 @@ See the License for the specific language governing permissions and limitations under the License. */ -// FIXME: these vars should be bundled up and attached to -// module.exports otherwise this will break when included by both -// react-sdk and apps layered on top. +const DEBUG = 0; -var DEBUG = 0; +// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue] +function colorToRgb(color) { + if (!color) { + return [0, 0, 0]; + } -// The colour keys to be replaced as referred to in CSS -var keyRgb = [ - "rgb(118, 207, 166)", // Vector Green - "rgb(234, 245, 240)", // Vector Light Green - "rgb(211, 239, 225)", // BottomLeftMenu overlay (20% Vector Green) -]; - -// Some algebra workings for calculating the tint % of Vector Green & Light Green -// x * 118 + (1 - x) * 255 = 234 -// x * 118 + 255 - 255 * x = 234 -// x * 118 - x * 255 = 234 - 255 -// (255 - 118) x = 255 - 234 -// x = (255 - 234) / (255 - 118) = 0.16 - -// The colour keys to be replaced as referred to in SVGs -var keyHex = [ - "#76CFA6", // Vector Green - "#EAF5F0", // Vector Light Green - "#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green) - "#FFFFFF", // white highlights of the SVGs (for switching to dark theme) -]; - -// cache of our replacement colours -// defaults to our keys. -var colors = [ - keyHex[0], - keyHex[1], - keyHex[2], - keyHex[3], -]; - -var 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' - // index: ordinal of primary, secondary or tertiary - // } -]; - -// CSS attributes to be fixed up -var cssAttrs = [ - "color", - "backgroundColor", - "borderColor", - "borderTopColor", - "borderBottomColor", - "borderLeftColor", -]; - -var svgAttrs = [ - "fill", - "stroke", -]; - -var cached = false; - -function calcCssFixups() { - if (DEBUG) console.log("calcSvgFixups start"); - for (var i = 0; i < document.styleSheets.length; i++) { - var ss = document.styleSheets[i]; - if (!ss) continue; // well done safari >:( - // Chromium apparently sometimes returns null here; unsure why. - // see $14534907369972FRXBx:matrix.org in HQ - // ...ah, it's because there's a third party extension like - // privacybadger inserting its own stylesheet in there with a - // resource:// URI or something which results in a XSS error. - // See also #vector:matrix.org/$145357669685386ebCfr:matrix.org - // ...except some browsers apparently return stylesheets without - // hrefs, which we have no choice but ignore right now - - // XXX seriously? we are hardcoding the name of vector's CSS file in - // here? - // - // Why do we need to limit it to vector's CSS file anyway - if there - // are other CSS files affecting the doc don't we want to apply the - // same transformations to them? - // - // Iterating through the CSS looking for matches to hack on feels - // pretty horrible anyway. And what if the application skin doesn't use - // Vector Green as its primary color? - - 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]; - if (!rule.style) continue; - for (var k = 0; k < cssAttrs.length; k++) { - var attr = cssAttrs[k]; - for (var l = 0; l < keyRgb.length; l++) { - if (rule.style[attr] === keyRgb[l]) { - cssFixups.push({ - style: rule.style, - attr: attr, - index: l, - }); - } - } - } + if (color[0] === '#') { + color = color.slice(1); + if (color.length === 3) { + color = color[0] + color[0] + + color[1] + color[1] + + color[2] + color[2]; + } + const val = parseInt(color, 16); + const r = (val >> 16) & 255; + const g = (val >> 8) & 255; + const b = val & 255; + return [r, g, b]; + } else { + const match = color.match(/rgb\((.*?),(.*?),(.*?)\)/); + if (match) { + return [ + parseInt(match[1]), + parseInt(match[2]), + parseInt(match[3]), + ]; } } - if (DEBUG) console.log("calcSvgFixups end"); + return [0, 0, 0]; } -function applyCssFixups() { - if (DEBUG) console.log("applyCssFixups start"); - for (var i = 0; i < cssFixups.length; i++) { - var cssFixup = cssFixups[i]; - cssFixup.style[cssFixup.attr] = colors[cssFixup.index]; - } - if (DEBUG) console.log("applyCssFixups end"); -} - -function hexToRgb(color) { - if (color[0] === '#') color = color.slice(1); - if (color.length === 3) { - color = color[0] + color[0] + - 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; - return [r, g, b]; -} - -function rgbToHex(rgb) { - var val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; +// utility to turn [red,green,blue] into #rrggbb +function rgbToColor(rgb) { + const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; return '#' + (0x1000000 + val).toString(16).slice(1); } -// List of functions to call when the tint changes. -const tintables = []; +class Tinter { + constructor() { + // The default colour keys to be replaced as referred to in CSS + // (should be overridden by .mx_theme_accentColor and .mx_theme_secondaryAccentColor) + this.keyRgb = [ + "rgb(118, 207, 166)", // Vector Green + "rgb(234, 245, 240)", // Vector Light Green + "rgb(211, 239, 225)", // roomsublist-label-bg-color (20% Green overlaid on Light Green) + ]; + + // Some algebra workings for calculating the tint % of Vector Green & Light Green + // x * 118 + (1 - x) * 255 = 234 + // x * 118 + 255 - 255 * x = 234 + // x * 118 - x * 255 = 234 - 255 + // (255 - 118) x = 255 - 234 + // x = (255 - 234) / (255 - 118) = 0.16 + + // The colour keys to be replaced as referred to in SVGs + this.keyHex = [ + "#76CFA6", // Vector Green + "#EAF5F0", // Vector Light Green + "#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green) + "#FFFFFF", // white highlights of the SVGs (for switching to dark theme) + "#000000", // black lowlights of the SVGs (for switching to dark theme) + ]; + + // track the replacement colours actually being used + // defaults to our keys. + this.colors = [ + this.keyHex[0], + this.keyHex[1], + this.keyHex[2], + this.keyHex[3], + this.keyHex[4], + ]; + + // track the most current tint request inputs (which may differ from the + // end result stored in this.colors + this.currentTint = [ + undefined, + undefined, + undefined, + undefined, + undefined, + ]; + + this.cssFixups = [ + // { theme: { + // style: a style object that should be fixed up taken from a stylesheet + // attr: name of the attribute to be clobbered, e.g. 'color' + // index: ordinal of primary, secondary or tertiary + // }, + // } + ]; + + // CSS attributes to be fixed up + this.cssAttrs = [ + "color", + "backgroundColor", + "borderColor", + "borderTopColor", + "borderBottomColor", + "borderLeftColor", + ]; + + this.svgAttrs = [ + "fill", + "stroke", + ]; + + // List of functions to call when the tint changes. + this.tintables = []; + + // the currently loaded theme (if any) + this.theme = undefined; + + // whether to force a tint (e.g. after changing theme) + this.forceTint = false; + } -module.exports = { /** * Register a callback to fire when the tint changes. * This is used to rewrite the tintable SVGs with the new tint. @@ -167,111 +145,273 @@ module.exports = { * * @param {Function} tintable Function to call when the tint changes. */ - registerTintable : function(tintable) { - tintables.push(tintable); - }, + registerTintable(tintable) { + this.tintables.push(tintable); + } - tint: function(primaryColor, secondaryColor, tertiaryColor) { + getKeyRgb() { + return this.keyRgb; + } - if (!cached) { - calcCssFixups(); - cached = true; + tint(primaryColor, secondaryColor, tertiaryColor) { + this.currentTint[0] = primaryColor; + this.currentTint[1] = secondaryColor; + this.currentTint[2] = tertiaryColor; + + this.calcCssFixups(); + + if (DEBUG) { + console.log("Tinter.tint(" + primaryColor + ", " + + secondaryColor + ", " + + tertiaryColor + ")"); } if (!primaryColor) { - primaryColor = "#76CFA6"; // Vector green - secondaryColor = "#EAF5F0"; // Vector light green + primaryColor = this.keyRgb[0]; + secondaryColor = this.keyRgb[1]; + tertiaryColor = this.keyRgb[2]; } if (!secondaryColor) { const x = 0.16; // average weighting factor calculated from vector green & light green - var rgb = hexToRgb(primaryColor); + const rgb = colorToRgb(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; - secondaryColor = rgbToHex(rgb); + secondaryColor = rgbToColor(rgb); } if (!tertiaryColor) { const x = 0.19; - var rgb1 = hexToRgb(primaryColor); - var rgb2 = hexToRgb(secondaryColor); + const rgb1 = colorToRgb(primaryColor); + const rgb2 = colorToRgb(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]; - tertiaryColor = rgbToHex(rgb1); + tertiaryColor = rgbToColor(rgb1); } - if (colors[0] === primaryColor && - colors[1] === secondaryColor && - colors[2] === tertiaryColor) - { + if (this.forceTint == false && + this.colors[0] === primaryColor && + this.colors[1] === secondaryColor && + this.colors[2] === tertiaryColor) { return; } - colors[0] = primaryColor; - colors[1] = secondaryColor; - colors[2] = tertiaryColor; + this.forceTint = false; - if (DEBUG) console.log("Tinter.tint"); + this.colors[0] = primaryColor; + this.colors[1] = secondaryColor; + this.colors[2] = tertiaryColor; + + if (DEBUG) { + console.log("Tinter.tint final: (" + primaryColor + ", " + + secondaryColor + ", " + + tertiaryColor + ")"); + } // go through manually fixing up the stylesheets. - applyCssFixups(); + this.applyCssFixups(); // tell all the SVGs to go fix themselves up // we don't do this as a dispatch otherwise it will visually lag - tintables.forEach(function(tintable) { + this.tintables.forEach(function(tintable) { tintable(); }); - }, + } + + tintSvgWhite(whiteColor) { + this.currentTint[3] = whiteColor; - tintSvgWhite: function(whiteColor) { if (!whiteColor) { - whiteColor = colors[3]; + whiteColor = this.colors[3]; } - if (colors[3] === whiteColor) { + if (this.colors[3] === whiteColor) { return; } - colors[3] = whiteColor; - tintables.forEach(function(tintable) { + this.colors[3] = whiteColor; + this.tintables.forEach(function(tintable) { tintable(); }); - }, + } + + tintSvgBlack(blackColor) { + this.currentTint[4] = blackColor; + + if (!blackColor) { + blackColor = this.colors[4]; + } + if (this.colors[4] === blackColor) { + return; + } + this.colors[4] = blackColor; + this.tintables.forEach(function(tintable) { + tintable(); + }); + } + + + setTheme(theme) { + console.trace("setTheme " + theme); + this.theme = theme; + + // update keyRgb from the current theme CSS itself, if it defines it + if (document.getElementById('mx_theme_accentColor')) { + this.keyRgb[0] = window.getComputedStyle( + document.getElementById('mx_theme_accentColor')).color; + } + if (document.getElementById('mx_theme_secondaryAccentColor')) { + this.keyRgb[1] = window.getComputedStyle( + document.getElementById('mx_theme_secondaryAccentColor')).color; + } + if (document.getElementById('mx_theme_tertiaryAccentColor')) { + this.keyRgb[2] = window.getComputedStyle( + document.getElementById('mx_theme_tertiaryAccentColor')).color; + } + + this.calcCssFixups(); + this.forceTint = true; + + this.tint(this.currentTint[0], this.currentTint[1], this.currentTint[2]); + + if (theme === 'dark') { + // abuse the tinter to change all the SVG's #fff to #2d2d2d + // XXX: obviously this shouldn't be hardcoded here. + this.tintSvgWhite('#2d2d2d'); + this.tintSvgBlack('#dddddd'); + } else { + this.tintSvgWhite('#ffffff'); + this.tintSvgBlack('#000000'); + } + } + + calcCssFixups() { + // cache our fixups + if (this.cssFixups[this.theme]) return; + + if (DEBUG) { + console.debug("calcCssFixups start for " + this.theme + " (checking " + + document.styleSheets.length + + " stylesheets)"); + } + + this.cssFixups[this.theme] = []; + + 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 + // ...ah, it's because there's a third party extension like + // privacybadger inserting its own stylesheet in there with a + // resource:// URI or something which results in a XSS error. + // See also #vector:matrix.org/$145357669685386ebCfr:matrix.org + // ...except some browsers apparently return stylesheets without + // hrefs, which we have no choice but ignore right now + + // XXX seriously? we are hardcoding the name of vector's CSS file in + // here? + // + // Why do we need to limit it to vector's CSS file anyway - if there + // are other CSS files affecting the doc don't we want to apply the + // same transformations to them? + // + // Iterating through the CSS looking for matches to hack on feels + // pretty horrible anyway. And what if the application skin doesn't use + // Vector Green as its primary color? + // --richvdh + + // Yes, tinting assumes that you are using the Riot skin for now. + // The right solution will be to move the CSS over to react-sdk. + // And yes, the default assets for the base skin might as well use + // Vector Green as any other colour. + // --matthew + + if (ss.href && !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue; + if (ss.disabled) continue; + if (!ss.cssRules) continue; + + if (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href); + + for (let j = 0; j < ss.cssRules.length; j++) { + const rule = ss.cssRules[j]; + if (!rule.style) continue; + if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue; + for (let k = 0; k < this.cssAttrs.length; k++) { + const attr = this.cssAttrs[k]; + for (let l = 0; l < this.keyRgb.length; l++) { + if (rule.style[attr] === this.keyRgb[l]) { + this.cssFixups[this.theme].push({ + style: rule.style, + attr: attr, + index: l, + }); + } + } + } + } + } + if (DEBUG) { + console.log("calcCssFixups end (" + + this.cssFixups[this.theme].length + + " fixups)"); + } + } + + applyCssFixups() { + if (DEBUG) { + console.log("applyCssFixups start (" + + this.cssFixups[this.theme].length + + " fixups)"); + } + for (let i = 0; i < this.cssFixups[this.theme].length; i++) { + const cssFixup = this.cssFixups[this.theme][i]; + try { + cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index]; + } catch (e) { + // Firefox Quantum explodes if you manually edit the CSS in the + // inspector and then try to do a tint, as apparently all the + // fixups are then stale. + console.error("Failed to apply cssFixup in Tinter! ", e.name); + } + } + if (DEBUG) console.log("applyCssFixups end"); + } // XXX: we could just move this all into TintableSvg, but as it's so similar // to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg) // keeping it here for now. - calcSvgFixups: function(svgs) { + calcSvgFixups(svgs) { // go through manually fixing up SVG colours. // we could do this by stylesheets, but keeping the stylesheets // updated would be a PITA, so just brute-force search for the // 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++) { - var svgDoc; + const fixups = []; + for (let i = 0; i < svgs.length; i++) { + let 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; } if (e.stack) { msg += ' | stack: ' + e.stack; } - console.error(e); + console.error(msg); } 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++) { - if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) { + const tags = svgDoc.getElementsByTagName("*"); + for (let j = 0; j < tags.length; j++) { + const tag = tags[j]; + for (let k = 0; k < this.svgAttrs.length; k++) { + const attr = this.svgAttrs[k]; + for (let l = 0; l < this.keyHex.length; l++) { + if (tag.getAttribute(attr) && + tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) { fixups.push({ node: tag, attr: attr, @@ -285,14 +425,19 @@ module.exports = { if (DEBUG) console.log("calcSvgFixups end"); return fixups; - }, + } - applySvgFixups: function(fixups) { + applySvgFixups(fixups) { if (DEBUG) console.log("applySvgFixups start for " + fixups); - for (var i = 0; i < fixups.length; i++) { - var svgFixup = fixups[i]; - svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]); + for (let i = 0; i < fixups.length; i++) { + const svgFixup = fixups[i]; + svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]); } if (DEBUG) console.log("applySvgFixups end"); } -}; +} + +if (global.singletonTinter === undefined) { + global.singletonTinter = new Tinter(); +} +export default global.singletonTinter; diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js deleted file mode 100644 index e7d77b3b66..0000000000 --- a/src/UnknownDeviceErrorHandler.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import dis from './dispatcher'; -import sdk from './index'; -import Modal from './Modal'; - -let isDialogOpen = false; - -const onAction = function(payload) { - if (payload.action === 'unknown_device_error' && !isDialogOpen) { - const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); - isDialogOpen = true; - Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, { - devices: payload.err.devices, - room: payload.room, - onFinished: (r) => { - isDialogOpen = false; - // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 - console.log('UnknownDeviceDialog closed with '+r); - }, - }, 'mx_Dialog_unknownDevice'); - } -}; - -let ref = null; - -export function startListening() { - ref = dis.register(onAction); -} - -export function stopListening() { - if (ref) { - dis.unregister(ref); - ref = null; - } -} diff --git a/src/Unread.js b/src/Unread.js index 8fffc2a429..383b5c2e5a 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -14,10 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -var MatrixClientPeg = require('./MatrixClientPeg'); -import UserSettingsStore from './UserSettingsStore'; +const MatrixClientPeg = require('./MatrixClientPeg'); import shouldHideEvent from './shouldHideEvent'; -var sdk = require('./index'); +const sdk = require('./index'); module.exports = { /** @@ -34,17 +33,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 +53,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; } @@ -65,17 +63,16 @@ module.exports = { // we have and the read receipt. We could fetch more history to try & find out, // but currently we just guess. - 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 // that counts and we can stop looking because the user's read // this and everything before. return false; - } else if (!shouldHideEvent(ev, syncedSettings) && this.eventTriggersUnreadCount(ev)) { + } else if (!shouldHideEvent(ev) && this.eventTriggersUnreadCount(ev)) { // We've found a message that counts before we hit // the read marker, so this room is definitely unread. return true; @@ -86,5 +83,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..5d2af3715f 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. @@ -16,30 +17,11 @@ limitations under the License. import Promise from 'bluebird'; import MatrixClientPeg from './MatrixClientPeg'; -import Notifier from './Notifier'; -import { _t } from './languageHandler'; /* * TODO: Make things use this. This is all WIP - see UserSettings.js for usage. */ - export default { - LABS_FEATURES: [ - { - name: "-", - id: 'matrix_apps', - default: true, - - // XXX: Always use default, ignore localStorage and remove from labs - override: true, - }, - ], - - // horrible but it works. The locality makes this somewhat more palatable. - doTranslations: function() { - this.LABS_FEATURES[0].name = _t("Matrix Apps"); - }, - loadProfileInfo: function() { const cli = MatrixClientPeg.get(); return cli.getProfileInfo(cli.credentials.userId); @@ -62,25 +44,6 @@ export default { // TODO }, - getEnableNotifications: function() { - return Notifier.isEnabled(); - }, - - setEnableNotifications: function(enable) { - if (!Notifier.supportsDesktopNotifications()) { - return; - } - Notifier.setEnabled(enable); - }, - - getEnableAudioNotifications: function() { - return Notifier.isAudioEnabled(); - }, - - setEnableAudioNotifications: function(enable) { - Notifier.setAudioEnabled(enable); - }, - changePassword: function(oldPassword, newPassword) { const cli = MatrixClientPeg.get(); @@ -127,83 +90,4 @@ export default { append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address }); }, - - getUrlPreviewsDisabled: function() { - const event = MatrixClientPeg.get().getAccountData('org.matrix.preview_urls'); - return (event && event.getContent().disable); - }, - - setUrlPreviewsDisabled: function(disabled) { - // FIXME: handle errors - return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', { - disable: disabled, - }); - }, - - getSyncedSettings: function() { - const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings'); - return event ? event.getContent() : {}; - }, - - getSyncedSetting: function(type, defaultValue = null) { - const settings = this.getSyncedSettings(); - return settings.hasOwnProperty(type) ? settings[type] : defaultValue; - }, - - setSyncedSetting: function(type, value) { - const settings = this.getSyncedSettings(); - settings[type] = value; - // FIXME: handle errors - return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings); - }, - - getLocalSettings: function() { - const localSettingsString = localStorage.getItem('mx_local_settings') || '{}'; - return JSON.parse(localSettingsString); - }, - - getLocalSetting: function(type, defaultValue = null) { - const settings = this.getLocalSettings(); - return settings.hasOwnProperty(type) ? settings[type] : defaultValue; - }, - - setLocalSetting: function(type, value) { - const settings = this.getLocalSettings(); - settings[type] = value; - // FIXME: handle errors - 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 feature = this.getFeatureById(featureId); - if (!feature) { - console.warn(`Unknown feature "${featureId}"`); - 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) { - localStorage.setItem(`mx_labs_feature_${featureId}`, enabled); - }, }; 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..0edad8d4a5 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) { @@ -62,13 +68,11 @@ module.exports = { const names = whoIsTyping.map(function(m) { return m.name; }); - if (othersCount==1) { - return _t('%(names)s and one other are typing', {names: names.slice(0, limit - 1).join(', ')}); - } else if (othersCount>1) { - return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount}); + if (othersCount>=1) { + return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount}); } else { const lastPerson = names.pop(); return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson}); } - } + }, }; diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js new file mode 100644 index 0000000000..0f23413b5f --- /dev/null +++ b/src/WidgetMessaging.js @@ -0,0 +1,326 @@ +/* +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. +*/ + +/* +Listens for incoming postMessage requests from embedded widgets. The following API is exposed: +{ + api: "widget", + action: "content_loaded", + widgetId: $WIDGET_ID, + data: {} + // additional request fields +} + +The complete request object is returned to the caller with an additional "response" key like so: +{ + api: "widget", + action: "content_loaded", + widgetId: $WIDGET_ID, + data: {}, + // additional request fields + response: { ... } +} + +The "api" field is required to use this API, and must be set to "widget" in all requests. + +The "action" determines the format of the request and response. All actions can return an error response. + +Additional data can be sent as additional, abritrary fields. However, typically the data object should be used. + +A success response is an object with zero or more keys. + +An error response is a "response" object which consists of a sole "error" key to indicate an error. +They look like: +{ + error: { + message: "Unable to invite user into room.", + _error: + } +} +The "message" key should be a human-friendly string. + +ACTIONS +======= +** All actions must include an "api" field with valie "widget".** +All actions can return an error response instead of the response outlined below. + +content_loaded +-------------- +Indicates that widget contet has fully loaded + +Request: + - widgetId is the unique ID of the widget instance in riot / matrix state. + - No additional fields. +Response: +{ + success: true +} +Example: +{ + api: "widget", + action: "content_loaded", + widgetId: $WIDGET_ID +} + + +api_version +----------- +Get the current version of the widget postMessage API + +Request: + - No additional fields. +Response: +{ + api_version: "0.0.1" +} +Example: +{ + api: "widget", + action: "api_version", +} + +supported_api_versions +---------------------- +Get versions of the widget postMessage API that are currently supported + +Request: + - No additional fields. +Response: +{ + api: "widget" + supported_versions: ["0.0.1"] +} +Example: +{ + api: "widget", + action: "supported_api_versions", +} + +*/ + +import URL from 'url'; + +const WIDGET_API_VERSION = '0.0.1'; // Current API version +const SUPPORTED_WIDGET_API_VERSIONS = [ + '0.0.1', +]; + +import dis from './dispatcher'; + +if (!global.mxWidgetMessagingListenerCount) { + global.mxWidgetMessagingListenerCount = 0; +} +if (!global.mxWidgetMessagingMessageEndpoints) { + global.mxWidgetMessagingMessageEndpoints = []; +} + + +/** + * Register widget message event listeners + */ +function startListening() { + if (global.mxWidgetMessagingListenerCount === 0) { + window.addEventListener("message", onMessage, false); + } + global.mxWidgetMessagingListenerCount += 1; +} + +/** + * De-register widget message event listeners + */ +function stopListening() { + global.mxWidgetMessagingListenerCount -= 1; + if (global.mxWidgetMessagingListenerCount === 0) { + window.removeEventListener("message", onMessage); + } + if (global.mxWidgetMessagingListenerCount < 0) { + // Make an error so we get a stack trace + const e = new Error( + "WidgetMessaging: mismatched startListening / stopListening detected." + + " Negative count", + ); + console.error(e); + } +} + +/** + * Register a widget endpoint for trusted postMessage communication + * @param {string} widgetId Unique widget identifier + * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host) + */ +function addEndpoint(widgetId, endpointUrl) { + const u = URL.parse(endpointUrl); + if (!u || !u.protocol || !u.host) { + console.warn("Invalid origin:", endpointUrl); + return; + } + + const origin = u.protocol + '//' + u.host; + const endpoint = new WidgetMessageEndpoint(widgetId, origin); + if (global.mxWidgetMessagingMessageEndpoints) { + if (global.mxWidgetMessagingMessageEndpoints.some(function(ep) { + return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl); + })) { + // Message endpoint already registered + console.warn("Endpoint already registered"); + return; + } + global.mxWidgetMessagingMessageEndpoints.push(endpoint); + } +} + +/** + * De-register a widget endpoint from trusted communication sources + * @param {string} widgetId Unique widget identifier + * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host) + * @return {boolean} True if endpoint was successfully removed + */ +function removeEndpoint(widgetId, endpointUrl) { + const u = URL.parse(endpointUrl); + if (!u || !u.protocol || !u.host) { + console.warn("Invalid origin"); + return; + } + + const origin = u.protocol + '//' + u.host; + if (global.mxWidgetMessagingMessageEndpoints && global.mxWidgetMessagingMessageEndpoints.length > 0) { + const length = global.mxWidgetMessagingMessageEndpoints.length; + global.mxWidgetMessagingMessageEndpoints = global.mxWidgetMessagingMessageEndpoints.filter(function(endpoint) { + return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin); + }); + return (length > global.mxWidgetMessagingMessageEndpoints.length); + } + return false; +} + + +/** + * Handle widget postMessage events + * @param {Event} event Event to handle + * @return {undefined} + */ +function onMessage(event) { + if (!event.origin) { // Handle chrome + event.origin = event.originalEvent.origin; + } + + // Event origin is empty string if undefined + if ( + event.origin.length === 0 || + !trustedEndpoint(event.origin) || + event.data.api !== "widget" || + !event.data.widgetId + ) { + return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise + } + + const action = event.data.action; + const widgetId = event.data.widgetId; + if (action === 'content_loaded') { + dis.dispatch({ + action: 'widget_content_loaded', + widgetId: widgetId, + }); + sendResponse(event, {success: true}); + } else if (action === 'supported_api_versions') { + sendResponse(event, { + api: "widget", + supported_versions: SUPPORTED_WIDGET_API_VERSIONS, + }); + } else if (action === 'api_version') { + sendResponse(event, { + api: "widget", + version: WIDGET_API_VERSION, + }); + } else { + console.warn("Widget postMessage event unhandled"); + sendError(event, {message: "The postMessage was unhandled"}); + } +} + +/** + * Check if message origin is registered as trusted + * @param {string} origin PostMessage origin to check + * @return {boolean} True if trusted + */ +function trustedEndpoint(origin) { + if (!origin) { + return false; + } + + return global.mxWidgetMessagingMessageEndpoints.some((endpoint) => { + return endpoint.endpointUrl === origin; + }); +} + +/** + * Send a postmessage response to a postMessage request + * @param {Event} event The original postMessage request event + * @param {Object} res Response data + */ +function sendResponse(event, res) { + const data = JSON.parse(JSON.stringify(event.data)); + data.response = res; + event.source.postMessage(data, event.origin); +} + +/** + * Send an error response to a postMessage request + * @param {Event} event The original postMessage request event + * @param {string} msg Error message + * @param {Error} nestedError Nested error event (optional) + */ +function sendError(event, msg, nestedError) { + console.error("Action:" + event.data.action + " failed with message: " + msg); + const data = JSON.parse(JSON.stringify(event.data)); + data.response = { + error: { + message: msg, + }, + }; + if (nestedError) { + data.response.error._error = nestedError; + } + event.source.postMessage(data, event.origin); +} + +/** + * Represents mapping of widget instance to URLs for trusted postMessage communication. + */ +class WidgetMessageEndpoint { + /** + * Mapping of widget instance to URL for trusted postMessage communication. + * @param {string} widgetId Unique widget identifier + * @param {string} endpointUrl Widget wurl origin. + */ + constructor(widgetId, endpointUrl) { + if (!widgetId) { + throw new Error("No widgetId specified in widgetMessageEndpoint constructor"); + } + if (!endpointUrl) { + throw new Error("No endpoint specified in widgetMessageEndpoint constructor"); + } + this.widgetId = widgetId; + this.endpointUrl = endpointUrl; + } +} + +export default { + startListening: startListening, + stopListening: stopListening, + addEndpoint: addEndpoint, + removeEndpoint: removeEndpoint, +}; diff --git a/src/actions/GroupActions.js b/src/actions/GroupActions.js new file mode 100644 index 0000000000..006c2da5b8 --- /dev/null +++ b/src/actions/GroupActions.js @@ -0,0 +1,34 @@ +/* +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 { asyncAction } from './actionCreators'; + +const GroupActions = {}; + +/** + * Creates an action thunk that will do an asynchronous request to fetch + * the groups to which a user is joined. + * + * @param {MatrixClient} matrixClient the matrix client to query. + * @returns {function} an action thunk that will dispatch actions + * indicating the status of the request. + * @see asyncAction + */ +GroupActions.fetchJoinedGroups = function(matrixClient) { + return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups()); +}; + +export default GroupActions; diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js new file mode 100644 index 0000000000..33bdb53799 --- /dev/null +++ b/src/actions/MatrixActionCreators.js @@ -0,0 +1,108 @@ +/* +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 dis from '../dispatcher'; + +// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events +// become dispatches in the same place. +/** + * Create a MatrixActions.sync action that represents a MatrixClient `sync` event, + * each parameter mapping to a key-value in the action. + * + * @param {MatrixClient} matrixClient the matrix client + * @param {string} state the current sync state. + * @param {string} prevState the previous sync state. + * @returns {Object} an action of type MatrixActions.sync. + */ +function createSyncAction(matrixClient, state, prevState) { + return { + action: 'MatrixActions.sync', + state, + prevState, + matrixClient, + }; +} + +/** + * @typedef AccountDataAction + * @type {Object} + * @property {string} action 'MatrixActions.accountData'. + * @property {MatrixEvent} event the MatrixEvent that triggered the dispatch. + * @property {string} event_type the type of the MatrixEvent, e.g. "m.direct". + * @property {Object} event_content the content of the MatrixEvent. + */ + +/** + * Create a MatrixActions.accountData action that represents a MatrixClient `accountData` + * matrix event. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {MatrixEvent} accountDataEvent the account data event. + * @returns {AccountDataAction} an action of type MatrixActions.accountData. + */ +function createAccountDataAction(matrixClient, accountDataEvent) { + return { + action: 'MatrixActions.accountData', + event: accountDataEvent, + event_type: accountDataEvent.getType(), + event_content: accountDataEvent.getContent(), + }; +} + +/** + * This object is responsible for dispatching actions when certain events are emitted by + * the given MatrixClient. + */ +export default { + // A list of callbacks to call to unregister all listeners added + _matrixClientListenersStop: [], + + /** + * Start listening to certain events from the MatrixClient and dispatch actions when + * they are emitted. + * @param {MatrixClient} matrixClient the MatrixClient to listen to events from + */ + start(matrixClient) { + this._addMatrixClientListener(matrixClient, 'sync', createSyncAction); + this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); + }, + + /** + * Start listening to events of type eventName on matrixClient and when they are emitted, + * dispatch an action created by the actionCreator function. + * @param {MatrixClient} matrixClient a MatrixClient to register a listener with. + * @param {string} eventName the event to listen to on MatrixClient. + * @param {function} actionCreator a function that should return an action to dispatch + * when given the MatrixClient as an argument as well as + * arguments emitted in the MatrixClient event. + */ + _addMatrixClientListener(matrixClient, eventName, actionCreator) { + const listener = (...args) => { + dis.dispatch(actionCreator(matrixClient, ...args)); + }; + matrixClient.on(eventName, listener); + this._matrixClientListenersStop.push(() => { + matrixClient.removeListener(eventName, listener); + }); + }, + + /** + * Stop listening to events. + */ + stop() { + this._matrixClientListenersStop.forEach((stopListener) => stopListener()); + }, +}; diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js new file mode 100644 index 0000000000..60946ea7f1 --- /dev/null +++ b/src/actions/TagOrderActions.js @@ -0,0 +1,47 @@ +/* +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 Analytics from '../Analytics'; +import { asyncAction } from './actionCreators'; +import TagOrderStore from '../stores/TagOrderStore'; + +const TagOrderActions = {}; + +/** + * Creates an action thunk that will do an asynchronous request to + * commit TagOrderStore.getOrderedTags() to account data and dispatch + * actions to indicate the status of the request. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @returns {function} an action thunk that will dispatch actions + * indicating the status of the request. + * @see asyncAction + */ +TagOrderActions.commitTagOrdering = function(matrixClient) { + return asyncAction('TagOrderActions.commitTagOrdering', () => { + // Only commit tags if the state is ready, i.e. not null + const tags = TagOrderStore.getOrderedTags(); + if (!tags) { + return; + } + + Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); + return matrixClient.setAccountData('im.vector.web.tag_ordering', {tags}); + }); +}; + +export default TagOrderActions; diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js new file mode 100644 index 0000000000..bddfbc7c63 --- /dev/null +++ b/src/actions/actionCreators.js @@ -0,0 +1,41 @@ +/* +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. +*/ + +/** + * Create an action thunk that will dispatch actions indicating the current + * status of the Promise returned by fn. + * + * @param {string} id the id to give the dispatched actions. This is given a + * suffix determining whether it is pending, successful or + * a failure. + * @param {function} fn a function that returns a Promise. + * @returns {function} an action thunk - a function that uses its single + * argument as a dispatch function to dispatch the + * following actions: + * `${id}.pending` and either + * `${id}.success` or + * `${id}.failure`. + */ +export function asyncAction(id, fn) { + return (dispatch) => { + dispatch({action: id + '.pending'}); + fn().then((result) => { + dispatch({action: id + '.success', result}); + }).catch((err) => { + dispatch({action: id + '.failure', err}); + }); + }; +} 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/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 4c7d039da4..c93ae4fb2a 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta 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. @@ -28,6 +29,10 @@ export default class AutocompleteProvider { } } + destroy() { + // stub + } + /** * Of the matched commands in the query, returns the first that contains or is contained by the selection, or null. */ diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 7a64fb154c..3d30363d9f 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -1,5 +1,6 @@ /* Copyright 2016 Aviral Dasgupta +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. @@ -22,6 +23,7 @@ import DuckDuckGoProvider from './DuckDuckGoProvider'; import RoomProvider from './RoomProvider'; import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; +import NotifProvider from './NotifProvider'; import Promise from 'bluebird'; export type SelectionRange = { @@ -43,43 +45,59 @@ const PROVIDERS = [ UserProvider, RoomProvider, EmojiProvider, + NotifProvider, CommandProvider, DuckDuckGoProvider, -].map(completer => completer.getInstance()); +]; // Providers will get rejected if they take longer than this. const PROVIDER_COMPLETION_TIMEOUT = 3000; -export async function getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { - /* Note: That this waits for all providers to return is *intentional* - otherwise, we run into a condition where new completions are displayed - while the user is interacting with the list, which makes it difficult - to predict whether an action will actually do what is intended - */ - const completionsList = await Promise.all( - // Array of inspections of promises that might timeout. Instead of allowing a - // single timeout to reject the Promise.all, reflect each one and once they've all - // settled, filter for the fulfilled ones - PROVIDERS.map((provider) => { - return provider - .getCompletions(query, selection, force) - .timeout(PROVIDER_COMPLETION_TIMEOUT) - .reflect(); - }), - ); +export default class Autocompleter { + constructor(room) { + this.room = room; + this.providers = PROVIDERS.map((p) => { + return new p(room); + }); + } - return completionsList.filter( - (inspection) => inspection.isFulfilled(), - ).map((completionsState, i) => { - return { - completions: completionsState.value(), - provider: PROVIDERS[i], + destroy() { + this.providers.forEach((p) => { + p.destroy(); + }); + } - /* the currently matched "command" the completer tried to complete - * we pass this through so that Autocomplete can figure out when to - * re-show itself once hidden. - */ - command: PROVIDERS[i].getCurrentCommand(query, selection, force), - }; - }); + async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { + /* Note: This intentionally waits for all providers to return, + otherwise, we run into a condition where new completions are displayed + while the user is interacting with the list, which makes it difficult + to predict whether an action will actually do what is intended + */ + const completionsList = await Promise.all( + // Array of inspections of promises that might timeout. Instead of allowing a + // single timeout to reject the Promise.all, reflect each one and once they've all + // settled, filter for the fulfilled ones + this.providers.map((provider) => { + return provider + .getCompletions(query, selection, force) + .timeout(PROVIDER_COMPLETION_TIMEOUT) + .reflect(); + }), + ); + + return completionsList.filter( + (inspection) => inspection.isFulfilled(), + ).map((completionsState, i) => { + return { + completions: completionsState.value(), + provider: this.providers[i], + + /* the currently matched "command" the completer tried to complete + * we pass this through so that Autocomplete can figure out when to + * re-show itself once hidden. + */ + command: this.providers[i].getCurrentCommand(query, selection, force), + }; + }); + } } diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 6f2f68b121..d47f1a161a 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,7 +17,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,80 +28,88 @@ 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 ]; const COMMAND_RE = /(^\/\w*)/g; -let instance = null; - export default class CommandProvider extends AutocompleteProvider { constructor() { super(COMMAND_RE); @@ -119,7 +128,7 @@ export default class CommandProvider extends AutocompleteProvider { component: (), range, }; @@ -132,15 +141,9 @@ export default class CommandProvider extends AutocompleteProvider { return '*️⃣ ' + _t('Commands'); } - static getInstance(): CommandProvider { - if (instance === null) instance = new CommandProvider(); - - return instance; - } - 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..68d4915f56 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta 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. @@ -25,8 +26,6 @@ import {TextualCompletion} from './Components'; const DDG_REGEX = /\/ddg\s+(.+)$/g; const REFERRER = 'vector'; -let instance = null; - export default class DuckDuckGoProvider extends AutocompleteProvider { constructor() { super(DDG_REGEX); @@ -38,7 +37,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 +46,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: ( @@ -96,16 +95,9 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { return '🔍 ' + _t('Results from DuckDuckGo'); } - static getInstance(): DuckDuckGoProvider { - if (instance == null) { - instance = new DuckDuckGoProvider(); - } - return instance; - } - renderCompletions(completions: [React.Component]): ?React.Component { return
- {completions} + { completions }
; } } diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 16e0347a5b..f4e576ea0f 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta 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. @@ -25,6 +26,7 @@ import {PillCompletion} from './Components'; import type {SelectionRange, Completion} from './Autocompleter'; import _uniq from 'lodash/uniq'; import _sortBy from 'lodash/sortBy'; +import SettingsStore from "../settings/SettingsStore"; import EmojiData from '../stripped-emoji.json'; @@ -69,8 +71,6 @@ const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sor }; }); -let instance = null; - function score(query, space) { const index = space.indexOf(query); if (index === -1) { @@ -96,6 +96,10 @@ export default class EmojiProvider extends AutocompleteProvider { } async getCompletions(query: string, selection: SelectionRange) { + if (SettingsStore.getValue("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 +137,7 @@ export default class EmojiProvider extends AutocompleteProvider { return { completion: unicode, component: ( - {unicode}} /> + { unicode }} /> ), range, }; @@ -146,15 +150,9 @@ export default class EmojiProvider extends AutocompleteProvider { return '😃 ' + _t('Emoji'); } - static getInstance() { - if (instance == null) - {instance = new EmojiProvider();} - return instance; - } - renderCompletions(completions: [React.Component]): ?React.Component { return
- {completions} + { completions }
; } } diff --git a/src/autocomplete/NotifProvider.js b/src/autocomplete/NotifProvider.js new file mode 100644 index 0000000000..b7ac645525 --- /dev/null +++ b/src/autocomplete/NotifProvider.js @@ -0,0 +1,62 @@ +/* +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 React from 'react'; +import AutocompleteProvider from './AutocompleteProvider'; +import { _t } from '../languageHandler'; +import MatrixClientPeg from '../MatrixClientPeg'; +import {PillCompletion} from './Components'; +import sdk from '../index'; + +const AT_ROOM_REGEX = /@\S*/g; + +export default class NotifProvider extends AutocompleteProvider { + constructor(room) { + super(AT_ROOM_REGEX); + this.room = room; + } + + async getCompletions(query: string, selection: {start: number, end: number}, force = false) { + const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); + + const client = MatrixClientPeg.get(); + + if (!this.room.currentState.mayTriggerNotifOfType('room', client.credentials.userId)) return []; + + const {command, range} = this.getCurrentCommand(query, selection, force); + if (command && command[0] && '@room'.startsWith(command[0]) && command[0].length > 1) { + return [{ + completion: '@room', + suffix: ' ', + component: ( + } title="@room" description={_t("Notify the whole room")} /> + ), + range, + }]; + } + return []; + } + + getName() { + return '❗️ ' + _t('Room Notification'); + } + + renderCompletions(completions: [React.Component]): ?React.Component { + return
+ { completions } +
; + } +} diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 1770089eb2..1e1928a1ee 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta 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. @@ -27,8 +28,6 @@ import _sortBy from 'lodash/sortBy'; const ROOM_REGEX = /(?=#)(\S*)/g; -let instance = null; - function score(query, space) { const index = space.indexOf(query); if (index === -1) { @@ -96,17 +95,9 @@ export default class RoomProvider extends AutocompleteProvider { return '💬 ' + _t('Rooms'); } - static getInstance() { - if (instance == null) { - instance = new RoomProvider(); - } - - return instance; - } - renderCompletions(completions: [React.Component]): ?React.Component { return
- {completions} + { completions }
; } } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 017491a07e..794f507d21 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -2,6 +2,7 @@ /* Copyright 2016 Aviral Dasgupta 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. @@ -30,19 +31,57 @@ import type {Room, RoomMember} from 'matrix-js-sdk'; const USER_REGEX = /@\S*/g; -let instance = null; - export default class UserProvider extends AutocompleteProvider { - users: Array = []; + users: Array = null; + room: Room = null; - constructor() { + constructor(room) { super(USER_REGEX, { keys: ['name'], }); + this.room = room; this.matcher = new FuzzyMatcher([], { keys: ['name', 'userId'], shouldMatchPrefix: true, }); + + this._onRoomTimelineBound = this._onRoomTimeline.bind(this); + this._onRoomStateMemberBound = this._onRoomStateMember.bind(this); + + MatrixClientPeg.get().on("Room.timeline", this._onRoomTimelineBound); + MatrixClientPeg.get().on("RoomState.members", this._onRoomStateMemberBound); + } + + destroy() { + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound); + MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound); + } + } + + _onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { + if (!room) return; + if (removed) return; + if (room.roomId !== this.room.roomId) return; + + // ignore events from filtered timelines + if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; + + // ignore anything but real-time updates at the end of the room: + // updates from pagination will happen when the paginate completes. + if (toStartOfTimeline || !data || !data.liveEvent) return; + + this.onUserSpoke(ev.sender); + } + + _onRoomStateMember(ev, state, member) { + // ignore members in other rooms + if (member.roomId !== this.room.roomId) { + return; + } + + // blow away the users cache + this.users = null; } async getCompletions(query: string, selection: {start: number, end: number}, force = false) { @@ -54,8 +93,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 +109,7 @@ export default class UserProvider extends AutocompleteProvider { href: 'https://matrix.to/#/' + user.userId, component: ( } + initialComponent={} title={displayName} description={user.userId} /> ), @@ -82,16 +124,16 @@ export default class UserProvider extends AutocompleteProvider { return '👥 ' + _t('Users'); } - setUserListFromRoom(room: Room) { - const events = room.getLiveTimeline().getEvents(); + _makeUsers() { + const events = this.room.getLiveTimeline().getEvents(); const lastSpoken = {}; - for(const event of events) { + for (const event of events) { lastSpoken[event.getSender()] = event.getTs(); } 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 +145,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( @@ -113,16 +156,9 @@ export default class UserProvider extends AutocompleteProvider { this.matcher.setObjects(this.users); } - static getInstance(): UserProvider { - if (instance == null) { - instance = new UserProvider(); - } - return instance; - } - 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..3c2308e6a7 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 @@ -33,10 +33,11 @@ module.exports = { menuHeight: React.PropTypes.number, chevronOffset: React.PropTypes.number, menuColour: React.PropTypes.string, + chevronFace: React.PropTypes.string, // top, bottom, left, right }, getOrCreateContainer: function() { - var container = document.getElementById(this.ContextualMenuContainerId); + let container = document.getElementById(this.ContextualMenuContainerId); if (!container) { container = document.createElement("div"); @@ -48,9 +49,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,47 +59,64 @@ module.exports = { } }; - var position = { - top: props.top, - }; + const position = {}; + let chevronFace = null; - var chevronOffset = {}; - if (props.chevronOffset) { + if (props.top) { + position.top = props.top; + } else { + position.bottom = props.bottom; + } + + if (props.left) { + position.left = props.left; + chevronFace = 'left'; + } else { + position.right = props.right; + chevronFace = 'right'; + } + + const chevronOffset = {}; + if (props.chevronFace) { + chevronFace = props.chevronFace; + } + if (chevronFace === 'top' || chevronFace === 'bottom') { + chevronOffset.left = props.chevronOffset; + } else { 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 { border-right-color: ${props.menuColour}; } - .mx_ContextualMenu_chevron_right:after { border-left-color: ${props.menuColour}; } + .mx_ContextualMenu_chevron_top:after { + border-left-color: ${props.menuColour}; + } + .mx_ContextualMenu_chevron_bottom:after { + border-left-color: ${props.menuColour}; + } `; } - var chevron = null; - if (props.left) { - chevron =
; - position.left = props.left; - } else { - chevron =
; - position.right = props.right; - } + const chevron =
; + const className = 'mx_ContextualMenu_wrapper'; - var className = 'mx_ContextualMenu_wrapper'; - - var menuClasses = classNames({ + const menuClasses = classNames({ 'mx_ContextualMenu': true, - 'mx_ContextualMenu_left': props.left, - 'mx_ContextualMenu_right': !props.left, + 'mx_ContextualMenu_left': chevronFace === 'left', + 'mx_ContextualMenu_right': chevronFace === 'right', + 'mx_ContextualMenu_top': chevronFace === 'top', + 'mx_ContextualMenu_bottom': chevronFace === 'bottom', }); - var menuStyle = {}; + const menuStyle = {}; if (props.menuWidth) { menuStyle.width = props.menuWidth; } @@ -113,14 +131,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 (
- +
-
-