diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 28e56e6e32..430546d281 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -1,85 +1,63 @@ # 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/UserProvider.js -src/CallHandler.js src/component-index.js -src/components/structures/ContextualMenu.js +src/components/structures/BottomLeftMenu.js +src/components/structures/CompatibilityPage.js src/components/structures/CreateRoom.js -src/components/structures/FilePanel.js -src/components/structures/InteractiveAuth.js +src/components/structures/HomePage.js +src/components/structures/LeftPanel.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/LoginBox.js src/components/structures/MessagePanel.js src/components/structures/NotificationPanel.js +src/components/structures/RoomDirectory.js src/components/structures/RoomStatusBar.js src/components/structures/RoomView.js src/components/structures/ScrollPanel.js +src/components/structures/SearchBox.js src/components/structures/TimelinePanel.js src/components/structures/UploadBar.js +src/components/structures/UserSettings.js +src/components/structures/ViewSource.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/ChangelogDialog.js src/components/views/dialogs/DeactivateAccountDialog.js -src/components/views/dialogs/InteractiveAuthDialog.js +src/components/views/dialogs/SetPasswordDialog.js src/components/views/dialogs/UnknownDeviceDialog.js -src/components/views/elements/AccessibleButton.js -src/components/views/elements/ActionButton.js +src/components/views/directory/NetworkDropdown.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/ImageView.js +src/components/views/elements/InlineSpinner.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/Spinner.js src/components/views/elements/TintableSvg.js -src/components/views/elements/TruncatedList.js +src/components/views/elements/UserInfo.js src/components/views/elements/UserSelector.js -src/components/views/login/CaptchaForm.js -src/components/views/login/CasLogin.js +src/components/views/globals/MatrixToolbar.js +src/components/views/globals/NewVersionBar.js +src/components/views/globals/UpdateCheckBar.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 src/components/views/rooms/Autocomplete.js src/components/views/rooms/AuxPanel.js src/components/views/rooms/EntityTile.js -src/components/views/rooms/EventTile.js src/components/views/rooms/LinkPreviewWidget.js src/components/views/rooms/MemberDeviceInfo.js src/components/views/rooms/MemberInfo.js @@ -87,26 +65,24 @@ 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/PresenceLabel.js -src/components/views/rooms/ReadReceiptMarker.js +src/components/views/rooms/PinnedEventTile.js +src/components/views/rooms/RoomDropTarget.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/SearchBar.js src/components/views/rooms/SearchResultTile.js src/components/views/rooms/TopUnreadMessagesBar.js src/components/views/rooms/UserTile.js src/components/views/settings/AddPhoneNumber.js 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/components/views/settings/IntegrationsManager.js +src/components/views/settings/Notifications.js src/ContentMessages.js +src/GroupAddressPicker.js src/HtmlUtils.js src/ImageUtils.js src/languageHandler.js @@ -115,39 +91,43 @@ src/Login.js src/Markdown.js src/MatrixClientPeg.js src/Modal.js +src/notifications/ContentRules.js +src/notifications/NotificationUtils.js +src/notifications/PushRuleVectorState.js +src/notifications/StandardActions.js +src/notifications/VectorPushRulesDefinitions.js src/Notifier.js src/PlatformPeg.js src/Presence.js +src/rageshake/rageshake.js +src/rageshake/submit-rageshake.js src/ratelimitedfunc.js src/RichText.js src/Roles.js src/Rooms.js src/ScalarAuthClient.js -src/ScalarMessaging.js -src/TextForEvent.js -src/Tinter.js src/UiEffects.js src/Unread.js src/utils/DecryptFile.js +src/utils/DirectoryUtils.js src/utils/DMRoomMap.js src/utils/FormattingUtils.js src/utils/MultiInviter.js src/utils/Receipt.js +src/VectorConferenceHandler.js src/Velociraptor.js src/VelocityBounce.js src/WhoIsTyping.js src/wrappers/withMatrixClient.js -test/all-tests.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/components/views/rooms/RoomSettings-test.js test/mock-clock.js -test/skinned-sdk.js +test/notifications/ContentRules-test.js +test/notifications/PushRuleVectorState-test.js test/stores/RoomViewStore-test.js -test/test-utils.js diff --git a/.eslintrc.js b/.eslintrc.js index 74790a2964..62d24ea707 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,17 +29,42 @@ 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", { + // (but we currently use them in some places) + "react/jsx-no-bind": ["warn", { "ignoreRefs": true, }], "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 + // + // Disabled for now - if anything we'd like to *enforce* spacing in JSX + // curly brackets for legibility, but in practice it's not clear that the + // consistency particularly improves legibility here. --Matthew + // + // "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, @@ -69,8 +94,8 @@ module.exports = { "valid-jsdoc": ["warn"], "new-cap": ["warn"], "key-spacing": ["warn"], - "arrow-parens": ["warn"], "prefer-const": ["warn"], + "arrow-parens": "off", // crashes currently: https://github.com/eslint/eslint/issues/6274 "generator-star-spacing": "off", diff --git a/.gitignore b/.gitignore index f828c37393..6acf1b565a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ npm-debug.log /src/component-index.js .DS_Store + +# https://github.com/vector-im/riot-web/issues/7083 +package-lock.json diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh index 87200871a5..876ce207f3 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -9,15 +9,8 @@ set -ev RIOT_WEB_DIR=riot-web REACT_SDK_DIR=`pwd` -curbranch="${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}" -echo "Determined branch to be $curbranch" - -git clone https://github.com/vector-im/riot-web.git \ - "$RIOT_WEB_DIR" - -cd "$RIOT_WEB_DIR" - -git checkout "$curbranch" || git checkout develop +scripts/fetchdep.sh vector-im riot-web +pushd "$RIOT_WEB_DIR" mkdir node_modules npm install @@ -30,4 +23,16 @@ ln -s "$REACT_SDK_DIR/node_modules/matrix-js-sdk" node_modules/matrix-js-sdk rm -r node_modules/matrix-react-sdk ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk +npm run build npm run test +popd + +# run end to end tests +git clone https://github.com/matrix-org/matrix-react-end-to-end-tests.git --branch master +pushd matrix-react-end-to-end-tests +ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web +# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh +# CHROME_PATH=$(which google-chrome-stable) ./run.sh +./install.sh +./run.sh +popd diff --git a/.travis.yml b/.travis.yml index 4137d754bf..0def6d50f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,10 @@ dist: trusty # we don't need sudo, so can run in a container, which makes startup much # quicker. -sudo: false +# +# unfortunately we do temporarily require sudo as a workaround for +# https://github.com/travis-ci/travis-ci/issues/8836 +sudo: required language: node_js node_js: @@ -12,6 +15,7 @@ addons: chrome: stable install: - npm install - - (cd node_modules/matrix-js-sdk && npm install) +# install synapse prerequisites for end to end tests + - sudo apt-get install build-essential python2.7-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev script: ./scripts/travis.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 090f5a49da..390b90a41e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,1786 @@ +Changes in [0.13.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.4) (2018-09-10) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.4-rc.1...v0.13.4) + + * No changes since rc.1 + +Changes in [0.13.4-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.4-rc.1) (2018-09-07) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.3...v0.13.4-rc.1) + + * Error on splash screen if sync is failing + [\#2155](https://github.com/matrix-org/matrix-react-sdk/pull/2155) + * Do full registration if HS doesn't support ILAG + [\#2150](https://github.com/matrix-org/matrix-react-sdk/pull/2150) + * Re-apply "Don't rely on room members to query power levels" + [\#2152](https://github.com/matrix-org/matrix-react-sdk/pull/2152) + * s/DidMount/WillMount/ in MessageComposerInput + [\#2151](https://github.com/matrix-org/matrix-react-sdk/pull/2151) + * Revert "Don't rely on room members to query power levels" + [\#2149](https://github.com/matrix-org/matrix-react-sdk/pull/2149) + * Don't rely on room members to query power levels + [\#2145](https://github.com/matrix-org/matrix-react-sdk/pull/2145) + * Correctly mark email as optional + [\#2148](https://github.com/matrix-org/matrix-react-sdk/pull/2148) + * guests trying to join communities should fire the ILAG flow. + [\#2059](https://github.com/matrix-org/matrix-react-sdk/pull/2059) + * Fix DM avatars, part 3 + [\#2146](https://github.com/matrix-org/matrix-react-sdk/pull/2146) + * Fix: show spinner again while recovering from connection error + [\#2143](https://github.com/matrix-org/matrix-react-sdk/pull/2143) + * Fix: infinite spinner on trying to create welcomeUserId room without consent + [\#2147](https://github.com/matrix-org/matrix-react-sdk/pull/2147) + * Show spinner in member list while loading members + [\#2139](https://github.com/matrix-org/matrix-react-sdk/pull/2139) + * Slash command to discard megolm session + [\#2140](https://github.com/matrix-org/matrix-react-sdk/pull/2140) + +Changes in [0.13.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.3) (2018-09-03) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.3-rc.2...v0.13.3) + + * No changes since rc.2 + +Changes in [0.13.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.3-rc.2) (2018-08-31) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.3-rc.1...v0.13.3-rc.2) + + * Update js-sdk to fix exception + +Changes in [0.13.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.3-rc.1) (2018-08-30) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.2...v0.13.3-rc.1) + + * Fix DM avatar + [\#2141](https://github.com/matrix-org/matrix-react-sdk/pull/2141) + * Update from Weblate. + [\#2142](https://github.com/matrix-org/matrix-react-sdk/pull/2142) + * Support m.room.tombstone events + [\#2124](https://github.com/matrix-org/matrix-react-sdk/pull/2124) + * Support room creation events + [\#2123](https://github.com/matrix-org/matrix-react-sdk/pull/2123) + * Support for room upgrades + [\#2122](https://github.com/matrix-org/matrix-react-sdk/pull/2122) + * Fix: dont show 1:1 avatar for rooms +2 members but only <=2 members loaded + [\#2137](https://github.com/matrix-org/matrix-react-sdk/pull/2137) + * Render terms & conditions in settings + [\#2136](https://github.com/matrix-org/matrix-react-sdk/pull/2136) + * Don't crash if the value of a room tag is null + [\#2133](https://github.com/matrix-org/matrix-react-sdk/pull/2133) + * Add stub for getVisibleRooms() + [\#2134](https://github.com/matrix-org/matrix-react-sdk/pull/2134) + * Fix LL crash trying to render own avatar in composer when member isn't + available yet + [\#2132](https://github.com/matrix-org/matrix-react-sdk/pull/2132) + * Support M_INCOMPATIBLE_ROOM_VERSION + [\#2125](https://github.com/matrix-org/matrix-react-sdk/pull/2125) + * Hide replaced rooms + [\#2127](https://github.com/matrix-org/matrix-react-sdk/pull/2127) + * Fix CPU spin on joining large room + [\#2128](https://github.com/matrix-org/matrix-react-sdk/pull/2128) + * Change format of server usage limit message + [\#2131](https://github.com/matrix-org/matrix-react-sdk/pull/2131) + * Re-apply "Fix showing peek preview while LL members are loading"" + [\#2130](https://github.com/matrix-org/matrix-react-sdk/pull/2130) + * Revert "Fix showing peek preview while LL members are loading" + [\#2129](https://github.com/matrix-org/matrix-react-sdk/pull/2129) + * Fix showing peek preview while LL members are loading + [\#2126](https://github.com/matrix-org/matrix-react-sdk/pull/2126) + * Destroy non-persistent widgets when switching room + [\#2098](https://github.com/matrix-org/matrix-react-sdk/pull/2098) + * Lazy loading of room members + [\#2118](https://github.com/matrix-org/matrix-react-sdk/pull/2118) + * Lazy loading: feature toggle + [\#2115](https://github.com/matrix-org/matrix-react-sdk/pull/2115) + * Lazy loading: cleanup + [\#2116](https://github.com/matrix-org/matrix-react-sdk/pull/2116) + * Lazy loading: fix end-to-end encryption rooms + [\#2113](https://github.com/matrix-org/matrix-react-sdk/pull/2113) + * Lazy loading: Lazy load members while backpaginating + [\#2104](https://github.com/matrix-org/matrix-react-sdk/pull/2104) + * Lazy loading: don't assume we have our own member available + [\#2102](https://github.com/matrix-org/matrix-react-sdk/pull/2102) + * Lazy load room members - Part I + [\#2072](https://github.com/matrix-org/matrix-react-sdk/pull/2072) + +Changes in [0.13.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.2) (2018-08-23) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.1...v0.13.2) + + * Don't crash if the value of a room tag is null + [\#2135](https://github.com/matrix-org/matrix-react-sdk/pull/2135) + +Changes in [0.13.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.1) (2018-08-20) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.1-rc.1...v0.13.1) + + * No changes since rc.1 + +Changes in [0.13.1-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.1-rc.1) (2018-08-16) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.0...v0.13.1-rc.1) + + * Update from Weblate. + [\#2121](https://github.com/matrix-org/matrix-react-sdk/pull/2121) + * Shift to M_RESOURCE_LIMIT_EXCEEDED errors + [\#2120](https://github.com/matrix-org/matrix-react-sdk/pull/2120) + * Fix RoomSettings test + [\#2119](https://github.com/matrix-org/matrix-react-sdk/pull/2119) + * Show room version number in room settings + [\#2117](https://github.com/matrix-org/matrix-react-sdk/pull/2117) + * Warning bar for MAU limit hit + [\#2114](https://github.com/matrix-org/matrix-react-sdk/pull/2114) + * Recognise server notices room(s) + [\#2112](https://github.com/matrix-org/matrix-react-sdk/pull/2112) + * Update room tags behaviour to match spec more + [\#2111](https://github.com/matrix-org/matrix-react-sdk/pull/2111) + * while logging out ignore `Session.logged_out` as it is intentional + [\#2058](https://github.com/matrix-org/matrix-react-sdk/pull/2058) + * Don't show 'connection lost' bar on MAU error + [\#2110](https://github.com/matrix-org/matrix-react-sdk/pull/2110) + * Support MAU error on sync + [\#2108](https://github.com/matrix-org/matrix-react-sdk/pull/2108) + * Support active user limit on message send + [\#2106](https://github.com/matrix-org/matrix-react-sdk/pull/2106) + * Run end to end tests as part of Travis build + [\#2091](https://github.com/matrix-org/matrix-react-sdk/pull/2091) + * Remove package-lock.json for now + [\#2097](https://github.com/matrix-org/matrix-react-sdk/pull/2097) + * Support montly active user limit error on /login + [\#2103](https://github.com/matrix-org/matrix-react-sdk/pull/2103) + * Unpin sanitize-html + [\#2105](https://github.com/matrix-org/matrix-react-sdk/pull/2105) + * Pin sanitize-html to 0.18.2 + [\#2101](https://github.com/matrix-org/matrix-react-sdk/pull/2101) + * Make clicking on side panels close settings (mk 3) + [\#2096](https://github.com/matrix-org/matrix-react-sdk/pull/2096) + * Fix persistent element location not updating + [\#2092](https://github.com/matrix-org/matrix-react-sdk/pull/2092) + * fix Devtools input autofocus && state traversal when len === 1 && key="" + [\#2090](https://github.com/matrix-org/matrix-react-sdk/pull/2090) + * allow autocompleting Emoji by common aliases, e.g :+1: to :thumbsup: + [\#2085](https://github.com/matrix-org/matrix-react-sdk/pull/2085) + +Changes in [0.13.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.0) (2018-07-30) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.0-rc.2...v0.13.0) + + * Fix composer bug where cursor position would change when Riot regained focus + [\#2093](https://github.com/matrix-org/matrix-react-sdk/pull/2093) + * Fix persistend element location not updating + [\#2094](https://github.com/matrix-org/matrix-react-sdk/pull/2094) + * Slate Fixes 42? + [\#2089](https://github.com/matrix-org/matrix-react-sdk/pull/2089) + +Changes in [0.13.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.0-rc.2) (2018-07-24) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.0-rc.1...v0.13.0-rc.2) + + * Take jitsi conf calling out of labs + [\#2087](https://github.com/matrix-org/matrix-react-sdk/pull/2087) + +Changes in [0.13.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.0-rc.1) (2018-07-24) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.9...v0.13.0-rc.1) + + * Update from Weblate. + [\#2086](https://github.com/matrix-org/matrix-react-sdk/pull/2086) + * Moar Slate Fixes + [\#2082](https://github.com/matrix-org/matrix-react-sdk/pull/2082) + * Destroy the widget when its permission is revoked + [\#2081](https://github.com/matrix-org/matrix-react-sdk/pull/2081) + * Make ActiveWidgetStore clear persistent widgets + [\#2084](https://github.com/matrix-org/matrix-react-sdk/pull/2084) + * CreateRoomDialog is rendered before getting the config default_federate + [\#2078](https://github.com/matrix-org/matrix-react-sdk/pull/2078) + * Slate Fixes + [\#2076](https://github.com/matrix-org/matrix-react-sdk/pull/2076) + * FIX: Don't error on rooms the user has left already + [\#2077](https://github.com/matrix-org/matrix-react-sdk/pull/2077) + * Fix persistent apps being the wrong size + [\#2080](https://github.com/matrix-org/matrix-react-sdk/pull/2080) + * Fix widgets resetting when going to the top-left + [\#2079](https://github.com/matrix-org/matrix-react-sdk/pull/2079) + * Jitsi: Use integrations URL from config + [\#2062](https://github.com/matrix-org/matrix-react-sdk/pull/2062) + * Allow jitsi in e2e rooms + [\#2075](https://github.com/matrix-org/matrix-react-sdk/pull/2075) + * Fix border around persisted widgets + [\#2071](https://github.com/matrix-org/matrix-react-sdk/pull/2071) + * Fix e2e icons floating above jitsi + [\#2073](https://github.com/matrix-org/matrix-react-sdk/pull/2073) + * hide some commands after space as they have special semantics + [\#2074](https://github.com/matrix-org/matrix-react-sdk/pull/2074) + * Even More Slate Fixes :D + [\#2070](https://github.com/matrix-org/matrix-react-sdk/pull/2070) + * Improve UX for Jitsi by adding local echo for widgets + [\#2035](https://github.com/matrix-org/matrix-react-sdk/pull/2035) + * Jitsi: Check integrations server before call + [\#2063](https://github.com/matrix-org/matrix-react-sdk/pull/2063) + * Jitsi: Error message on no permission + [\#2061](https://github.com/matrix-org/matrix-react-sdk/pull/2061) + * Fix read receipts on top of Jitsi + [\#2065](https://github.com/matrix-org/matrix-react-sdk/pull/2065) + * Moar Slate Fixes + [\#2069](https://github.com/matrix-org/matrix-react-sdk/pull/2069) + * fix 2nd typo in one PR :( + [\#2068](https://github.com/matrix-org/matrix-react-sdk/pull/2068) + * check if has some completions, not if >=0 + [\#2067](https://github.com/matrix-org/matrix-react-sdk/pull/2067) + * Slate fixes + [\#2066](https://github.com/matrix-org/matrix-react-sdk/pull/2066) + * Implement always-on-screen capability for widgets + [\#2056](https://github.com/matrix-org/matrix-react-sdk/pull/2056) + * simplify MessageComposerStore and improve its performance + [\#2064](https://github.com/matrix-org/matrix-react-sdk/pull/2064) + * Replace Draft with Slate + [\#1890](https://github.com/matrix-org/matrix-react-sdk/pull/1890) + * Fix not stopping to peek when navigating away from peeked room + [\#2055](https://github.com/matrix-org/matrix-react-sdk/pull/2055) + * T3chguy/slate cont2 + [\#2049](https://github.com/matrix-org/matrix-react-sdk/pull/2049) + * add null-guard for stickerpickerWidget in StickerPicker + [\#2057](https://github.com/matrix-org/matrix-react-sdk/pull/2057) + * Implement always-on-screen capability for widgets + [\#2053](https://github.com/matrix-org/matrix-react-sdk/pull/2053) + * fix nullguard on EventTile, getComponent never returns falsey, it throws + [\#2024](https://github.com/matrix-org/matrix-react-sdk/pull/2024) + * Fix stickerpicker PersistedElement usage + [\#2051](https://github.com/matrix-org/matrix-react-sdk/pull/2051) + * encrypt for invited users if history visibility allows. + [\#2042](https://github.com/matrix-org/matrix-react-sdk/pull/2042) + * move nag bar clear statement to any desktop notif toggle not just 0->1 + [\#2031](https://github.com/matrix-org/matrix-react-sdk/pull/2031) + * use TruncatedList to prevent rendering hundreds/thousands of DOM nodes + [\#2041](https://github.com/matrix-org/matrix-react-sdk/pull/2041) + * Fix stuff + [\#2047](https://github.com/matrix-org/matrix-react-sdk/pull/2047) + * Show m.room.server_acl + [\#2046](https://github.com/matrix-org/matrix-react-sdk/pull/2046) + +Changes in [0.12.9](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.9) (2018-07-09) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.9-rc.2...v0.12.9) + + * No changes since rc.1 + +Changes in [0.12.9-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.9-rc.2) (2018-07-06) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.9-rc.1...v0.12.9-rc.2) + + * Implement aggregation by error type for tracked decryption failures + [\#2045](https://github.com/matrix-org/matrix-react-sdk/pull/2045) + * make new hiding of roomsublist behaviour opt-in + [\#2044](https://github.com/matrix-org/matrix-react-sdk/pull/2044) + * Implement aggregation by error type for tracked decryption failures + [\#2043](https://github.com/matrix-org/matrix-react-sdk/pull/2043) + * make new hiding of roomsublist behaviour opt-in + [\#2030](https://github.com/matrix-org/matrix-react-sdk/pull/2030) + +Changes in [0.12.9-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.9-rc.1) (2018-07-04) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.8...v0.12.9-rc.1) + + * Update from Weblate. + [\#2040](https://github.com/matrix-org/matrix-react-sdk/pull/2040) + * Import react as React in src/components/views/messages/MStickerBody.js + [\#2039](https://github.com/matrix-org/matrix-react-sdk/pull/2039) + * Import react as React in src/GroupAddressPicker.js + [\#2038](https://github.com/matrix-org/matrix-react-sdk/pull/2038) + * Give PersistedElement a key + [\#2036](https://github.com/matrix-org/matrix-react-sdk/pull/2036) + * Revert " make click to insert nick work on join/parts, /me's etc" + [\#2034](https://github.com/matrix-org/matrix-react-sdk/pull/2034) + * Track an event name when tracking a decryption failure + [\#2033](https://github.com/matrix-org/matrix-react-sdk/pull/2033) + * warn on self-mute + [\#1974](https://github.com/matrix-org/matrix-react-sdk/pull/1974) + * make click to insert nick work on join/parts, /me's etc + [\#1945](https://github.com/matrix-org/matrix-react-sdk/pull/1945) + * Fix layout bug introduced by #2025 + [\#2029](https://github.com/matrix-org/matrix-react-sdk/pull/2029) + * Fix room topics/names resetting when UserSetting re-renders + [\#2028](https://github.com/matrix-org/matrix-react-sdk/pull/2028) + * Improve tracking of UISIs + [\#2027](https://github.com/matrix-org/matrix-react-sdk/pull/2027) + * Replace share icons + [\#2026](https://github.com/matrix-org/matrix-react-sdk/pull/2026) + * Improve status bar errors (namely the consent error) + [\#2025](https://github.com/matrix-org/matrix-react-sdk/pull/2025) + * Fix incorrectly positioned copy button on `
` blocks
+   [\#2023](https://github.com/matrix-org/matrix-react-sdk/pull/2023)
+ * Redact pathnames with origin `file://`
+   [\#2018](https://github.com/matrix-org/matrix-react-sdk/pull/2018)
+ * Update package-lock.json
+   [\#2022](https://github.com/matrix-org/matrix-react-sdk/pull/2022)
+ * on room sub list badge click goto first relevant room
+   [\#2021](https://github.com/matrix-org/matrix-react-sdk/pull/2021)
+ * improve linkifier AGAIN
+   [\#2020](https://github.com/matrix-org/matrix-react-sdk/pull/2020)
+ * fix historical section
+   [\#2016](https://github.com/matrix-org/matrix-react-sdk/pull/2016)
+ * Fix RoomSubList headers by re-commiting 1faecfd
+   [\#2014](https://github.com/matrix-org/matrix-react-sdk/pull/2014)
+ * don't fire share dialog when clicking timestamp of event,
+   [\#2017](https://github.com/matrix-org/matrix-react-sdk/pull/2017)
+ * Revert "affix copyButton so that it doesn't get scrolled horizontally"
+   [\#2013](https://github.com/matrix-org/matrix-react-sdk/pull/2013)
+ * when the user switches room, close room settings
+   [\#2019](https://github.com/matrix-org/matrix-react-sdk/pull/2019)
+ * Refactor widgets code
+   [\#2015](https://github.com/matrix-org/matrix-react-sdk/pull/2015)
+ * Login local errors for blank fields
+   [\#2009](https://github.com/matrix-org/matrix-react-sdk/pull/2009)
+ * Update lolex to 2.7.0
+   [\#1917](https://github.com/matrix-org/matrix-react-sdk/pull/1917)
+ * Improve Linkifier
+   [\#2011](https://github.com/matrix-org/matrix-react-sdk/pull/2011)
+ * use enum constants for EventStatus and correct isSent check
+   [\#2010](https://github.com/matrix-org/matrix-react-sdk/pull/2010)
+ * accent insensitive autocomplete
+   [\#2007](https://github.com/matrix-org/matrix-react-sdk/pull/2007)
+ * default to not showing url previews in e2ee rooms.
+   [\#2001](https://github.com/matrix-org/matrix-react-sdk/pull/2001)
+ * allow chaining right click contextmenus
+   [\#1999](https://github.com/matrix-org/matrix-react-sdk/pull/1999)
+ * hide empty roomsublists when filtering via search/tagpanel
+   [\#1954](https://github.com/matrix-org/matrix-react-sdk/pull/1954)
+ * prevent user,room,group autocomplete firing mid-word
+   [\#2012](https://github.com/matrix-org/matrix-react-sdk/pull/2012)
+ * fix instances of composer not getting/regaining focus
+   [\#2008](https://github.com/matrix-org/matrix-react-sdk/pull/2008)
+ * notif panel fixes
+   [\#2006](https://github.com/matrix-org/matrix-react-sdk/pull/2006)
+ * factor out conditional LanguageSelector as functional component
+   [\#2003](https://github.com/matrix-org/matrix-react-sdk/pull/2003)
+ * Autocomplete and Pillify Communities
+   [\#1993](https://github.com/matrix-org/matrix-react-sdk/pull/1993)
+ * Very basic Jitsi integration
+   [\#1971](https://github.com/matrix-org/matrix-react-sdk/pull/1971)
+ * add additional classes which protect the text from overflowing
+   [\#1994](https://github.com/matrix-org/matrix-react-sdk/pull/1994)
+ * Upload File confirmation modal steals focus, send it back to composer
+   [\#1992](https://github.com/matrix-org/matrix-react-sdk/pull/1992)
+ * delint MImageBody, fixes anonymous class and hyphenated style keys which
+   made react cry
+   [\#1991](https://github.com/matrix-org/matrix-react-sdk/pull/1991)
+ * allow using tab to navigate room list in a smarter way
+   [\#1977](https://github.com/matrix-org/matrix-react-sdk/pull/1977)
+ * fix no displayname usersettings
+   [\#1990](https://github.com/matrix-org/matrix-react-sdk/pull/1990)
+ * trigger TagTile context menu on right click
+   [\#1989](https://github.com/matrix-org/matrix-react-sdk/pull/1989)
+ * hide already chosen results from AddressPickerDialog
+   [\#2000](https://github.com/matrix-org/matrix-react-sdk/pull/2000)
+ * delint ChatCreateOrReuseDialog
+   [\#2002](https://github.com/matrix-org/matrix-react-sdk/pull/2002)
+ * fix set password & email flow possible to get stuck and onBlur murdering
+   your email
+   [\#1982](https://github.com/matrix-org/matrix-react-sdk/pull/1982)
+
+Changes in [0.12.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.8) (2018-06-29)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.8-rc.2...v0.12.8)
+
+ * Revert "affix copyButton so that it doesn't get scrolled horizontally"
+   [\#2013](https://github.com/matrix-org/matrix-react-sdk/pull/2013)
+ * don't fire share dialog when clicking timestamp of event
+   [\#2017](https://github.com/matrix-org/matrix-react-sdk/pull/2017)
+ * when the user switches room, close room settings
+   [\#2019](https://github.com/matrix-org/matrix-react-sdk/pull/2019)
+
+Changes in [0.12.8-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.8-rc.2) (2018-06-22)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.8-rc.1...v0.12.8-rc.2)
+
+ * slash got consumed in the consolidation
+   [\#1998](https://github.com/matrix-org/matrix-react-sdk/pull/1998)
+
+Changes in [0.12.8-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.8-rc.1) (2018-06-21)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.7...v0.12.8-rc.1)
+
+ * Update from Weblate.
+   [\#1997](https://github.com/matrix-org/matrix-react-sdk/pull/1997)
+ * refactor, consolidate and improve SlashCommands
+   [\#1988](https://github.com/matrix-org/matrix-react-sdk/pull/1988)
+ * Take replies out of labs!
+   [\#1996](https://github.com/matrix-org/matrix-react-sdk/pull/1996)
+ * re-merge reset PR
+   [\#1987](https://github.com/matrix-org/matrix-react-sdk/pull/1987)
+ * once command has a space, strict match instead of fuzzy match
+   [\#1985](https://github.com/matrix-org/matrix-react-sdk/pull/1985)
+ * Fix matrix.to URL RegExp
+   [\#1986](https://github.com/matrix-org/matrix-react-sdk/pull/1986)
+ * Fix blank sticker picker
+   [\#1984](https://github.com/matrix-org/matrix-react-sdk/pull/1984)
+ * fix e2ee file/media stuff
+   [\#1972](https://github.com/matrix-org/matrix-react-sdk/pull/1972)
+ * right click for room tile context menu
+   [\#1978](https://github.com/matrix-org/matrix-react-sdk/pull/1978)
+ * only show m.room.message in FilePanel
+   [\#1983](https://github.com/matrix-org/matrix-react-sdk/pull/1983)
+ * improve command provider
+   [\#1981](https://github.com/matrix-org/matrix-react-sdk/pull/1981)
+ * affix copyButton so that it doesn't get scrolled horizontally
+   [\#1980](https://github.com/matrix-org/matrix-react-sdk/pull/1980)
+ * split continuation if there is a gap in conversation
+   [\#1979](https://github.com/matrix-org/matrix-react-sdk/pull/1979)
+ * fix a bunch of instances of react console spam
+   [\#1973](https://github.com/matrix-org/matrix-react-sdk/pull/1973)
+ * Track decryption success/failure rate with piwik
+   [\#1949](https://github.com/matrix-org/matrix-react-sdk/pull/1949)
+ * route matrix.to/#/+... links internally (not just group ids)
+   [\#1975](https://github.com/matrix-org/matrix-react-sdk/pull/1975)
+ * implement `hitting enter after Ctrl-K should switch to the first result`
+   [\#1976](https://github.com/matrix-org/matrix-react-sdk/pull/1976)
+ * Remove tag panel feature flag
+   [\#1970](https://github.com/matrix-org/matrix-react-sdk/pull/1970)
+ * QuestionDialog pass hasCancelButton to DialogButtons
+   [\#1968](https://github.com/matrix-org/matrix-react-sdk/pull/1968)
+ * check type before msgtype in the case of `m.sticker` with msgtype
+   [\#1965](https://github.com/matrix-org/matrix-react-sdk/pull/1965)
+ * apply roomlist searchFilter to aliases if it begins with a `#`
+   [\#1957](https://github.com/matrix-org/matrix-react-sdk/pull/1957)
+ * Share Dialog
+   [\#1948](https://github.com/matrix-org/matrix-react-sdk/pull/1948)
+ * make RoomTooltip generic and add ContextMenu&Tooltip to GroupInviteTile
+   [\#1950](https://github.com/matrix-org/matrix-react-sdk/pull/1950)
+ *  Fix widgets re-appearing after being deleted
+   [\#1958](https://github.com/matrix-org/matrix-react-sdk/pull/1958)
+ * Fix crash on unspecified thumbnail info, and handle gracefully
+   [\#1967](https://github.com/matrix-org/matrix-react-sdk/pull/1967)
+ * fix styling of clearButton when its not there
+   [\#1964](https://github.com/matrix-org/matrix-react-sdk/pull/1964)
+ *  Implement slightly magical CSS soln. to thumbnail sizing
+   [\#1912](https://github.com/matrix-org/matrix-react-sdk/pull/1912)
+ * Select audio output for WebRTC
+   [\#1932](https://github.com/matrix-org/matrix-react-sdk/pull/1932)
+ * move css rule to be more generic; remove overriden rule
+   [\#1962](https://github.com/matrix-org/matrix-react-sdk/pull/1962)
+ * improve tag panel accessibility and remove a no-op dispatch
+   [\#1960](https://github.com/matrix-org/matrix-react-sdk/pull/1960)
+ * Revert "Fix exception when opening dev tools"
+   [\#1963](https://github.com/matrix-org/matrix-react-sdk/pull/1963)
+ * fix message appears unencrypted while encrypting and not_sent
+   [\#1959](https://github.com/matrix-org/matrix-react-sdk/pull/1959)
+ * Fix exception when opening dev tools
+   [\#1961](https://github.com/matrix-org/matrix-react-sdk/pull/1961)
+ * show redacted stickers like other redacted messages
+   [\#1956](https://github.com/matrix-org/matrix-react-sdk/pull/1956)
+ * add mx_filterFlipColor to mx_MemberInfo_cancel img
+   [\#1951](https://github.com/matrix-org/matrix-react-sdk/pull/1951)
+ * don't set the displayname on registration as Synapse now does it
+   [\#1953](https://github.com/matrix-org/matrix-react-sdk/pull/1953)
+ * allow CreateRoom to scale properly horizontally
+   [\#1955](https://github.com/matrix-org/matrix-react-sdk/pull/1955)
+ * Keep context menus that extend downwards vertically on screen
+   [\#1952](https://github.com/matrix-org/matrix-react-sdk/pull/1952)
+ * re-run checkIfAlone if a member change occurred in the active room
+   [\#1947](https://github.com/matrix-org/matrix-react-sdk/pull/1947)
+ * Persist pinned message open-ness between room switches
+   [\#1935](https://github.com/matrix-org/matrix-react-sdk/pull/1935)
+ * Pinned message cosmetic improvements
+   [\#1933](https://github.com/matrix-org/matrix-react-sdk/pull/1933)
+ * Update sinon to 5.0.7
+   [\#1916](https://github.com/matrix-org/matrix-react-sdk/pull/1916)
+ * re-run checkIfAlone if a member change occurred in the active room
+   [\#1946](https://github.com/matrix-org/matrix-react-sdk/pull/1946)
+ * Replace "Login as guest" with "Try the app first" on login page
+   [\#1937](https://github.com/matrix-org/matrix-react-sdk/pull/1937)
+ * kill stream when using gUM for permission to device labels to turn off
+   camera
+   [\#1931](https://github.com/matrix-org/matrix-react-sdk/pull/1931)
+
+Changes in [0.12.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.7) (2018-06-12)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.7-rc.1...v0.12.7)
+
+ * No changes since rc.1
+
+Changes in [0.12.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.7-rc.1) (2018-06-06)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.6...v0.12.7-rc.1)
+
+ * Update from Weblate.
+   [\#1944](https://github.com/matrix-org/matrix-react-sdk/pull/1944)
+ * Import react as React in src/components/views/elements/DNDTagTile.js
+   [\#1943](https://github.com/matrix-org/matrix-react-sdk/pull/1943)
+ * Fix click on faded left/right/middle panel -> close settings
+   [\#1940](https://github.com/matrix-org/matrix-react-sdk/pull/1940)
+ * Add null-guard to support browsers that don't support performance
+   [\#1942](https://github.com/matrix-org/matrix-react-sdk/pull/1942)
+ * Support third party integration managers in AppPermission
+   [\#1455](https://github.com/matrix-org/matrix-react-sdk/pull/1455)
+ * Update pinned messages in real time
+   [\#1934](https://github.com/matrix-org/matrix-react-sdk/pull/1934)
+ * Expose at-room power level setting
+   [\#1938](https://github.com/matrix-org/matrix-react-sdk/pull/1938)
+
+Changes in [0.12.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.6) (2018-05-25)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.6-rc.1...v0.12.6)
+
+ * No changes since v0.12.6-rc.1
+
+Changes in [0.12.6-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.6-rc.1) (2018-05-24)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.5...v0.12.6-rc.1)
+
+ * Add a "reload widget" button.
+   [\#1920](https://github.com/matrix-org/matrix-react-sdk/pull/1920)
+ * Make devTools styling more consistent and easier to edit event data.
+   [\#1923](https://github.com/matrix-org/matrix-react-sdk/pull/1923)
+ * Update from Weblate.
+   [\#1930](https://github.com/matrix-org/matrix-react-sdk/pull/1930)
+ * Cookie bar update
+   [\#1929](https://github.com/matrix-org/matrix-react-sdk/pull/1929)
+ * Message for leaving server notices room
+   [\#1928](https://github.com/matrix-org/matrix-react-sdk/pull/1928)
+ * More thorough check of IM URL validity.
+   [\#1927](https://github.com/matrix-org/matrix-react-sdk/pull/1927)
+ * Add usage data link to cookie bar
+   [\#1926](https://github.com/matrix-org/matrix-react-sdk/pull/1926)
+ * Change wording and appearance of Deactivate Account dialog
+   [\#1925](https://github.com/matrix-org/matrix-react-sdk/pull/1925)
+ * fix membership list ordering when presence is disabled.
+   [\#1924](https://github.com/matrix-org/matrix-react-sdk/pull/1924)
+ * Implement erasure option upon deactivation
+   [\#1922](https://github.com/matrix-org/matrix-react-sdk/pull/1922)
+ * Add cookie warning to widget warning (AppPermission)
+   [\#1921](https://github.com/matrix-org/matrix-react-sdk/pull/1921)
+ * Terms and Conditions dialog
+   [\#1919](https://github.com/matrix-org/matrix-react-sdk/pull/1919)
+ * improve privileged section users in room settings
+   [\#1902](https://github.com/matrix-org/matrix-react-sdk/pull/1902)
+ * Space between sentences in 'leave room' warning
+   [\#1918](https://github.com/matrix-org/matrix-react-sdk/pull/1918)
+ * Specify valid address types to "Start a chat" dialog
+   [\#1908](https://github.com/matrix-org/matrix-react-sdk/pull/1908)
+ * Implement opt-in analytics with cookie bar
+   [\#1906](https://github.com/matrix-org/matrix-react-sdk/pull/1906)
+ * Fix vector-im/riot-web#6523 Emoji rendering destroys paragraphs
+   [\#1910](https://github.com/matrix-org/matrix-react-sdk/pull/1910)
+
+Changes in [0.12.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.5) (2018-05-17)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.4...v0.12.5)
+
+ * Fix image size jumping regression
+   [\#1909](https://github.com/matrix-org/matrix-react-sdk/pull/1909)
+
+Changes in [0.12.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.4) (2018-05-16)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.4-rc.6...v0.12.4)
+
+ * No changes from rc.5
+
+Changes in [0.12.4-rc.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.4-rc.6) (2018-05-15)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.4-rc.5...v0.12.4-rc.6)
+
+ * Wait for deletion of widgets as well addition
+   [\#1907](https://github.com/matrix-org/matrix-react-sdk/pull/1907)
+
+Changes in [0.12.4-rc.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.4-rc.5) (2018-05-15)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.4-rc.4...v0.12.4-rc.5)
+
+ * Wait for echo from server when adding user widgets
+   [\#1905](https://github.com/matrix-org/matrix-react-sdk/pull/1905)
+
+Changes in [0.12.4-rc.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.4-rc.4) (2018-05-14)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.4-rc.3...v0.12.4-rc.4)
+
+ * Update from Weblate.
+   [\#1904](https://github.com/matrix-org/matrix-react-sdk/pull/1904)
+ * Correctly identify sticker picker widgets
+   [\#1894](https://github.com/matrix-org/matrix-react-sdk/pull/1894)
+ * Quick fix for sticker picker position
+   [\#1903](https://github.com/matrix-org/matrix-react-sdk/pull/1903)
+ * Remove redundant logging (currently shown on every render when no sti…
+   [\#1901](https://github.com/matrix-org/matrix-react-sdk/pull/1901)
+ * Fix stickers briefly being 2x the size
+   [\#1899](https://github.com/matrix-org/matrix-react-sdk/pull/1899)
+ * Send required properties when making requests to widgets over postMessage
+   [\#1891](https://github.com/matrix-org/matrix-react-sdk/pull/1891)
+ * Fix room widget second load infini spinner
+   [\#1897](https://github.com/matrix-org/matrix-react-sdk/pull/1897)
+ * Update widget state when account data changes
+   [\#1896](https://github.com/matrix-org/matrix-react-sdk/pull/1896)
+ * Remove margins when in a ReplyThread to stop them taking so much space
+   [\#1882](https://github.com/matrix-org/matrix-react-sdk/pull/1882)
+ * Add setting to enable widget screenshots (if widgets declare support)
+   [\#1892](https://github.com/matrix-org/matrix-react-sdk/pull/1892)
+ * T3chguy/replies html tag
+   [\#1889](https://github.com/matrix-org/matrix-react-sdk/pull/1889)
+ * Instant Sticker Picker
+   [\#1888](https://github.com/matrix-org/matrix-react-sdk/pull/1888)
+ * Update widget 'widgetData' key to 'data' to match spec.
+   [\#1887](https://github.com/matrix-org/matrix-react-sdk/pull/1887)
+ * Fix 'state_key' field name.
+   [\#1886](https://github.com/matrix-org/matrix-react-sdk/pull/1886)
+ * Improve appearance of short-lived app loading spinner
+   [\#1885](https://github.com/matrix-org/matrix-react-sdk/pull/1885)
+ * Take feature_sticker_messagse out of labs
+   [\#1883](https://github.com/matrix-org/matrix-react-sdk/pull/1883)
+ * Fix issue incorrect positioning with widget loading indicator
+   [\#1884](https://github.com/matrix-org/matrix-react-sdk/pull/1884)
+ * Users should always be able to edit their user/non-room  widgets
+   [\#1879](https://github.com/matrix-org/matrix-react-sdk/pull/1879)
+
+Changes in [0.12.4-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.4-rc.3) (2018-05-11)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.4-rc.2...v0.12.4-rc.3)
+
+ * Instant Sticker Picker :zap:
+   [\#1888](https://github.com/matrix-org/matrix-react-sdk/pull/1888)
+
+Changes in [0.12.4-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.4-rc.2) (2018-05-09)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.4-rc.1...v0.12.4-rc.2)
+
+ * Improve appearance of short-lived widget loading spinner
+ * Make sticker picker fully-fledged feature
+ * Fix incorrect positioning with widget loading indicator
+
+Changes in [0.12.4-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.4-rc.1) (2018-05-09)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.3...v0.12.4-rc.1)
+
+ * Update from Weblate.
+   [\#1881](https://github.com/matrix-org/matrix-react-sdk/pull/1881)
+ * Pin lolex at 2.3.2 to avoid bug causing tests to fail
+   [\#1880](https://github.com/matrix-org/matrix-react-sdk/pull/1880)
+ * Replies: un-break click-to-mention on SenderProfile for reply&preview
+   [\#1878](https://github.com/matrix-org/matrix-react-sdk/pull/1878)
+ * Add tests for RoomList
+   [\#1877](https://github.com/matrix-org/matrix-react-sdk/pull/1877)
+ * Fix crash when browser doesn't report page change measurement
+   [\#1874](https://github.com/matrix-org/matrix-react-sdk/pull/1874)
+ * fix thinko when changing from ClientPeg to context in static method (DUH)
+   [\#1875](https://github.com/matrix-org/matrix-react-sdk/pull/1875)
+ * Fix Replies :D
+   [\#1873](https://github.com/matrix-org/matrix-react-sdk/pull/1873)
+ * Update eslint-plugin-react
+   [\#1871](https://github.com/matrix-org/matrix-react-sdk/pull/1871)
+ * relax lint for jsx-curly-spacing and arrow-parens
+   [\#1872](https://github.com/matrix-org/matrix-react-sdk/pull/1872)
+ * Use develop js-sdk in jenkins build
+   [\#1870](https://github.com/matrix-org/matrix-react-sdk/pull/1870)
+ * Replies
+   [\#1741](https://github.com/matrix-org/matrix-react-sdk/pull/1741)
+ * Use the right js-sdk branch when testing
+   [\#1869](https://github.com/matrix-org/matrix-react-sdk/pull/1869)
+ * Prevent error responses wedging group request concurrency limit
+   [\#1867](https://github.com/matrix-org/matrix-react-sdk/pull/1867)
+ * Refresh group rooms and members when selecting a tag
+   [\#1868](https://github.com/matrix-org/matrix-react-sdk/pull/1868)
+ * Refactor GroupStores into one global GroupStore
+   [\#1866](https://github.com/matrix-org/matrix-react-sdk/pull/1866)
+ * Switch back to using blob URLs for rendering e2e attachments
+   [\#1864](https://github.com/matrix-org/matrix-react-sdk/pull/1864)
+ * Hide inline encryption icons except when hovering over a message
+   [\#1845](https://github.com/matrix-org/matrix-react-sdk/pull/1845)
+ *  UI fixes in SessionRestoreErrorDialog
+   [\#1860](https://github.com/matrix-org/matrix-react-sdk/pull/1860)
+ *  Fix UX issues with bug report dialog
+   [\#1863](https://github.com/matrix-org/matrix-react-sdk/pull/1863)
+ * fix ugly img errors and correctly render SVG thumbnails
+   [\#1865](https://github.com/matrix-org/matrix-react-sdk/pull/1865)
+ * Fix error handling on session restore
+   [\#1859](https://github.com/matrix-org/matrix-react-sdk/pull/1859)
+ * Add tests for GroupView
+   [\#1862](https://github.com/matrix-org/matrix-react-sdk/pull/1862)
+ * Update version of hoek
+   [\#1861](https://github.com/matrix-org/matrix-react-sdk/pull/1861)
+ *  Fix bug that caused crash when analytics HS/IS whitelists not specified
+   [\#1858](https://github.com/matrix-org/matrix-react-sdk/pull/1858)
+ * Fix Analytics to not import DEFAULTS, therefore avoiding NPE
+   [\#1857](https://github.com/matrix-org/matrix-react-sdk/pull/1857)
+ * Null check piwik config before using it
+   [\#1856](https://github.com/matrix-org/matrix-react-sdk/pull/1856)
+ * Track actual window location origin and hash
+   [\#1853](https://github.com/matrix-org/matrix-react-sdk/pull/1853)
+ * Replace document.origin with window.location.origin
+   [\#1855](https://github.com/matrix-org/matrix-react-sdk/pull/1855)
+ * Optionally hide widget popout button.
+   [\#1854](https://github.com/matrix-org/matrix-react-sdk/pull/1854)
+ * Add a button to 'pop out' widgets in to their own tab.
+   [\#1851](https://github.com/matrix-org/matrix-react-sdk/pull/1851)
+
+Changes in [0.12.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.3) (2018-04-30)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.3-rc.3...v0.12.3)
+
+ * No changes since rc.3
+
+Changes in [0.12.3-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.3-rc.3) (2018-04-26)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.3-rc.2...v0.12.3-rc.3)
+
+ * Replace document.origin with window.location.origin
+   [\#1855](https://github.com/matrix-org/matrix-react-sdk/pull/1855)
+
+Changes in [0.12.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.3-rc.2) (2018-04-25)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.3-rc.1...v0.12.3-rc.2)
+
+ * Fix npm packaging
+
+Changes in [0.12.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.3-rc.1) (2018-04-25)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.2...v0.12.3-rc.1)
+
+ * Update from Weblate.
+   [\#1852](https://github.com/matrix-org/matrix-react-sdk/pull/1852)
+ * Support origin lock in cross-origin renderer
+   [\#1849](https://github.com/matrix-org/matrix-react-sdk/pull/1849)
+ * s/contian/contain/g
+   [\#1850](https://github.com/matrix-org/matrix-react-sdk/pull/1850)
+ * Don't autocomplete users for single "@"
+   [\#1848](https://github.com/matrix-org/matrix-react-sdk/pull/1848)
+ * Update from Weblate.
+   [\#1844](https://github.com/matrix-org/matrix-react-sdk/pull/1844)
+ * Bind onImageError in constructor
+   [\#1846](https://github.com/matrix-org/matrix-react-sdk/pull/1846)
+ * Use mxid as sender name on set display name
+   [\#1841](https://github.com/matrix-org/matrix-react-sdk/pull/1841)
+ * Fix rageshake
+   [\#1840](https://github.com/matrix-org/matrix-react-sdk/pull/1840)
+ * Add UI for displaying room avatars full size
+   [\#1843](https://github.com/matrix-org/matrix-react-sdk/pull/1843)
+ * Update from Weblate.
+   [\#1842](https://github.com/matrix-org/matrix-react-sdk/pull/1842)
+ * move everything not explicitly riot (or status) branded into matrix-react-
+   sdk
+   [\#1836](https://github.com/matrix-org/matrix-react-sdk/pull/1836)
+ * Null check node before we pass it to velocity
+   [\#1838](https://github.com/matrix-org/matrix-react-sdk/pull/1838)
+ * Remove presence management
+   [\#1676](https://github.com/matrix-org/matrix-react-sdk/pull/1676)
+ * Null check stylesheet href
+   [\#1835](https://github.com/matrix-org/matrix-react-sdk/pull/1835)
+ * TopUnreadMessagesBar a11y
+   [\#1819](https://github.com/matrix-org/matrix-react-sdk/pull/1819)
+ * Use correct 1-1 room avatar after users leave
+   [\#593](https://github.com/matrix-org/matrix-react-sdk/pull/593)
+ * Use GeminiScrollbarWrapper in Flair settings of UserSettings
+   [\#1833](https://github.com/matrix-org/matrix-react-sdk/pull/1833)
+ * Add 500ms delay to show `membershipBusy` for longer
+   [\#1832](https://github.com/matrix-org/matrix-react-sdk/pull/1832)
+ * Improve group join/leave feedback
+   [\#1831](https://github.com/matrix-org/matrix-react-sdk/pull/1831)
+ * Update from Weblate.
+   [\#1830](https://github.com/matrix-org/matrix-react-sdk/pull/1830)
+ * Bump source-map-loader version to avoid bug /w inline base64 maps
+   [\#1829](https://github.com/matrix-org/matrix-react-sdk/pull/1829)
+ * Make stickers/messages continuations of each other
+   [\#1828](https://github.com/matrix-org/matrix-react-sdk/pull/1828)
+ * Update to match is_openly_joinable API
+   [\#1827](https://github.com/matrix-org/matrix-react-sdk/pull/1827)
+ * Fix to prevent guests from seeing features
+   [\#1826](https://github.com/matrix-org/matrix-react-sdk/pull/1826)
+ * Fix broken ForgotPassword component
+   [\#1825](https://github.com/matrix-org/matrix-react-sdk/pull/1825)
+ * Fix warning "Unknown prop `wrappedRef` on 
tag..." + [\#1824](https://github.com/matrix-org/matrix-react-sdk/pull/1824) + * Add radio button for setting group is_joinable + [\#1817](https://github.com/matrix-org/matrix-react-sdk/pull/1817) + * Fix widget grant / revoke permission binding + [\#1823](https://github.com/matrix-org/matrix-react-sdk/pull/1823) + * Sticker picker styling + [\#1822](https://github.com/matrix-org/matrix-react-sdk/pull/1822) + * Bi-directional widget postMessaging API (stickerpacks) [WIP] + [\#1672](https://github.com/matrix-org/matrix-react-sdk/pull/1672) + * Add null-guard to prevent RoomAvatar NPE when room is null + [\#1821](https://github.com/matrix-org/matrix-react-sdk/pull/1821) + * Don't notify for bad encrypted messages + [\#1818](https://github.com/matrix-org/matrix-react-sdk/pull/1818) + * Join this community button + [\#1815](https://github.com/matrix-org/matrix-react-sdk/pull/1815) + * Reword group setting delay + [\#1816](https://github.com/matrix-org/matrix-react-sdk/pull/1816) + * Track duration of page changes + [\#1814](https://github.com/matrix-org/matrix-react-sdk/pull/1814) + * Wrap GeminiScrollbar in a component, enabled forceGemini + [\#1810](https://github.com/matrix-org/matrix-react-sdk/pull/1810) + * Add display name to the read receipt view + [\#1742](https://github.com/matrix-org/matrix-react-sdk/pull/1742) + * Fix broken import preventing people tag + [\#1811](https://github.com/matrix-org/matrix-react-sdk/pull/1811) + * Add /devtools to Autocomplete and run gen-i18n + [\#1778](https://github.com/matrix-org/matrix-react-sdk/pull/1778) + * Fix PresenceLabel in MemberInfo + [\#1809](https://github.com/matrix-org/matrix-react-sdk/pull/1809) + * Fix room tile badge not disappearing when receiving a read receipt + [\#1807](https://github.com/matrix-org/matrix-react-sdk/pull/1807) + * Option to remove the presence feature by HS + [\#1806](https://github.com/matrix-org/matrix-react-sdk/pull/1806) + * Dialog a11y + [\#1652](https://github.com/matrix-org/matrix-react-sdk/pull/1652) + * Change wording of debug log submission + [\#1740](https://github.com/matrix-org/matrix-react-sdk/pull/1740) + * Fix TextualBody.js to remove NodeList.forEach() + [\#1768](https://github.com/matrix-org/matrix-react-sdk/pull/1768) + * Use undocumented piwik cmd to disable heartbeattimer + [\#1770](https://github.com/matrix-org/matrix-react-sdk/pull/1770) + * Enable autocompletion for non-English languages. + [\#1800](https://github.com/matrix-org/matrix-react-sdk/pull/1800) + +Changes in [0.12.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.2) (2018-04-12) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.1...v0.12.2) + + * Null check stylesheet href + [\#1835](https://github.com/matrix-org/matrix-react-sdk/pull/1835) + * Remove the presence management labs feature + +Changes in [0.12.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.1) (2018-04-11) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0...v0.12.1) + + * Use correct js-sdk version + +Changes in [0.12.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0) (2018-04-11) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.7...v0.12.0) + + * Further improve group joining/leaving feedback + [\#1832](https://github.com/matrix-org/matrix-react-sdk/pull/1832) + * Cosmetic changes to Communities button + +Changes in [0.12.0-rc.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.7) (2018-04-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.6...v0.12.0-rc.7) + + * Reword group setting delay + [\#1816](https://github.com/matrix-org/matrix-react-sdk/pull/1816) + * Improve group joining/leaving feedback + [\#1831](https://github.com/matrix-org/matrix-react-sdk/pull/1831) + +Changes in [0.12.0-rc.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.6) (2018-04-09) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.5...v0.12.0-rc.6) + + * Fix group join button not appearing + +Changes in [0.12.0-rc.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.5) (2018-04-09) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.4...v0.12.0-rc.5) + + * Added radio button to set group join policy + * Fix to prevent guests from accessing lab features + * Fix broken forgot password page + * Fix crash when joining a room after peeking + +Changes in [0.12.0-rc.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.4) (2018-03-22) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.3...v0.12.0-rc.4) + + * Fix broken import preventing people tag + [\#1811](https://github.com/matrix-org/matrix-react-sdk/pull/1811) + +Changes in [0.12.0-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.3) (2018-03-20) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.2...v0.12.0-rc.3) + + * Fix room tile badge not disappearing when receiving a read receipt + [\#1807](https://github.com/matrix-org/matrix-react-sdk/pull/1807) + +Changes in [0.12.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.2) (2018-03-19) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.1...v0.12.0-rc.2) + + * Take TagPanel out of labs + [\#1805](https://github.com/matrix-org/matrix-react-sdk/pull/1805) + +Changes in [0.12.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.1) (2018-03-19) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.4...v0.12.0-rc.1) + + * Remove the message on migrating crypto data + [\#1803](https://github.com/matrix-org/matrix-react-sdk/pull/1803) + * Update from Weblate. + [\#1804](https://github.com/matrix-org/matrix-react-sdk/pull/1804) + * Improve room list performance when receiving messages + [\#1801](https://github.com/matrix-org/matrix-react-sdk/pull/1801) + * Add change delay warning in GroupView settings + [\#1802](https://github.com/matrix-org/matrix-react-sdk/pull/1802) + * Only use `dangerouslySetInnerHTML` for HTML messages + [\#1799](https://github.com/matrix-org/matrix-react-sdk/pull/1799) + * Limit group requests to 3 at once + [\#1798](https://github.com/matrix-org/matrix-react-sdk/pull/1798) + * Show GroupMemberList after inviting a group member + [\#1796](https://github.com/matrix-org/matrix-react-sdk/pull/1796) + * Fix syntax fail + [\#1794](https://github.com/matrix-org/matrix-react-sdk/pull/1794) + * Use TintableSvg for TagPanel clear filter button + [\#1793](https://github.com/matrix-org/matrix-react-sdk/pull/1793) + * Fix missing space between "...is a" and user ID + [\#1792](https://github.com/matrix-org/matrix-react-sdk/pull/1792) + * E2E "fudge-button" + [\#1791](https://github.com/matrix-org/matrix-react-sdk/pull/1791) + * Remove spurious console.trace + [\#1790](https://github.com/matrix-org/matrix-react-sdk/pull/1790) + * Don't reset the presence timer on every dispatch + [\#1789](https://github.com/matrix-org/matrix-react-sdk/pull/1789) + * Potentially fix a memory leak in FlairStore + [\#1788](https://github.com/matrix-org/matrix-react-sdk/pull/1788) + * Implement transparent RoomTile for use in some places + [\#1785](https://github.com/matrix-org/matrix-react-sdk/pull/1785) + * Fix varying default group avatar colour for given group + [\#1784](https://github.com/matrix-org/matrix-react-sdk/pull/1784) + * Fix bug where avatar change not reflected in LLP + [\#1783](https://github.com/matrix-org/matrix-react-sdk/pull/1783) + * Workaround for atlassian/react-beautiful-dnd#273 + [\#1782](https://github.com/matrix-org/matrix-react-sdk/pull/1782) + * Add setting to disable TagPanel + [\#1781](https://github.com/matrix-org/matrix-react-sdk/pull/1781) + * [DO NOT MERGE] Tests proven to fail + [\#1780](https://github.com/matrix-org/matrix-react-sdk/pull/1780) + * Fix room power level settings + [\#1779](https://github.com/matrix-org/matrix-react-sdk/pull/1779) + * fix shouldHideEvent saying an event is a leave/join when a profile ch… + [\#1769](https://github.com/matrix-org/matrix-react-sdk/pull/1769) + * Add "Did you know:..." microcopy to groups view + [\#1777](https://github.com/matrix-org/matrix-react-sdk/pull/1777) + * Give emptySubListTip a container for correct bg colour + [\#1753](https://github.com/matrix-org/matrix-react-sdk/pull/1753) + * Do proper null-checks on decypted events to fix NPEs + [\#1776](https://github.com/matrix-org/matrix-react-sdk/pull/1776) + * Reorder the RoomListStore lists on Event.decrypted + [\#1775](https://github.com/matrix-org/matrix-react-sdk/pull/1775) + * Fix bug where global "Never send to unverified..." is ignored + [\#1772](https://github.com/matrix-org/matrix-react-sdk/pull/1772) + * Fix bug that prevented tint updates + [\#1767](https://github.com/matrix-org/matrix-react-sdk/pull/1767) + * Fix group member spinner being out of flex order + [\#1765](https://github.com/matrix-org/matrix-react-sdk/pull/1765) + * Allow widget iframes to request camera and microphone permissions. + [\#1766](https://github.com/matrix-org/matrix-react-sdk/pull/1766) + * Change icon from "R" to "X" + [\#1764](https://github.com/matrix-org/matrix-react-sdk/pull/1764) + * Regenerate room lists on Room event + [\#1762](https://github.com/matrix-org/matrix-react-sdk/pull/1762) + * Fix DMs being marked as with the current user ("me") + [\#1761](https://github.com/matrix-org/matrix-react-sdk/pull/1761) + * Make RoomListStore aware of Room.timeline events + [\#1756](https://github.com/matrix-org/matrix-react-sdk/pull/1756) + * improve origin check of ScalarMessaging postmessage API. + [\#1760](https://github.com/matrix-org/matrix-react-sdk/pull/1760) + * Implement global filter to deselect all tags + [\#1759](https://github.com/matrix-org/matrix-react-sdk/pull/1759) + * Don't show empty custom tags when filtering tags + [\#1758](https://github.com/matrix-org/matrix-react-sdk/pull/1758) + * Do not assume that tags have been removed + [\#1757](https://github.com/matrix-org/matrix-react-sdk/pull/1757) + * Change CSS class for message panel spinner + [\#1747](https://github.com/matrix-org/matrix-react-sdk/pull/1747) + * Remove RoomListStore listener + [\#1752](https://github.com/matrix-org/matrix-react-sdk/pull/1752) + * Implement GroupTile avatar dragging to TagPanel + [\#1751](https://github.com/matrix-org/matrix-react-sdk/pull/1751) + * Fix custom tags not being ordered manually + [\#1750](https://github.com/matrix-org/matrix-react-sdk/pull/1750) + * Store component state for editors + [\#1746](https://github.com/matrix-org/matrix-react-sdk/pull/1746) + * Give the login page its spinner back + [\#1745](https://github.com/matrix-org/matrix-react-sdk/pull/1745) + * Add context menu to TagTile + [\#1743](https://github.com/matrix-org/matrix-react-sdk/pull/1743) + * If a tag is unrecognised, assume manual ordering + [\#1748](https://github.com/matrix-org/matrix-react-sdk/pull/1748) + * Move RoomList state to RoomListStore + [\#1719](https://github.com/matrix-org/matrix-react-sdk/pull/1719) + * Move groups button to TagPanel + [\#1744](https://github.com/matrix-org/matrix-react-sdk/pull/1744) + * Add seconds to timestamp on hover + [\#1738](https://github.com/matrix-org/matrix-react-sdk/pull/1738) + * Do not truncate autocompleted users in composer + [\#1739](https://github.com/matrix-org/matrix-react-sdk/pull/1739) + * RoomView: guard against unmounting during peeking + [\#1737](https://github.com/matrix-org/matrix-react-sdk/pull/1737) + * Fix HS/IS URL reset when switching to Registration + [\#1736](https://github.com/matrix-org/matrix-react-sdk/pull/1736) + * Fix the reject/accept call buttons in canary (mk2) + [\#1734](https://github.com/matrix-org/matrix-react-sdk/pull/1734) + * Make ratelimitedfunc time from the function's end + [\#1731](https://github.com/matrix-org/matrix-react-sdk/pull/1731) + * Give dialogs a matrixClient context + [\#1735](https://github.com/matrix-org/matrix-react-sdk/pull/1735) + * Fix key bindings in address picker dialog + [\#1732](https://github.com/matrix-org/matrix-react-sdk/pull/1732) + * Try upgrading eslint-plugin-react + [\#1712](https://github.com/matrix-org/matrix-react-sdk/pull/1712) + * Fix display name change text + [\#1730](https://github.com/matrix-org/matrix-react-sdk/pull/1730) + * Persist contentState when sending SlashCommand via MessageComposerInput + [\#1721](https://github.com/matrix-org/matrix-react-sdk/pull/1721) + * This is actually MFileBody not MImageBody, change classname + [\#1726](https://github.com/matrix-org/matrix-react-sdk/pull/1726) + * Use invite_3pid prop of createRoom instead of manual invite after create + [\#1717](https://github.com/matrix-org/matrix-react-sdk/pull/1717) + * guard against m.room.aliases events with no keys (redaction?) + [\#1729](https://github.com/matrix-org/matrix-react-sdk/pull/1729) + * Fix not showing Invited section if all invites are 3PID + [\#1718](https://github.com/matrix-org/matrix-react-sdk/pull/1718) + * Fix Rich Replies on files + [\#1720](https://github.com/matrix-org/matrix-react-sdk/pull/1720) + * Update from Weblate. + [\#1728](https://github.com/matrix-org/matrix-react-sdk/pull/1728) + * Null guard against falsey (non-null) props.node, to make react happy + [\#1724](https://github.com/matrix-org/matrix-react-sdk/pull/1724) + * Use correct condition for getting account data after first sync + [\#1722](https://github.com/matrix-org/matrix-react-sdk/pull/1722) + * Fix order calculation logic when reordering a room + [\#1725](https://github.com/matrix-org/matrix-react-sdk/pull/1725) + * Linear Rich Quoting + [\#1715](https://github.com/matrix-org/matrix-react-sdk/pull/1715) + * Fix CreateGroupDialog issues + [\#1714](https://github.com/matrix-org/matrix-react-sdk/pull/1714) + * Show a warning if the user attempts to leave a room that is invite only + [\#1713](https://github.com/matrix-org/matrix-react-sdk/pull/1713) + * Swap RoomList to react-beautiful-dnd + [\#1711](https://github.com/matrix-org/matrix-react-sdk/pull/1711) + * don't pass back {} when we have no `org.matrix.room.color_scheme` + [\#1710](https://github.com/matrix-org/matrix-react-sdk/pull/1710) + * Don't paginate whilst decrypting events + [\#1700](https://github.com/matrix-org/matrix-react-sdk/pull/1700) + * Fall back for missing i18n plurals + [\#1699](https://github.com/matrix-org/matrix-react-sdk/pull/1699) + * Fix group store redundant requests + [\#1709](https://github.com/matrix-org/matrix-react-sdk/pull/1709) + * Ignore remote echos caused by this client + [\#1708](https://github.com/matrix-org/matrix-react-sdk/pull/1708) + * Replace TagPanel react-dnd with react-beautiful-dnd + [\#1705](https://github.com/matrix-org/matrix-react-sdk/pull/1705) + * Only set selected tags state when updating rooms + [\#1704](https://github.com/matrix-org/matrix-react-sdk/pull/1704) + * Add formatFullDateNoTime to DateUtils and stop passing 12/24h to DateSep + [\#1702](https://github.com/matrix-org/matrix-react-sdk/pull/1702) + * Fix autofocus on QuestionDialog + [\#1698](https://github.com/matrix-org/matrix-react-sdk/pull/1698) + * Iterative fixes on Rich Quoting + [\#1697](https://github.com/matrix-org/matrix-react-sdk/pull/1697) + * Fix missing negation + [\#1696](https://github.com/matrix-org/matrix-react-sdk/pull/1696) + * Add Analytics Info and add Piwik to SdkConfig.DEFAULTS + [\#1625](https://github.com/matrix-org/matrix-react-sdk/pull/1625) + * Attempt to re-register for a scalar token if ours is invalid + [\#1668](https://github.com/matrix-org/matrix-react-sdk/pull/1668) + * Normalise dialogs + [\#1674](https://github.com/matrix-org/matrix-react-sdk/pull/1674) + * Add 'send without verifying' to status bar + [\#1695](https://github.com/matrix-org/matrix-react-sdk/pull/1695) + * Implement Rich Quoting/Replies + [\#1660](https://github.com/matrix-org/matrix-react-sdk/pull/1660) + * Revert "MD-escape URLs/alises/user IDs prior to parsing markdown" + [\#1694](https://github.com/matrix-org/matrix-react-sdk/pull/1694) + * Cache isConfCallRoom + [\#1693](https://github.com/matrix-org/matrix-react-sdk/pull/1693) + * Improve performance of tag panel selection (when tags are selected) + [\#1687](https://github.com/matrix-org/matrix-react-sdk/pull/1687) + * Hide status bar on visible->hidden transition + [\#1680](https://github.com/matrix-org/matrix-react-sdk/pull/1680) + * [revived] Singularise unsent message prompt, if applicable + [\#1692](https://github.com/matrix-org/matrix-react-sdk/pull/1692) + * small refactor && warn on self-demotion + [\#1683](https://github.com/matrix-org/matrix-react-sdk/pull/1683) + * Remove use of deprecated React.PropTypes + [\#1677](https://github.com/matrix-org/matrix-react-sdk/pull/1677) + * only save RelatedGroupSettings if it was modified. Otherwise perms issue + [\#1691](https://github.com/matrix-org/matrix-react-sdk/pull/1691) + * Fix a couple more issues with granular settings + [\#1675](https://github.com/matrix-org/matrix-react-sdk/pull/1675) + * Allow argument to op slashcommand to be negative as PLs can be -ve + [\#1673](https://github.com/matrix-org/matrix-react-sdk/pull/1673) + * Update from Weblate. + [\#1645](https://github.com/matrix-org/matrix-react-sdk/pull/1645) + * make RoomDetailRow reusable for the Room Directory + [\#1624](https://github.com/matrix-org/matrix-react-sdk/pull/1624) + * Prefetch group data for all joined groups when RoomList mounts + [\#1686](https://github.com/matrix-org/matrix-react-sdk/pull/1686) + * Remove unused selectedRoom prop + [\#1690](https://github.com/matrix-org/matrix-react-sdk/pull/1690) + * Fix shift and shift-ctrl click in TagPanel + [\#1684](https://github.com/matrix-org/matrix-react-sdk/pull/1684) + * skip direct chats which either you or the target have left + [\#1344](https://github.com/matrix-org/matrix-react-sdk/pull/1344) + * Make scroll on paste in RTE compatible with https://github.com/vector-im + /riot-web/pull/5900 + [\#1682](https://github.com/matrix-org/matrix-react-sdk/pull/1682) + * Remove extra full stop + [\#1685](https://github.com/matrix-org/matrix-react-sdk/pull/1685) + * Dedupe requests to fetch group profile data + [\#1666](https://github.com/matrix-org/matrix-react-sdk/pull/1666) + * Get Group profile from TagTile instead of TagPanel + [\#1667](https://github.com/matrix-org/matrix-react-sdk/pull/1667) + * Fix leaking of GroupStore listeners in RoomList + [\#1664](https://github.com/matrix-org/matrix-react-sdk/pull/1664) + * Add option to also output untranslated string + [\#1658](https://github.com/matrix-org/matrix-react-sdk/pull/1658) + * Give the current theme to widgets and the integration manager + [\#1669](https://github.com/matrix-org/matrix-react-sdk/pull/1669) + * Fixes #1953 Allow multiple file uploads using drag & drop for RoomView + [\#1671](https://github.com/matrix-org/matrix-react-sdk/pull/1671) + * Fix issue with preview of phone number on register and waiting for sms code + confirmation code + [\#1670](https://github.com/matrix-org/matrix-react-sdk/pull/1670) + * Attempt to improve TagPanel performance + [\#1647](https://github.com/matrix-org/matrix-react-sdk/pull/1647) + * Fix one variant of a scroll jump that occurs when decrypting an m.text + [\#1656](https://github.com/matrix-org/matrix-react-sdk/pull/1656) + * Avoid NPEs by using ref method for collecting loggedInView in MatrixChat + [\#1665](https://github.com/matrix-org/matrix-react-sdk/pull/1665) + * DnD Ordered TagPanel + [\#1653](https://github.com/matrix-org/matrix-react-sdk/pull/1653) + * Update widget title on edit. + [\#1663](https://github.com/matrix-org/matrix-react-sdk/pull/1663) + * Set widget title + [\#1661](https://github.com/matrix-org/matrix-react-sdk/pull/1661) + * Display custom widget content titles + [\#1650](https://github.com/matrix-org/matrix-react-sdk/pull/1650) + * Add maximize / minimize apps drawer icons. + [\#1649](https://github.com/matrix-org/matrix-react-sdk/pull/1649) + * Warn when migrating e2e data to indexeddb + [\#1654](https://github.com/matrix-org/matrix-react-sdk/pull/1654) + * Don't Auto-show UnknownDeviceDialog + [\#1600](https://github.com/matrix-org/matrix-react-sdk/pull/1600) + * Remove logging. + [\#1655](https://github.com/matrix-org/matrix-react-sdk/pull/1655) + * Add messaging endpoint for room encryption status. + [\#1648](https://github.com/matrix-org/matrix-react-sdk/pull/1648) + * Add some missing translatable strings + [\#1588](https://github.com/matrix-org/matrix-react-sdk/pull/1588) + * Add widget -> riot postMessage API + [\#1640](https://github.com/matrix-org/matrix-react-sdk/pull/1640) + * Add some null checks + [\#1646](https://github.com/matrix-org/matrix-react-sdk/pull/1646) + * Implement shift-click and ctrl-click semantics for TP + [\#1641](https://github.com/matrix-org/matrix-react-sdk/pull/1641) + * Don't show group when clicking tag panel + [\#1642](https://github.com/matrix-org/matrix-react-sdk/pull/1642) + * Implement TagPanel (or LeftLeftPanel) for group filtering + [\#1639](https://github.com/matrix-org/matrix-react-sdk/pull/1639) + * Implement UI for using bulk device deletion API + [\#1638](https://github.com/matrix-org/matrix-react-sdk/pull/1638) + * Replace (IRC) with flair + [\#1637](https://github.com/matrix-org/matrix-react-sdk/pull/1637) + * Allow guests to view individual groups + [\#1635](https://github.com/matrix-org/matrix-react-sdk/pull/1635) + * Allow guest to see MyGroups, show ILAG when creating a group + [\#1636](https://github.com/matrix-org/matrix-react-sdk/pull/1636) + * Move group publication toggles to UserSettings + [\#1634](https://github.com/matrix-org/matrix-react-sdk/pull/1634) + * Pull the theme through the default process + [\#1617](https://github.com/matrix-org/matrix-react-sdk/pull/1617) + * Rebase ConfirmRedactDialog on QuestionDialog + [\#1630](https://github.com/matrix-org/matrix-react-sdk/pull/1630) + * Fix logging of missing substitution variables + [\#1629](https://github.com/matrix-org/matrix-react-sdk/pull/1629) + * Rename Related Groups to improve readability + [\#1632](https://github.com/matrix-org/matrix-react-sdk/pull/1632) + * Make PresenceLabel more easily translatable + [\#1616](https://github.com/matrix-org/matrix-react-sdk/pull/1616) + * Perform substitution on all parts, not just the last one + [\#1618](https://github.com/matrix-org/matrix-react-sdk/pull/1618) + * Send Access Token in Headers to help prevent it being spit out in errors + [\#1552](https://github.com/matrix-org/matrix-react-sdk/pull/1552) + * Add aria-labels to ActionButtons + [\#1628](https://github.com/matrix-org/matrix-react-sdk/pull/1628) + * MemberPresenceAvatar: fix null references + [\#1620](https://github.com/matrix-org/matrix-react-sdk/pull/1620) + * Disable presence controls if there's no presence + [\#1623](https://github.com/matrix-org/matrix-react-sdk/pull/1623) + * Fix GroupMemberList search for users without displayname + [\#1627](https://github.com/matrix-org/matrix-react-sdk/pull/1627) + * Remove redundant super class EventEmitter for FlairStore + [\#1626](https://github.com/matrix-org/matrix-react-sdk/pull/1626) + * Fix granular URL previews + [\#1622](https://github.com/matrix-org/matrix-react-sdk/pull/1622) + * Flairstore: Fix broken reference + [\#1619](https://github.com/matrix-org/matrix-react-sdk/pull/1619) + * Do something more sensible for sender profile name/aux opacity + [\#1615](https://github.com/matrix-org/matrix-react-sdk/pull/1615) + * Add eslint rule keyword-spacing + [\#1614](https://github.com/matrix-org/matrix-react-sdk/pull/1614) + * Fix various issues surrounding granular settings to date + [\#1613](https://github.com/matrix-org/matrix-react-sdk/pull/1613) + * differentiate between state events and message events + [\#1612](https://github.com/matrix-org/matrix-react-sdk/pull/1612) + * Refactor translations + [\#1608](https://github.com/matrix-org/matrix-react-sdk/pull/1608) + * Make TintableSvg links behave like normal image links + [\#1611](https://github.com/matrix-org/matrix-react-sdk/pull/1611) + * Fix linting errors. + [\#1610](https://github.com/matrix-org/matrix-react-sdk/pull/1610) + * Granular settings + [\#1516](https://github.com/matrix-org/matrix-react-sdk/pull/1516) + * Implement user-controlled presence + [\#1482](https://github.com/matrix-org/matrix-react-sdk/pull/1482) + * Edit widget icon styling + [\#1609](https://github.com/matrix-org/matrix-react-sdk/pull/1609) + * Attempt to improve textual power levels + [\#1607](https://github.com/matrix-org/matrix-react-sdk/pull/1607) + * Determine whether power level is custom once Roles have been determined + [\#1606](https://github.com/matrix-org/matrix-react-sdk/pull/1606) + * Status.im theme + [\#1605](https://github.com/matrix-org/matrix-react-sdk/pull/1605) + * Revert "Lowercase all usernames" + [\#1604](https://github.com/matrix-org/matrix-react-sdk/pull/1604) + +Changes in [0.11.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.4) (2018-02-09) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.3...v0.11.4) + + * Add isUrlPermitted function to sanity check URLs + +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) diff --git a/README.md b/README.md index 144e89c938..ac45497dd4 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,8 @@ a 'skin'. A skin provides: * The containing application * Zero or more 'modules' containing non-UI functionality -**WARNING: As of July 2016, the skinning abstraction is broken due to rapid -development of `matrix-react-sdk` to meet the needs of Riot (codenamed Vector), the first app -to be built on top of the SDK** (https://github.com/vector-im/riot-web). -Right now `matrix-react-sdk` depends on some functionality from `riot-web` -(e.g. CSS), and `matrix-react-sdk` contains some Riot specific behaviour -(grep for 'vector'). This layering will be fixed asap once Riot development -has stabilised, but for now we do not advise trying to create new skins for -matrix-react-sdk until the layers are clearly separated again. - -In the interim, `vector-im/riot-web` and `matrix-org/matrix-react-sdk` should +As of Aug 2018, the only skin that exists is `vector-im/riot-web`; it and +`matrix-org/matrix-react-sdk` should effectively be considered as a single project (for instance, matrix-react-sdk bugs are currently filed against vector-im/riot-web rather than this project). @@ -46,17 +38,16 @@ 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: +Code should be committed as follows: * All new components: https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components * Riot-specific components: https://github.com/vector-im/riot-web/tree/master/src/components * In practice, `matrix-react-sdk` is still evolving so fast that the maintenance burden of customising and overriding these components for Riot can seriously impede development. So right now, there should be very few (if any) customisations for Riot. - * CSS for Matrix SDK components: https://github.com/vector-im/riot-web/tree/master/src/skins/vector/css/matrix-react-sdk - * CSS for Riot-specific overrides and components: https://github.com/vector-im/riot-web/tree/master/src/skins/vector/css/riot-web + * CSS: https://github.com/vector-im/riot-web/tree/master/src/skins/vector/css/matrix-react-sdk + * Theme specific CSS & resources: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes React components in matrix-react-sdk are come in two different flavours: 'structures' and 'views'. Structures are stateful components which handle the @@ -84,6 +75,7 @@ practices that anyone working with the SDK needs to be be aware of and uphold: * Per-view CSS is optional - it could choose to inherit all its styling from the context of the rest of the app, although this is unusual for any but + * Theme specific CSS & resources: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes structural components (lacking presentation logic) and the simplest view components. @@ -139,8 +131,7 @@ for now. OUTDATED: To Create Your Own Skin ================================= -**This is ALL LIES currently, as skinning is currently broken - see the WARNING -section at the top of this readme.** +**This is ALL LIES currently, and needs to be updated** Skins are modules are exported from such a package in the `lib` directory. `lib/skins` contains one directory per-skin, named after the skin, and the 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/docs/slate-formats.md b/docs/slate-formats.md new file mode 100644 index 0000000000..7bb2fc9c5f --- /dev/null +++ b/docs/slate-formats.md @@ -0,0 +1,88 @@ +Guide to data types used by the Slate-based Rich Text Editor +------------------------------------------------------------ + +We always store the Slate editor state in its Value form. + +The schema for the Value is the same whether the editor is in MD or rich text mode, and is currently (rather arbitrarily) +dictated by the schema expected by slate-md-serializer, simply because it was the only bit of the pipeline which +has opinions on the schema. (slate-html-serializer lets you define how to serialize whatever schema you like). + +The BLOCK_TAGS and MARK_TAGS give the mapping from HTML tags to the schema's node types (for blocks, which describe +block content like divs, and marks, which describe inline formatted sections like spans). + +We use

as the parent tag for the message (XXX: although some tags are technically not allowed to be nested within p's) + +Various conversions are performed as content is moved between HTML, MD, and plaintext representations of HTML and MD. + +The primitives used are: + + * Markdown.js - models commonmark-formatted MD strings (as entered by the composer in MD mode) + * toHtml() - renders them to HTML suitable for sending on the wire + * isPlainText() - checks whether the parsed MD contains anything other than simple text. + * toPlainText() - renders MD to plain text in order to remove backslashes. Only works if the MD is already plaintext (otherwise it just emits HTML) + + * slate-html-serializer + * converts Values to HTML (serialising) using our schema rules + * converts HTML to Values (deserialising) using our schema rules + + * slate-md-serializer + * converts rich Values to MD strings (serialising) but using a non-commonmark generic MD dialect. + * This should use commonmark, but we use the serializer here for expedience rather than writing a commonmark one. + + * slate-plain-serializer + * converts Values to plain text strings (serialising them) by concatenating the strings together + * converts Values from plain text strings (deserialiasing them). + * Used to initialise the editor by deserializing "" into a Value. Apparently this is the idiomatic way to initialise a blank editor. + * Used (as a bodge) to turn a rich text editor into a MD editor, when deserialising the converted MD string of the editor into a value + + * PlainWithPillsSerializer + * A fork of slate-plain-serializer which is aware of Pills (hence the name) and Emoji. + * It can be configured to output Pills as: + * "plain": Pills are rendered via their 'completion' text - e.g. 'Matthew'; used for sending messages) + * "md": Pills are rendered as MD, e.g. [Matthew](https://matrix.to/#/@matthew:matrix.org) ) + * "id": Pills are rendered as IDs, e.g. '@matthew:matrix.org' (used for authoring / commands) + * Emoji nodes are converted to inline utf8 emoji. + +The actual conversion transitions are: + + * Quoting: + * The message being quoted is taken as HTML + * ...and deserialised into a Value + * ...and then serialised into MD via slate-md-serializer if the editor is in MD mode + + * Roundtripping between MD and rich text editor mode + * From MD to richtext (mdToRichEditorState): + * Serialise the MD-format Value to a MD string (converting pills to MD) with PlainWithPillsSerializer in 'md' mode + * Convert that MD string to HTML via Markdown.js + * Deserialise that Value to HTML via slate-html-serializer + * From richtext to MD (richToMdEditorState): + * Serialise the richtext-format Value to a MD string with slate-md-serializer (XXX: this should use commonmark) + * Deserialise that to a plain text value via slate-plain-serializer + + * Loading history in one format into an editor which is in the other format + * Uses the same functions as for roundtripping + + * Scanning the editor for a slash command + * If the editor is a single line node starting with /, then serialize it to a string with PlainWithPillsSerializer in 'id' mode + So that pills get converted to IDs suitable for commands being passed around + + * Sending messages + * In RT mode: + * If there is rich content, serialize the RT-format Value to HTML body via slate-html-serializer + * Serialize the RT-format Value to the plain text fallback via PlainWithPillsSerializer in 'plain' mode + * In MD mode: + * Serialize the MD-format Value into an MD string with PlainWithPillsSerializer in 'md' mode + * Parse the string with Markdown.js + * If it contains no formatting: + * Send as plaintext (as taken from Markdown.toPlainText()) + * Otherwise + * Send as HTML (as taken from Markdown.toHtml()) + * Serialize the RT-format Value to the plain text fallback via PlainWithPillsSerializer in 'plain' mode + + * Pasting HTML + * Deserialize HTML to a RT Value via slate-html-serializer + * In RT mode, insert it straight into the editor as a fragment + * In MD mode, serialise it to an MD string via slate-md-serializer and then insert the string into the editor as a fragment. + +The various scenarios and transitions could be drawn into a pretty diagram if one felt the urge, but hopefully the above +gives sufficient detail on how it's all meant to work. \ No newline at end of file diff --git a/header b/header index beee1ebe89..33b7fb9e80 100644 --- a/header +++ b/header @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd +Copyright 2017, 2018 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. diff --git a/jenkins.sh b/jenkins.sh index 0979edfa13..8cf5ee4a1f 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -11,8 +11,10 @@ set -x # install the other dependencies npm install -# we may be using a dev branch of js-sdk in which case we need to build it -(cd node_modules/matrix-js-sdk && npm install) +scripts/fetchdep.sh matrix-org matrix-js-sdk +rm -r node_modules/matrix-js-sdk || true +ln -s ../matrix-js-sdk node_modules/matrix-js-sdk +(cd matrix-js-sdk && npm install) # run the mocha tests npm run test -- --no-colors @@ -21,9 +23,7 @@ npm run test -- --no-colors npm run lintall -- -f checkstyle -o eslint.xml || true # re-run the linter, excluding any files known to have errors or warnings. -./node_modules/.bin/eslint --max-warnings 0 \ - --ignore-path .eslintignore.errorfiles \ - src test +npm run lintwithexclusions # delete the old tarball, if it exists rm -f matrix-react-sdk-*.tgz diff --git a/package.json b/package.json index 38a647ff67..2cbdb98c2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.10.3", + "version": "0.13.4", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -25,22 +25,28 @@ "release.sh", "scripts", "src", - "test" + "test", + "res" ], "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 20 --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" }, @@ -51,29 +57,39 @@ "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", "classnames": "^2.1.2", - "commonmark": "^0.27.0", + "commonmark": "^0.28.1", "counterpart": "^0.18.0", - "draft-js": "^0.11.0-alpha", - "draft-js-export-html": "^0.6.0", - "draft-js-export-markdown": "^0.3.0", "emojione": "2.2.7", "file-saver": "^1.3.3", "filesize": "3.5.6", "flux": "2.1.1", + "focus-trap-react": "^3.0.5", "fuse.js": "^2.2.0", + "gemini-scrollbar": "matrix-org/gemini-scrollbar#b302279", + "gfm.css": "^1.1.1", "glob": "^5.0.14", - "highlight.js": "^8.9.1", + "highlight.js": "^9.0.0", "isomorphic-fetch": "^2.2.1", - "linkifyjs": "^2.1.3", + "linkifyjs": "^2.1.6", "lodash": "^4.13.1", - "matrix-js-sdk": "0.8.2", + "lolex": "2.3.2", + "matrix-js-sdk": "0.11.0", "optimist": "^0.6.1", + "pako": "^1.0.5", "prop-types": "^15.5.8", - "react": "^15.4.0", + "qrcode-react": "^0.1.16", + "querystring": "^0.2.0", + "react": "^15.6.0", "react-addons-css-transition-group": "15.3.2", - "react-dom": "^15.4.0", + "react-beautiful-dnd": "^4.0.1", + "react-dom": "^15.6.0", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", - "sanitize-html": "^1.14.1", + "resize-observer-polyfill": "^1.5.0", + "slate": "0.34.7", + "slate-react": "^0.12.4", + "slate-html-serializer": "^0.6.1", + "slate-md-serializer": "matrix-org/slate-md-serializer#f7c4ad3", + "sanitize-html": "^1.18.4", "text-encoding-utf-8": "^1.0.1", "url": "^0.11.0", "velocity-vector": "vector-im/velocity#059e3b2", @@ -99,8 +115,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.7.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", @@ -112,14 +130,16 @@ "karma-spec-reporter": "^0.0.31", "karma-summary-reporter": "^1.3.3", "karma-webpack": "^1.7.0", + "matrix-mock-request": "^1.2.1", "matrix-react-test-utils": "^0.1.1", - "mocha": "^2.4.5", - "parallelshell": "^1.2.0", + "mocha": "^5.0.5", + "parallelshell": "3.0.1", "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", + "sinon": "^5.0.7", + "source-map-loader": "^0.2.3", + "walk": "^2.3.9", "webpack": "^1.12.14" } } diff --git a/res/css/_common.scss b/res/css/_common.scss new file mode 100644 index 0000000000..38f576a532 --- /dev/null +++ b/res/css/_common.scss @@ -0,0 +1,366 @@ +/* +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. +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. +*/ + +html { + /* hack to stop overscroll bounce on OSX and iOS. + N.B. Breaks things when we have legitimate horizontal overscroll */ + height: 100%; + overflow: hidden; +} + +body { + font-family: $font-family; + font-size: 15px; + background-color: $primary-bg-color; + color: $primary-fg-color; + border: 0px; + margin: 0px; + /* This should render the fonts the same accross browsers */ + -webkit-font-smoothing: subpixel-antialiased; +} + +div.error, div.warning { + color: $warning-color; +} + +h2 { + color: $primary-fg-color; + font-weight: 400; + font-size: 18px; + margin-top: 16px; + margin-bottom: 16px; +} + +a:hover, +a:link, +a:visited { + color: $accent-color; +} + +input[type=text], input[type=password], textarea { + background-color: transparent; + color: $primary-fg-color; +} + +input[type=text].error, input[type=password].error { + border: 1px solid $warning-color; +} + +input[type=text]:focus, input[type=password]:focus, textarea:focus { + border: 1px solid $accent-color; + outline: none; + box-shadow: none; +} + +/* Required by Firefox */ +textarea { + font-family: $font-family; +} + +/* Prevent ugly dotted highlight around selected elements in Firefox */ +::-moz-focus-inner { + border: 0; +} + +/* applied to side-panels and messagepanel when in RoomSettings */ +.mx_fadable { + opacity: 1; + transition: opacity 0.2s ease-in-out; +} + +.mx_fadable.mx_fadable_faded { + opacity: 0.3; + pointer-events: none; +} + +/* XXX: critical hack to GeminiScrollbar to allow them to work in FF 42 and Chrome 48. + Stop the scrollbar view from pushing out the container's overall sizing, which causes + flexbox to adapt to the new size and cause the view to keep growing. + */ +.gm-scrollbar-container .gm-scroll-view { + position: absolute; +} + +/* Expand thumbs on hoverover */ +.gm-scrollbar { + border-radius: 5px ! important; +} +.gm-scrollbar.-vertical { + width: 6px; + transition: width 120ms ease-out ! important; +} +.gm-scrollbar.-vertical:hover, +.gm-scrollbar.-vertical:active { + width: 8px; + transition: width 120ms ease-out ! important; +} +.gm-scrollbar.-horizontal { + height: 6px; + transition: height 120ms ease-out ! important; +} +.gm-scrollbar.-horizontal:hover, +.gm-scrollbar.-horizontal:active { + height: 8px; + transition: height 120ms ease-out ! important; +} + +// These are magic constants which are excluded from tinting, to let themes +// (which only have CSS, unlike skins) tell the app what their non-tinted +// colourscheme is by inspecting the stylesheet DOM. +// +// They are not used for layout!! +#mx_theme_accentColor { + color: $accent-color; +} + +#mx_theme_secondaryAccentColor { + color: $secondary-accent-color; +} + +#mx_theme_tertiaryAccentColor { + color: $roomsublist-label-bg-color; +} + +.mx_Dialog_wrapper { + position: fixed; + z-index: 4000; + top: 0; + left: 0; + width: 100%; + height: 100%; + + display: flex; + align-items: center; + justify-content: center; +} + +/* Spinner Dialog overide */ +.mx_Dialog_wrapper.mx_Dialog_spinner .mx_Dialog { + width: auto; + border-radius: 8px; + padding: 0px; + box-shadow: none; +} + +/* View Source Dialog overide */ +.mx_Dialog_wrapper.mx_Dialog_viewsource .mx_Dialog { + padding-left: 10px; + padding-right: 10px; +} + +.mx_Dialog { + background-color: $primary-bg-color; + color: $light-fg-color; + z-index: 4010; + font-weight: 300; + font-size: 15px; + position: relative; + padding-left: 58px; + padding-bottom: 36px; + width: 60%; + max-width: 704px; + box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2); + max-height: 80%; + overflow-y: auto; +} + +.mx_Dialog_background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: $dialog-background-bg-color; + opacity: 0.8; +} + +.mx_Dialog_lightbox .mx_Dialog_background { + opacity: 0.85; + background-color: $lightbox-background-bg-color; +} + +.mx_Dialog_lightbox .mx_Dialog { + border-radius: 0px; + background-color: transparent; + width: 100%; + height: 100%; + max-width: 100%; + max-height: 100%; + pointer-events: none; +} + +.mx_Dialog_cancelButton { + position: absolute; + right: 11px; + top: 13px; + cursor: pointer; +} + +.mx_Dialog_cancelButton object { + pointer-events: none; +} + +.mx_Dialog_content { + margin: 24px 58px 68px 0; + font-size: 14px; + color: $primary-fg-color; + word-wrap: break-word; +} + +.mx_Dialog button, .mx_Dialog input[type="submit"] { + @mixin mx_DialogButton; + margin-left: 0px; + margin-right: 8px; + + // flip colours for the secondary ones + font-weight: 600; + border: 1px solid $accent-color ! important; + color: $accent-color; + background-color: $accent-fg-color; +} + +.mx_Dialog button:hover, .mx_Dialog input[type="submit"]:hover { + @mixin mx_DialogButton_hover; +} + +.mx_Dialog button:focus, .mx_Dialog input[type="submit"]:focus { + filter: brightness($focus-brightness); +} + +.mx_Dialog button.mx_Dialog_primary, .mx_Dialog input[type="submit"].mx_Dialog_primary { + color: $accent-fg-color; + background-color: $accent-color; +} + +.mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger { + background-color: $warning-color; + border: solid 1px $warning-color; + color: $accent-fg-color; +} + +.mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled { + background-color: $light-fg-color; + border: solid 1px $light-fg-color; + opacity: 0.7; +} + +.mx_Dialog_title { + min-height: 16px; + padding-top: 40px; + font-weight: bold; + font-size: 22px; + line-height: 1.4; + color: $primary-fg-color; +} + +.mx_Dialog_title.danger { + color: $warning-color; +} + +.mx_TextInputDialog_label { + text-align: left; + padding-bottom: 12px; +} + +.mx_TextInputDialog_input { + font-size: 15px; + border-radius: 3px; + border: 1px solid $input-border-color; + padding: 9px; + color: $primary-fg-color; + background-color: $primary-bg-color; +} + +.mx_emojione { + height: 1em; + vertical-align: middle; +} + +.mx_emojione_selected { + background-color: $accent-color; +} + +::-moz-selection { + background-color: $accent-color; + color: $selection-fg-color; +} + +::selection { + background-color: $accent-color; + color: $selection-fg-color; +} + +.mx_textButton { + @mixin mx_DialogButton_small; +} + +.mx_textButton:hover { + @mixin mx_DialogButton_hover; +} + +.mx_button_row { + margin-top: 69px; +} + +.mx_Beta { + color: red; + margin-right: 10px; + position: relative; + top: -3px; + background-color: white; + padding: 0 4px; + border-radius: 3px; + border: 1px solid darkred; + cursor: help; + transition-duration: 200ms; + font-size: smaller; + filter: opacity(0.5); +} + +.mx_Beta:hover { + color: white; + border: 1px solid gray; + background-color: darkred; +} + +.mx_TintableSvgButton { + position: relative; + display: flex; + flex-direction: row; + justify-content: center; + align-content: center; +} + +.mx_TintableSvgButton object { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + max-width: 100%; + max-height: 100%; +} + +.mx_TintableSvgButton span { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0; + cursor: pointer; +} diff --git a/res/css/_components.scss b/res/css/_components.scss new file mode 100644 index 0000000000..0e40b40a29 --- /dev/null +++ b/res/css/_components.scss @@ -0,0 +1,114 @@ +// autogenerated by rethemendex.sh +@import "./_common.scss"; +@import "./_fonts.scss"; +@import "./structures/_CompatibilityPage.scss"; +@import "./structures/_ContextualMenu.scss"; +@import "./structures/_CreateRoom.scss"; +@import "./structures/_FilePanel.scss"; +@import "./structures/_GroupView.scss"; +@import "./structures/_HomePage.scss"; +@import "./structures/_LeftPanel.scss"; +@import "./structures/_LoginBox.scss"; +@import "./structures/_MatrixChat.scss"; +@import "./structures/_MyGroups.scss"; +@import "./structures/_NotificationPanel.scss"; +@import "./structures/_RightPanel.scss"; +@import "./structures/_RoomDirectory.scss"; +@import "./structures/_RoomStatusBar.scss"; +@import "./structures/_RoomSubList.scss"; +@import "./structures/_RoomView.scss"; +@import "./structures/_SearchBox.scss"; +@import "./structures/_TagPanel.scss"; +@import "./structures/_UploadBar.scss"; +@import "./structures/_UserSettings.scss"; +@import "./structures/_ViewSource.scss"; +@import "./structures/login/_Login.scss"; +@import "./views/avatars/_BaseAvatar.scss"; +@import "./views/context_menus/_MessageContextMenu.scss"; +@import "./views/context_menus/_RoomTileContextMenu.scss"; +@import "./views/context_menus/_TagTileContextMenu.scss"; +@import "./views/dialogs/_BugReportDialog.scss"; +@import "./views/dialogs/_ChangelogDialog.scss"; +@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; +@import "./views/dialogs/_ChatInviteDialog.scss"; +@import "./views/dialogs/_ConfirmUserActionDialog.scss"; +@import "./views/dialogs/_CreateGroupDialog.scss"; +@import "./views/dialogs/_CreateRoomDialog.scss"; +@import "./views/dialogs/_DeactivateAccountDialog.scss"; +@import "./views/dialogs/_DevtoolsDialog.scss"; +@import "./views/dialogs/_EncryptedEventDialog.scss"; +@import "./views/dialogs/_GroupAddressPicker.scss"; +@import "./views/dialogs/_QuestionDialog.scss"; +@import "./views/dialogs/_RoomUpgradeDialog.scss"; +@import "./views/dialogs/_SetEmailDialog.scss"; +@import "./views/dialogs/_SetMxIdDialog.scss"; +@import "./views/dialogs/_SetPasswordDialog.scss"; +@import "./views/dialogs/_ShareDialog.scss"; +@import "./views/dialogs/_UnknownDeviceDialog.scss"; +@import "./views/directory/_NetworkDropdown.scss"; +@import "./views/elements/_AccessibleButton.scss"; +@import "./views/elements/_AddressSelector.scss"; +@import "./views/elements/_AddressTile.scss"; +@import "./views/elements/_DirectorySearchBox.scss"; +@import "./views/elements/_Dropdown.scss"; +@import "./views/elements/_EditableItemList.scss"; +@import "./views/elements/_ImageView.scss"; +@import "./views/elements/_InlineSpinner.scss"; +@import "./views/elements/_MemberEventListSummary.scss"; +@import "./views/elements/_ProgressBar.scss"; +@import "./views/elements/_ReplyThread.scss"; +@import "./views/elements/_RichText.scss"; +@import "./views/elements/_RoleButton.scss"; +@import "./views/elements/_Spinner.scss"; +@import "./views/elements/_SyntaxHighlight.scss"; +@import "./views/elements/_ToolTipButton.scss"; +@import "./views/globals/_MatrixToolbar.scss"; +@import "./views/groups/_GroupPublicityToggle.scss"; +@import "./views/groups/_GroupRoomList.scss"; +@import "./views/groups/_GroupUserSettings.scss"; +@import "./views/login/_InteractiveAuthEntryComponents.scss"; +@import "./views/login/_ServerConfig.scss"; +@import "./views/messages/_CreateEvent.scss"; +@import "./views/messages/_DateSeparator.scss"; +@import "./views/messages/_MEmoteBody.scss"; +@import "./views/messages/_MFileBody.scss"; +@import "./views/messages/_MImageBody.scss"; +@import "./views/messages/_MNoticeBody.scss"; +@import "./views/messages/_MStickerBody.scss"; +@import "./views/messages/_MTextBody.scss"; +@import "./views/messages/_MessageTimestamp.scss"; +@import "./views/messages/_RoomAvatarEvent.scss"; +@import "./views/messages/_SenderProfile.scss"; +@import "./views/messages/_TextualEvent.scss"; +@import "./views/messages/_UnknownBody.scss"; +@import "./views/rooms/_AppsDrawer.scss"; +@import "./views/rooms/_Autocomplete.scss"; +@import "./views/rooms/_EntityTile.scss"; +@import "./views/rooms/_EventTile.scss"; +@import "./views/rooms/_LinkPreviewWidget.scss"; +@import "./views/rooms/_MemberDeviceInfo.scss"; +@import "./views/rooms/_MemberInfo.scss"; +@import "./views/rooms/_MemberList.scss"; +@import "./views/rooms/_MessageComposer.scss"; +@import "./views/rooms/_PinnedEventTile.scss"; +@import "./views/rooms/_PinnedEventsPanel.scss"; +@import "./views/rooms/_PresenceLabel.scss"; +@import "./views/rooms/_ReplyPreview.scss"; +@import "./views/rooms/_RoomDropTarget.scss"; +@import "./views/rooms/_RoomHeader.scss"; +@import "./views/rooms/_RoomList.scss"; +@import "./views/rooms/_RoomPreviewBar.scss"; +@import "./views/rooms/_RoomSettings.scss"; +@import "./views/rooms/_RoomTile.scss"; +@import "./views/rooms/_RoomTooltip.scss"; +@import "./views/rooms/_RoomUpgradeWarningBar.scss"; +@import "./views/rooms/_SearchBar.scss"; +@import "./views/rooms/_SearchableEntityList.scss"; +@import "./views/rooms/_Stickers.scss"; +@import "./views/rooms/_TopUnreadMessagesBar.scss"; +@import "./views/settings/_DevicesPanel.scss"; +@import "./views/settings/_IntegrationsManager.scss"; +@import "./views/settings/_Notifications.scss"; +@import "./views/voip/_CallView.scss"; +@import "./views/voip/_IncomingCallbox.scss"; +@import "./views/voip/_VideoView.scss"; diff --git a/res/css/_fonts.scss b/res/css/_fonts.scss new file mode 100644 index 0000000000..52ac95b569 --- /dev/null +++ b/res/css/_fonts.scss @@ -0,0 +1,67 @@ +/* + * Open Sans + * Includes extended Latin, Greek, Cyrillic and Vietnamese character sets + */ + +/* the 'src' links are relative to the bundle.css, which is in a subdirectory. + */ +@font-face { + font-family: 'Open Sans'; + src: url('../../fonts/Open_Sans/OpenSans-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Open Sans'; + src: url('../../fonts/Open_Sans/OpenSans-Italic.ttf') format('truetype'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: 'Open Sans'; + src: url('../../fonts/Open_Sans/OpenSans-Semibold.ttf') format('truetype'); + font-weight: 600; + font-style: normal; +} + +@font-face { + font-family: 'Open Sans'; + src: url('../../fonts/Open_Sans/OpenSans-SemiboldItalic.ttf') format('truetype'); + font-weight: 600; + font-style: italic; +} + +@font-face { + font-family: 'Open Sans'; + src: url('../../fonts/Open_Sans/OpenSans-Bold.ttf') format('truetype'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Open Sans'; + src: url('../../fonts/Open_Sans/OpenSans-BoldItalic.ttf') format('truetype'); + font-weight: 700; + font-style: italic; +} + +/* + * Fira Mono + * Used for monospace copy, i.e. code + */ + +@font-face { + font-family: 'Fira Mono'; + src: url('../../fonts/Fira_Mono/FiraMono-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Fira Mono'; + src: url('../../fonts/Fira_Mono/FiraMono-Bold.ttf') format('truetype'); + font-weight: 700; + font-style: normal; +} diff --git a/res/css/rethemendex.sh b/res/css/rethemendex.sh new file mode 100755 index 0000000000..13be73f9a9 --- /dev/null +++ b/res/css/rethemendex.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +cd `dirname $0` + +{ + echo "// autogenerated by rethemendex.sh" + + # we used to have exclude /themes from the find at this point. + # as themes are no longer a spurious subdirectory of css/, we don't + # need it any more. + find . -iname _\*.scss | fgrep -v _components.scss | LC_ALL=C sort | + while read i; do + echo "@import \"$i\";" + done +} > _components.scss diff --git a/res/css/structures/_CompatibilityPage.scss b/res/css/structures/_CompatibilityPage.scss new file mode 100644 index 0000000000..f3f032c975 --- /dev/null +++ b/res/css/structures/_CompatibilityPage.scss @@ -0,0 +1,19 @@ +.mx_CompatibilityPage { + width: 100%; + height: 100%; + background-color: #e55; +} + +.mx_CompatibilityPage_box { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; + width: 500px; + height: 300px; + border: 1px solid; + padding: 10px; + background-color: #fcc; +} \ No newline at end of file diff --git a/res/css/structures/_ContextualMenu.scss b/res/css/structures/_ContextualMenu.scss new file mode 100644 index 0000000000..7474c3d107 --- /dev/null +++ b/res/css/structures/_ContextualMenu.scss @@ -0,0 +1,160 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_ContextualMenu_wrapper { + position: fixed; + z-index: 5000; +} + +.mx_ContextualMenu_background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 1.0; + z-index: 5000; +} + +.mx_ContextualMenu { + border: solid 1px $menu-border-color; + border-radius: 4px; + background-color: $menu-bg-color; + color: $primary-fg-color; + position: absolute; + padding: 6px; + font-size: 14px; + z-index: 5001; +} + +.mx_ContextualMenu.mx_ContextualMenu_right { + right: 8px; +} + +.mx_ContextualMenu_chevron_right { + position: absolute; + right: -8px; + top: 0px; + width: 0; + height: 0; + border-top: 8px solid transparent; + border-left: 8px solid $menu-border-color; + border-bottom: 8px solid transparent; +} + +.mx_ContextualMenu_chevron_right:after { + content:''; + width: 0; + height: 0; + border-top: 7px solid transparent; + border-left: 7px solid $menu-bg-color; + border-bottom: 7px solid transparent; + position:absolute; + top: -7px; + right: 1px; +} + +.mx_ContextualMenu.mx_ContextualMenu_left { + left: 8px; +} + +.mx_ContextualMenu_chevron_left { + position: absolute; + left: -8px; + top: 0px; + width: 0; + height: 0; + border-top: 8px solid transparent; + border-right: 8px solid $menu-border-color; + border-bottom: 8px solid transparent; +} + +.mx_ContextualMenu_chevron_left:after{ + content:''; + width: 0; + height: 0; + border-top: 7px solid transparent; + border-right: 7px solid $menu-bg-color; + border-bottom: 7px solid transparent; + position:absolute; + top: -7px; + left: 1px; +} + +.mx_ContextualMenu.mx_ContextualMenu_top { + top: 8px; +} + +.mx_ContextualMenu_chevron_top { + position: absolute; + left: 0px; + top: -8px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-bottom: 8px solid $menu-border-color; + border-right: 8px solid transparent; +} + +.mx_ContextualMenu_chevron_top:after{ + content:''; + width: 0; + height: 0; + border-left: 7px solid transparent; + border-bottom: 7px solid $menu-bg-color; + border-right: 7px solid transparent; + position:absolute; + left: -7px; + top: 1px; +} + +.mx_ContextualMenu.mx_ContextualMenu_bottom { + bottom: 8px; +} + +.mx_ContextualMenu_chevron_bottom { + position: absolute; + left: 0px; + bottom: -8px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-top: 8px solid $menu-border-color; + border-right: 8px solid transparent; +} + +.mx_ContextualMenu_chevron_bottom:after{ + content:''; + width: 0; + height: 0; + border-left: 7px solid transparent; + border-top: 7px solid $menu-bg-color; + border-right: 7px solid transparent; + position:absolute; + left: -7px; + bottom: 1px; +} + +.mx_ContextualMenu_field { + padding: 3px 6px 3px 6px; + cursor: pointer; + white-space: nowrap; +} + +.mx_ContextualMenu_spinner { + display: block; + margin: 0 auto; +} diff --git a/res/css/structures/_CreateRoom.scss b/res/css/structures/_CreateRoom.scss new file mode 100644 index 0000000000..2be193525e --- /dev/null +++ b/res/css/structures/_CreateRoom.scss @@ -0,0 +1,37 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_CreateRoom { + width: 960px; + margin-left: auto; + margin-right: auto; + color: $primary-fg-color; +} + +.mx_CreateRoom input, +.mx_CreateRoom textarea { + border-radius: 3px; + border: 1px solid $strong-input-border-color; + font-weight: 300; + font-size: 13px; + padding: 9px; + margin-top: 6px; +} + +.mx_CreateRoom_description { + width: 330px; +} + diff --git a/res/css/structures/_FilePanel.scss b/res/css/structures/_FilePanel.scss new file mode 100644 index 0000000000..87dc0aa756 --- /dev/null +++ b/res/css/structures/_FilePanel.scss @@ -0,0 +1,114 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +.mx_FilePanel { + order: 2; + + flex: 1 1 0; + + width: 100%; + + overflow-y: auto; +} + +.mx_FilePanel .mx_RoomView_messageListWrapper { + margin-right: 20px; +} + +.mx_FilePanel .mx_RoomView_MessageList h2 { + display: none; +} + +/* FIXME: rather than having EventTile's default CSS be for MessagePanel, + we should make EventTile a base CSS class and customise it specifically + for usage in {Message,File,Notification}Panel. */ + +.mx_FilePanel .mx_EventTile_avatar { + display: none; +} + +/* Overrides for the attachment body tiles */ + +.mx_FilePanel .mx_EventTile { + word-break: break-word; +} + +.mx_FilePanel .mx_EventTile .mx_MImageBody { + margin-right: 0px; +} + +.mx_FilePanel .mx_EventTile .mx_MFileBody_download { + display: flex; + font-size: 14px; + color: $event-timestamp-color; +} + +.mx_FilePanel .mx_EventTile .mx_MFileBody_downloadLink { + flex: 1 1 auto; + color: $light-fg-color; +} + +.mx_FilePanel .mx_EventTile .mx_MImageBody_size { + flex: 1 0 0; + font-size: 11px; + text-align: right; + white-space: nowrap; +} + +/* Overides for the sender details line */ + +.mx_FilePanel .mx_EventTile_senderDetails { + display: flex; + margin-top: -2px; +} + +.mx_FilePanel .mx_EventTile_senderDetailsLink { + text-decoration: none; +} + +.mx_FilePanel .mx_EventTile .mx_SenderProfile { + flex: 1 1 auto; + line-height: initial; + padding: 0px; + font-size: 11px; + opacity: 1.0; + color: $event-timestamp-color; +} + +.mx_FilePanel .mx_EventTile .mx_MessageTimestamp { + flex: 1 0 0; + text-align: right; + visibility: visible; + position: initial; + font-size: 11px; + opacity: 1.0; + color: $event-timestamp-color; +} + +/* Overrides for the wrappers around the body tile */ + +.mx_FilePanel .mx_EventTile_line { + margin-right: 0px; + padding-left: 0px; +} + +.mx_FilePanel .mx_EventTile:hover .mx_EventTile_line { + background-color: $primary-bg-color; +} + +.mx_FilePanel .mx_EventTile_selected .mx_EventTile_line { + padding-left: 0px; +} diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss new file mode 100644 index 0000000000..02e5a948e9 --- /dev/null +++ b/res/css/structures/_GroupView.scss @@ -0,0 +1,349 @@ +/* +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. +*/ + +.mx_GroupView { + max-width: 960px; + width: 100%; + margin-left: auto; + margin-right: auto; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.mx_GroupView_error { + margin: auto; +} + +.mx_GroupView_header { + max-width: 960px; + min-height: 70px; + align-items: center; + display: flex; + padding-bottom: 10px; +} + +.mx_GroupView_header_view { + border-bottom: 1px solid $primary-hairline-color; + padding-bottom: 0px; +} + +.mx_GroupView_header_avatar, .mx_GroupView_header_info { + display: table-cell; + vertical-align: middle; +} + +.mx_GroupHeader_button { + margin-left: 12px; + cursor: pointer; +} + +.mx_GroupHeader_button object { + // prevents clicks from being swallowed by svg in 'object' tag + pointer-events: none; +} + +.mx_GroupView_editable { + border-bottom: 1px solid $strong-input-border-color ! important; + min-width: 150px; + cursor: text; +} + +.mx_GroupView_editable:focus { + border-bottom: 1px solid $accent-color ! important; + outline: none; + box-shadow: none; +} + +.mx_GroupView_header_isUserMember .mx_GroupView_header_name:hover div:not(.mx_GroupView_editable) { + color: $accent-color; + cursor: pointer; +} + +.mx_GroupView_avatarPicker { + position: relative; +} + +.mx_GroupView_avatarPicker_edit { + position: absolute; + top: 50px; + left: 15px; +} + +.mx_GroupView_avatarPicker .mx_Spinner { + width: 48px; + height: 48px ! important; +} + +.mx_GroupView_header_leftCol { + flex: 1; + + overflow: hidden; +} + +.mx_GroupView_header_rightCol { + display: flex; + align-items: center; +} + +.mx_GroupView_textButton { + display: inline-block; +} + +.mx_GroupView_header_groupid { + font-weight: normal; + font-size: initial; + padding-left: 10px; +} + +.mx_GroupView_header_name { + vertical-align: middle; + width: 100%; + height: 31px; + overflow: hidden; + color: $primary-fg-color; + font-weight: bold; + font-size: 22px; + padding-left: 19px; + padding-right: 16px; + /* why isn't text-overflow working? */ + text-overflow: ellipsis; + border-bottom: 1px solid transparent; +} + +.mx_GroupView_header_shortDesc { + vertical-align: bottom; + float: left; + max-height: 42px; + color: $settings-grey-fg-color; + font-weight: 300; + font-size: 13px; + padding-left: 19px; + margin-right: 16px; + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 1px solid transparent; +} + +.mx_GroupView_avatarPicker_label { + cursor: pointer; +} + +.mx_GroupView_cancelButton { + padding-left: 8px; +} + +.mx_GroupView_cancelButton img { + position: relative; + top: 5px; +} + +.mx_GroupView input[type='radio'] { + margin: 10px 10px 0px 10px; +} + +.mx_GroupView_label_text { + display: inline-block; + max-width: 80%; + vertical-align: 0.1em; + line-height: 2em; +} + +.mx_GroupView_body { + flex-grow: 1; +} + +.mx_GroupView_rooms { + flex-grow: 1; + display: flex; + flex-direction: column; + min-height: 200px; + user-select: none; +} + +.mx_GroupView h3 { + text-transform: uppercase; + color: $h3-color; + font-weight: 600; + font-size: 13px; + margin-bottom: 10px; +} + +.mx_GroupView_rooms_header .mx_AccessibleButton { + padding-left: 14px; + margin-bottom: 14px; + height: 24px; +} + +.mx_GroupView_group { + border-top: 1px solid $primary-hairline-color; +} + +.mx_GroupView_group_disabled { + opacity: 0.3; + pointer-events: none; +} + +.mx_GroupView_rooms_header_addRow_button { + display: inline-block; +} + +.mx_GroupView_rooms_header_addRow_button object { + pointer-events: none; +} + +.mx_GroupView_rooms_header_addRow_label { + display: inline-block; + vertical-align: top; + line-height: 24px; + padding-left: 28px; + color: $accent-color; +} + +.mx_GroupView_rooms .mx_RoomDetailList { + flex-grow: 1; + border-top: 1px solid $primary-hairline-color; + padding-top: 10px; + word-break: break-word; +} + +.mx_GroupView .mx_RoomView_messageListWrapper { + justify-content: flex-start; +} + +.mx_GroupView_membershipSection { + color: $greyed-fg-color; + margin-top: 10px; +} + +.mx_GroupView_membershipSubSection { + justify-content: space-between; + display: flex; +} + +.mx_GroupView_membershipSubSection .mx_Spinner { + justify-content: flex-end; +} + +.mx_GroupView_membershipSection_description { + /* To match textButton */ + line-height: 34px; +} + +.mx_GroupView_membershipSection_description .mx_BaseAvatar { + margin-right: 10px; +} + +.mx_GroupView_membershipSection .mx_GroupView_textButton { + margin-right: 0px; + margin-top: 0px; + margin-left: 8px; +} + +.mx_GroupView_memberSettings_toggle label { + cursor: pointer; + user-select: none; +} + +.mx_GroupView_memberSettings input { + margin-right: 6px; +} + +.mx_GroupView_featuredThings { + margin-top: 20px; +} + +.mx_GroupView_featuredThings_header { + font-weight: bold; + font-size: 120%; + margin-bottom: 20px; +} + +.mx_GroupView_featuredThings_category { + font-weight: bold; + font-size: 110%; + margin-top: 10px; +} + +.mx_GroupView_featuredThings_container { + display: flex; +} + +.mx_GroupView_featuredThings_addButton, +.mx_GroupView_featuredThing { + display: table-cell; + text-align: center; + + width: 100px; + margin: 0px 20px; +} + +.mx_GroupView_featuredThing { + position: relative; +} + +.mx_GroupView_featuredThing .mx_GroupView_featuredThing_deleteButton { + position: absolute; + top: -7px; + right: 11px; + opacity: 0.4; +} + +.mx_GroupView_featuredThing .mx_BaseAvatar { + /* To prevent misalignment with mx_TintableSvg (in addButton) */ + vertical-align: initial; +} + +.mx_GroupView_featuredThings_addButton object { + pointer-events: none; +} + +.mx_GroupView_featuredThing_name { + word-wrap: break-word; +} + +.mx_GroupView_uploadInput { + display: none; +} + +.mx_GroupView_body .gm-scroll-view > *{ + margin: 11px 50px 0px 68px; +} + +.mx_GroupView_groupDesc textarea { + width: 100%; + max-width: 100%; + height: 150px; +} + +.mx_GroupView_groupDesc_placeholder, +.mx_GroupView_changeDelayWarning { + background-color: $info-plinth-bg-color; + color: $info-plinth-fg-color; + border-radius: 10px; + text-align: center; + + margin: 20px 0px; +} + +.mx_GroupView_groupDesc_placeholder { + padding: 100px 20px; + cursor: pointer; +} + +.mx_GroupView_changeDelayWarning { + padding: 40px 20px; +} diff --git a/res/css/structures/_HomePage.scss b/res/css/structures/_HomePage.scss new file mode 100644 index 0000000000..cdac1bcc8a --- /dev/null +++ b/res/css/structures/_HomePage.scss @@ -0,0 +1,35 @@ +/* +Copyright 2016 OpenMarket Ltd +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. +*/ + +.mx_HomePage { + max-width: 960px; + width: 100%; + height: 100%; + margin-left: auto; + margin-right: auto; +} + +.mx_HomePage iframe { + display: block; + width: 100%; + height: 100%; + border: 0px; +} + +.mx_HomePage_body { +// margin-left: 63px; +} diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss new file mode 100644 index 0000000000..7cf6dd1119 --- /dev/null +++ b/res/css/structures/_LeftPanel.scss @@ -0,0 +1,134 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 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. +*/ + +.mx_LeftPanel { + position: relative; + + display: flex; + flex-direction: column; +} + +.mx_LeftPanel_container { + display: flex; + /* LeftPanel 235px */ + flex: 0 0 235px; +} + +.mx_LeftPanel_container.mx_LeftPanel_container_hasTagPanel { + /* TagPanel 60px + LeftPanel 235px */ + flex: 0 0 295px; +} + +.mx_LeftPanel_container_collapsed { + /* Collapsed LeftPanel 60px */ + flex: 0 0 60px; +} + +.mx_LeftPanel_container_collapsed.mx_LeftPanel_container_hasTagPanel { + /* TagPanel 60px + Collapsed LeftPanel 60px */ + flex: 0 0 120px; +} + +.mx_LeftPanel_hideButton { + position: absolute; + top: 10px; + right: 0px; + padding: 8px; + cursor: pointer; +} + +.mx_LeftPanel_callView { + +} + +.mx_LeftPanel .mx_AppTile_mini { + height: 132px; +} + +.mx_LeftPanel .mx_RoomList_scrollbar { + order: 1; + + flex: 1 1 0; + + overflow-y: auto; + z-index: 6; +} + +.mx_LeftPanel.collapsed .mx_BottomLeftMenu { + flex: 0 0 160px; + margin-bottom: 9px; +} + +.mx_LeftPanel .mx_BottomLeftMenu { + order: 3; + + border-top: 1px solid $panel-divider-color; + margin-left: 16px; /* gutter */ + margin-right: 16px; /* gutter */ + flex: 0 0 60px; + z-index: 1; +} + +.mx_LeftPanel .mx_BottomLeftMenu_options { + margin-top: 18px; +} + +.mx_BottomLeftMenu_options object { + pointer-events: none; +} + +.collapsed .mx_RoleButton { + margin-right: 0px ! important; + padding-top: 3px ! important; + padding-bottom: 3px ! important; +} + +.mx_BottomLeftMenu_options > div { + display: inline-block; +} + +.mx_BottomLeftMenu_options .mx_RoleButton { + margin-left: 0px; + margin-right: 10px; + height: 30px; +} + +.mx_BottomLeftMenu_options .mx_BottomLeftMenu_settings { + float: right; +} + +.mx_BottomLeftMenu_options .mx_BottomLeftMenu_settings .mx_RoleButton { + margin-right: 0px; +} + +.mx_LeftPanel.collapsed .mx_BottomLeftMenu_settings { + float: none; +} + +.mx_MatrixChat_useCompactLayout { + .mx_LeftPanel .mx_BottomLeftMenu { + flex: 0 0 50px; + } + + .mx_LeftPanel.collapsed .mx_BottomLeftMenu { + flex: 0 0 160px; + } + + .mx_LeftPanel .mx_BottomLeftMenu_options { + margin-top: 12px; + } +} diff --git a/res/css/structures/_LoginBox.scss b/res/css/structures/_LoginBox.scss new file mode 100644 index 0000000000..7f6199c451 --- /dev/null +++ b/res/css/structures/_LoginBox.scss @@ -0,0 +1,47 @@ +/* +Copyright 2017 OpenMarket 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. +*/ + +.mx_LoginBox { + min-height: 24px; + height: unset !important; + padding-top: 13px !important; + padding-bottom: 14px !important; +} + +.mx_LoginBox_loginButton_wrapper { + text-align: center; + width: 100%; +} + +.mx_LoginBox_loginButton, +.mx_LoginBox_registerButton { + margin-top: 3px; + height: 40px; + border: 0px; + border-radius: 40px; + margin-left: 4px; + margin-right: 4px; + min-width: 80px; + + background-color: $accent-color; + color: $primary-bg-color; + + cursor: pointer; + + font-size: 15px; + padding: 0 11px; + word-break: break-word; +} diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss new file mode 100644 index 0000000000..eae1f56180 --- /dev/null +++ b/res/css/structures/_MatrixChat.scss @@ -0,0 +1,116 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_MatrixChat_splash { + position: relative; + height: 100%; +} + +.mx_MatrixChat_splashButtons { + text-align: center; + width: 100%; + position: absolute; + bottom: 30px; +} + +.mx_MatrixChat_wrapper { + display: flex; + + flex-direction: column; + + width: 100%; + height: 100%; +} + +.mx_MatrixToolbar { + order: 1; + + height: 40px; +} + +.mx_MatrixChat_toolbarShowing { + height: auto; +} + +.mx_MatrixChat { + width: 100%; + height: 100%; + + display: flex; + + order: 2; + + flex: 1; +} + +.mx_MatrixChat_syncError { + color: $accent-fg-color; + background-color: $warning-bg-color; + border-radius: 5px; + display: table; + padding: 30px; + position: absolute; + top: 100px; + left: 50%; + transform: translateX(-50%); +} + +.mx_MatrixChat .mx_LeftPanel { + order: 1; + + background-color: $secondary-accent-color; + + flex: 0 0 235px; +} + +.mx_MatrixChat .mx_LeftPanel.collapsed { + flex: 0 0 60px; +} + +.mx_MatrixChat .mx_MatrixChat_middlePanel { + order: 2; + + padding-left: 20px; + padding-right: 22px; + background-color: $primary-bg-color; + + flex: 1; + + /* Experimental fix for https://github.com/vector-im/vector-web/issues/947 + and https://github.com/vector-im/vector-web/issues/946. + Empirically this stops the MessagePanel's width exploding outwards when + gemini is in 'prevented' mode + */ + overflow-x: auto; + + display: flex; + + /* To fix https://github.com/vector-im/riot-web/issues/3298 where Safari + needed height 100% all the way down to the HomePage. Height does not + have to be auto, empirically. + */ + height: 100%; +} + +.mx_MatrixChat .mx_RightPanel { + order: 3; + + flex: 0 0 235px; +} + +.mx_MatrixChat .mx_RightPanel.collapsed { + flex: 0 0 122px; +} diff --git a/res/css/structures/_MyGroups.scss b/res/css/structures/_MyGroups.scss new file mode 100644 index 0000000000..6d140721c8 --- /dev/null +++ b/res/css/structures/_MyGroups.scss @@ -0,0 +1,151 @@ +/* +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. +*/ + +.mx_MyGroups { + max-width: 960px; + margin-left: auto; + margin-right: auto; + + display: flex; + flex-direction: column; +} + +.mx_MyGroups .mx_RoomHeader_simpleHeader { + margin-left: 0px; +} + +.mx_MyGroups_header { + /* Keep mid-point of create button aligned with icon in page header */ + margin-left: 2px; + display: flex; + flex-wrap: wrap; +} + +.mx_MyGroups_headerCard { + flex: 1 0 50%; + margin-bottom: 30px; + min-width: 400px; + display: flex; + align-items: center; +} + +.mx_MyGroups_headerCard .mx_MyGroups_headerCard_button { + margin-right: 13px; + height: 50px; +} + +.mx_MyGroups_headerCard_button object { + /* Otherwise the SVG object absorbs clicks and the button doesn't work */ + pointer-events: none; +} + +.mx_MyGroups_headerCard_header { + font-weight: bold; + margin-bottom: 10px; +} + +.mx_MyGroups_headerCard_content { + padding-right: 15px; +} + +/* Until the button is wired up */ +.mx_MyGroups_joinBox { + visibility: hidden; + + /* When joinBox wraps onto its own row, it should take up zero height so + that there isn't an awkward gap between MyGroups_createBox and + MyGroups_content. + */ + height: 0px; + margin: 0px; +} + +.mx_MyGroups_content { + margin-left: 2px; + + flex: 1 0 0; + + display: flex; + flex-direction: column; +} + +.mx_MyGroups_placeholder { + background-color: $info-plinth-bg-color; + color: $info-plinth-fg-color; + line-height: 400px; + border-radius: 10px; + text-align: center; +} + +.mx_MyGroups_joinedGroups { + border-top: 1px solid $primary-hairline-color; + overflow-x: hidden; + + display: flex; + flex-direction: row; + flex-flow: wrap; + align-content: flex-start; +} + +.mx_MyGroups_joinedGroups .mx_GroupTile { + min-width: 300px; + max-width: 33%; + flex: 1 0 300px; + height: 75px; + margin: 10px 0px; + display: flex; + align-items: flex-start; + cursor: pointer; +} + +.mx_GroupTile_avatar { + cursor: grab, -webkit-grab; +} + +.mx_GroupTile_profile { + margin-left: 10px; + display: flex; + flex-direction: column; + justify-content: center; +} + +.mx_GroupTile_profile .mx_GroupTile_name, +.mx_GroupTile_profile .mx_GroupTile_groupId, +.mx_GroupTile_profile .mx_GroupTile_desc { + padding-right: 10px; +} + +.mx_GroupTile_profile .mx_GroupTile_name { + margin: 0px; + font-size: 15px; +} + +.mx_GroupTile_profile .mx_GroupTile_groupId { + font-size: 13px; +} + +.mx_GroupTile_profile .mx_GroupTile_desc { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + font-size: 13px; + max-height: 36px; + overflow: hidden; +} + +.mx_GroupTile_profile .mx_GroupTile_groupId { + opacity: 0.7; +} diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss new file mode 100644 index 0000000000..a899808d57 --- /dev/null +++ b/res/css/structures/_NotificationPanel.scss @@ -0,0 +1,100 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_NotificationPanel { + order: 2; + + flex: 1 1 0; + + width: 100%; + + overflow-y: auto; +} + +.mx_NotificationPanel .mx_RoomView_messageListWrapper { + margin-right: 20px; +} + +.mx_NotificationPanel .mx_RoomView_MessageList h2 { + margin-left: 0px; +} + +/* FIXME: rather than having EventTile's default CSS be for MessagePanel, + we should make EventTile a base CSS class and customise it specifically + for usage in {Message,File,Notification}Panel. */ + +.mx_NotificationPanel .mx_EventTile { + word-break: break-word; +} + +.mx_NotificationPanel .mx_EventTile_roomName { + font-weight: bold; + font-size: 14px; +} + +.mx_NotificationPanel .mx_EventTile_roomName a { + color: $primary-fg-color; +} + +.mx_NotificationPanel .mx_EventTile_avatar { + top: 8px; + left: 0px; +} + +.mx_NotificationPanel .mx_EventTile .mx_SenderProfile, +.mx_NotificationPanel .mx_EventTile .mx_MessageTimestamp { + color: $primary-fg-color; + font-size: 12px; + display: inline; + padding-left: 0px; +} + +.mx_NotificationPanel .mx_EventTile_senderDetails { + padding-left: 32px; + padding-top: 8px; + position: relative; +} + +.mx_NotificationPanel .mx_EventTile_roomName a, +.mx_NotificationPanel .mx_EventTile_senderDetails a { + text-decoration: none ! important; +} + +.mx_NotificationPanel .mx_EventTile .mx_MessageTimestamp { + visibility: visible; + position: initial; + display: inline; +} + +.mx_NotificationPanel .mx_EventTile_line { + margin-right: 0px; + padding-left: 32px; + padding-top: 0px; + padding-bottom: 0px; + padding-right: 0px; +} + +.mx_NotificationPanel .mx_EventTile:hover .mx_EventTile_line { + background-color: $primary-bg-color; +} + +.mx_NotificationPanel .mx_EventTile_selected .mx_EventTile_line { + padding-left: 0px; +} + +.mx_NotificationPanel .mx_EventTile_content { + margin-right: 0px; +} \ No newline at end of file diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss new file mode 100644 index 0000000000..b4dff612ed --- /dev/null +++ b/res/css/structures/_RightPanel.scss @@ -0,0 +1,133 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_RightPanel { + position: relative; + + display: flex; + flex-direction: column; +} + +.mx_RightPanel_header { + order: 1; + + border-bottom: 1px solid $primary-hairline-color; + margin-right: 20px; + + flex: 0 0 70px; +} + +/** Fixme - factor this out with the main header **/ + +.mx_RightPanel_headerButtonGroup { + margin-top: 6px; + display: flex; + width: 100%; + background-color: $primary-bg-color; + margin-left: 0px; +} + +.mx_RightPanel_headerButton { + cursor: pointer; + flex: 0 0 auto; + vertical-align: top; + padding-left: 4px; + padding-right: 5px; + text-align: center; + position: relative; +} + +.mx_RightPanel_headerButton object { + pointer-events: none; + padding-bottom: 3px; +} + +.mx_RightPanel_headerButton_highlight { + width: 25px; + height: 5px; + border-radius: 5px; + background-color: $accent-color; + opacity: 0.2; +} + +.mx_RightPanel_headerButton_badge { + font-size: 11px; + color: $accent-color; + font-weight: bold; + padding-bottom: 2px; +} + +.mx_RightPanel_collapsebutton { + flex: 1; + text-align: right; + margin-top: 20px; +} + +.mx_RightPanel .mx_MemberList, +.mx_RightPanel .mx_MemberInfo, +.mx_RightPanel .mx_GroupRoomList, +.mx_RightPanel_blank { + order: 2; + flex: 1 1 0; +} + +.mx_RightPanel .mx_RoomView_messagePanelSpinner { + order: 2; + margin: auto; +} + +.mx_RightPanel_footer { + order: 3; + + border-top: 1px solid $primary-hairline-color; + margin-right: 20px; + + flex: 0 0 60px; +} + +.mx_RightPanel_footer .mx_RightPanel_invite { + font-size: 14px; + color: $primary-fg-color; + padding-top: 13px; + padding-left: 5px; + cursor: pointer; + display: flex; + align-items: center; +} + +.collapsed .mx_RightPanel_footer .mx_RightPanel_invite { + display: none; +} + +.mx_RightPanel_invite .mx_RightPanel_icon object { + pointer-events: none; +} + +.mx_RightPanel_invite .mx_RightPanel_message { + padding-left: 10px; + line-height: 18px; +} + +.mx_MatrixChat_useCompactLayout { + .mx_RightPanel_footer { + flex: 0 0 50px; + } + + .mx_RightPanel_footer .mx_RightPanel_invite { + line-height: 25px; + padding-top: 8px; + } +} diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss new file mode 100644 index 0000000000..9cd3e7284c --- /dev/null +++ b/res/css/structures/_RoomDirectory.scss @@ -0,0 +1,131 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_RoomDirectory { + max-width: 960px; + width: 100%; + margin-left: auto; + margin-right: auto; + margin-bottom: 12px; + color: $primary-fg-color; + word-break: break-word; + + display: flex; + + flex-direction: column; +} + +.mx_RoomDirectory .mx_RoomHeader_simpleHeader { + margin-left: 0px; +} + +.mx_RoomDirectory_list { + flex: 1; + + display: flex; + + flex-direction: column; +} + +.mx_RoomDirectory_list .mx_RoomView_messageListWrapper { + justify-content: flex-start; +} + +.mx_RoomDirectory_listheader { + display: table; + table-layout: fixed; + width: 100%; + margin-top: 12px; + margin-bottom: 12px; + border-spacing: 5px; +} + +.mx_RoomDirectory_searchbox { + display: table-cell; + vertical-align: middle; +} + +.mx_RoomDirectory_listheader .mx_NetworkDropdown { + display: table-cell; + width: 200px; +} + +.mx_RoomDirectory_tableWrapper { + overflow-y: auto; + flex: 1 1 0; +} + +.mx_RoomDirectory_table { + font-size: 14px; + color: $primary-fg-color; + width: 100%; + text-align: left; + table-layout: fixed; +} + +.mx_RoomDirectory_roomAvatar { + width: 24px; + padding-left: 12px; + padding-right: 24px; + vertical-align: top; +} + +.mx_RoomDirectory_roomDescription { + padding-bottom: 16px; +} + +.mx_RoomDirectory_name { + display: inline-block; + font-weight: 600; +} + +.mx_RoomDirectory_perms { + display: inline-block; +} + +.mx_RoomDirectory_perm { + display: inline; + padding-left: 5px; + padding-right: 5px; + margin-right: 5px; + height: 15px; + border-radius: 11px; + background-color: $plinth-bg-color; + text-transform: uppercase; + font-weight: 600; + font-size: 11px; + color: $accent-color; +} + +.mx_RoomDirectory_topic { + cursor: initial; +} + +.mx_RoomDirectory_alias { + font-size: 12px; + color: $settings-grey-fg-color; +} + +.mx_RoomDirectory_roomMemberCount { + text-align: right; + width: 100px; + padding-right: 10px; +} + +.mx_RoomDirectory_table tr { + padding-bottom: 10px; + cursor: pointer; +} diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss new file mode 100644 index 0000000000..2a9cc9f6c7 --- /dev/null +++ b/res/css/structures/_RoomStatusBar.scss @@ -0,0 +1,184 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_RoomStatusBar { + margin-left: 65px; + min-height: 50px; +} + +/* position the indicator in the same place horizontally as .mx_EventTile_avatar. */ +.mx_RoomStatusBar_indicator { + padding-left: 17px; + padding-right: 12px; + margin-left: -73px; + margin-top: 15px; + float: left; + width: 24px; + text-align: center; +} + +.mx_RoomStatusBar_callBar { + height: 50px; + line-height: 50px; +} + +.mx_RoomStatusBar_placeholderIndicator span { + color: $primary-fg-color; + opacity: 0.5; + position: relative; + top: -4px; +/* + animation-duration: 1s; + animation-name: bounce; + animation-direction: alternate; + animation-iteration-count: infinite; +*/ +} + +.mx_RoomStatusBar_placeholderIndicator span:nth-child(1) { + animation-delay: 0.3s; +} +.mx_RoomStatusBar_placeholderIndicator span:nth-child(2) { + animation-delay: 0.6s; +} +.mx_RoomStatusBar_placeholderIndicator span:nth-child(3) { + animation-delay: 0.9s; +} + +@keyframes bounce { + from { + opacity: 0.5; + top: 0; + } + + to { + opacity: 0.2; + top: -3px; + } +} + +.mx_RoomStatusBar_typingIndicatorAvatars { + width: 52px; + margin-top: -1px; + text-align: left; +} + +.mx_RoomStatusBar_typingIndicatorAvatars .mx_BaseAvatar_image { + margin-right: -12px; + border: 1px solid $primary-bg-color; +} + +.mx_RoomStatusBar_typingIndicatorAvatars .mx_BaseAvatar_initial { + padding-left: 1px; + padding-top: 1px; +} + +.mx_RoomStatusBar_typingIndicatorRemaining { + display: inline-block; + color: #acacac; + background-color: #ddd; + border: 1px solid $primary-bg-color; + border-radius: 40px; + width: 24px; + height: 24px; + line-height: 24px; + font-size: 0.8em; + vertical-align: top; + text-align: center; + position: absolute; +} + +.mx_RoomStatusBar_scrollDownIndicator { + cursor: pointer; + padding-left: 1px; +} + +.mx_RoomStatusBar_unreadMessagesBar { + padding-top: 10px; + color: $warning-color; + cursor: pointer; +} + +.mx_RoomStatusBar_connectionLostBar { + display: flex; + + margin-top: 19px; + min-height: 58px; +} + +.mx_RoomStatusBar_connectionLostBar img { + padding-left: 10px; + padding-right: 22px; + vertical-align: middle; + float: left; +} + +.mx_RoomStatusBar_connectionLostBar_title { + color: $warning-color; +} + +.mx_RoomStatusBar_connectionLostBar_desc { + color: $primary-fg-color; + font-size: 13px; + opacity: 0.5; + padding-bottom: 20px; +} + +.mx_RoomStatusBar_resend_link { + color: $primary-fg-color ! important; + text-decoration: underline ! important; + cursor: pointer; +} + +.mx_RoomStatusBar_typingBar { + height: 50px; + line-height: 50px; + + color: $primary-fg-color; + opacity: 0.5; + overflow-y: hidden; + display: block; +} + +.mx_RoomStatusBar_isAlone { + height: 50px; + line-height: 50px; + + color: $primary-fg-color; + opacity: 0.5; + overflow-y: hidden; + display: block; +} + +.mx_MatrixChat_useCompactLayout { + .mx_RoomStatusBar { + min-height: 40px; + } + + .mx_RoomStatusBar_indicator { + margin-top: 10px; + } + + .mx_RoomStatusBar_callBar { + height: 40px; + line-height: 40px; + } + + .mx_RoomStatusBar_typingBar { + height: 40px; + line-height: 40px; + } +} diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss new file mode 100644 index 0000000000..6798f75a14 --- /dev/null +++ b/res/css/structures/_RoomSubList.scss @@ -0,0 +1,248 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_RoomSubList { + display: table; + table-layout: fixed; + width: 100%; + + background-color: $roomsublist-background; +} + +.mx_RoomSubList_labelContainer { + height: 31px; /* mx_RoomSubList_label height including border */ + width: 235px; /* LHS Panel width */ + position: relative; +} + +.mx_RoomSubList_label { + position: relative; + text-transform: uppercase; + color: $roomsublist-label-fg-color; + font-weight: 600; + font-size: 12px; + width: 203px; /* padding + width = LHS Panel width */ + height: 19px; /* height + padding = 31px = mx_RoomSubList_label height */ + padding-left: 16px; /* gutter */ + padding-right: 16px; /* gutter */ + padding-top: 6px; + padding-bottom: 6px; + cursor: pointer; + background-color: $secondary-accent-color; +} + +.mx_RoomSubList_label.mx_RoomSubList_fixed { + position: fixed; + top: 0; + z-index: 5; + /* pointer-events: none; */ +} + +.collapsed .mx_RoomSubList_label { + height: 17px; + width: 28px; /* collapsed LHS Panel width */ +} + +.collapsed .mx_RoomSubList_labelContainer { + width: 28px; /* collapsed LHS Panel width */ +} + +.mx_RoomSubList_roomCount { + display: inline-block; + font-size: 12px; + font-weight: normal; + color: $accent-color; + padding-left: 5px; + text-transform: none; +} + +.collapsed .mx_RoomSubList_roomCount { + display: none; +} + +.mx_RoomSubList_badge { + display: inline-block; + min-width: 15px; + height: 15px; + position: absolute; + right: 8px; /*gutter */ + top: 7px; + border-radius: 8px; + color: $accent-fg-color; + font-weight: 600; + font-size: 10px; + text-align: center; + padding-top: 1px; + padding-left: 4px; + padding-right: 4px; + background-color: $accent-color; +} + +.mx_RoomSubList_label .mx_RoomSubList_badge:hover { + filter: brightness($focus-brightness); +} + +/* +.collapsed .mx_RoomSubList_badge { + display: none; +} +*/ + +.mx_RoomSubList_badgeHighlight { + background-color: $warning-color; +} + +/* This is the bottom of the speech bubble */ +.mx_RoomSubList_badgeHighlight:after { + content: ""; + position: absolute; + display: block; + width: 0; + height: 0; + margin-left: 5px; + border-top: 5px solid $warning-color; + border-right: 7px solid transparent; +} + +/* Hide the bottom of speech bubble */ +.collapsed .mx_RoomSubList_badgeHighlight:after { + display: none; +} + +.mx_RoomSubList_chevron { + pointer-events: none; + position: absolute; + right: 41px; + top: 11px; +} + +.mx_RoomSubList_chevronDown { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 6px solid $roomsublist-chevron-color; +} + +.mx_RoomSubList_chevronUp { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 6px solid $roomsublist-chevron-color; +} + +.mx_RoomSubList_chevronRight { + width: 0; + height: 0; + border-top: 5px solid transparent; + border-left: 6px solid $roomsublist-chevron-color; + border-bottom: 5px solid transparent; +} + +/* The overflow section */ +.mx_RoomSubList_ellipsis { + display: block; + line-height: 11px; + height: 18px; + position: relative; + cursor: pointer; + font-size: 13px; + + background-color: $secondary-accent-color; +} + +.collapsed .mx_RoomSubList_ellipsis { + height: 20px; +} + +.mx_RoomSubList_line { + display: inline-block; + width: 159px; + border-top: dotted 2px $accent-color; + vertical-align: middle; +} + +.collapsed .mx_RoomSubList_line { + display: none; +} + +.mx_RoomSubList_more { + display: inline-block; + text-transform: uppercase; + font-size: 10px; + font-weight: 600; + text-align: left; + color: $accent-color; + padding-left: 7px; + padding-right: 7px; + padding-left: 7px; + vertical-align: middle; +} + +.collapsed .mx_RoomSubList_more { + display: none; +} + +.mx_RoomSubList_moreBadge { + display: inline-block; + min-width: 15px; + height: 13px; + position: absolute; + right: 8px; /*gutter */ + top: -2px; + border-radius: 8px; + border: solid 1px $accent-color; + color: $accent-fg-color; + font-weight: 600; + font-size: 10px; + text-align: center; + padding-top: 1px; + padding-left: 3px; + padding-right: 3px; + background-color: $primary-bg-color; + vertical-align: middle; +} + +.mx_RoomSubList_moreBadge.mx_RoomSubList_moreBadgeNotify { + background-color: $accent-color; + border: 0; + padding-top: 3px; + padding-left: 4px; + padding-right: 4px; +} + +.mx_RoomSubList_moreBadge.mx_RoomSubList_moreBadgeHighlight { + background-color: $warning-color; + border: 0; + padding-top: 3px; + padding-left: 4px; + padding-right: 4px; +} + +.collapsed .mx_RoomSubList_moreBadge { + position: static; + margin-left: 16px; + margin-top: 2px; +} + +.mx_RoomSubList_ellipsis .mx_RoomSubList_chevronDown { + position: relative; + top: 4px; + left: 2px; +} + + diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss new file mode 100644 index 0000000000..02418f70db --- /dev/null +++ b/res/css/structures/_RoomView.scss @@ -0,0 +1,269 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_RoomView { + word-wrap: break-word; + position: relative; + + display: flex; + width: 100%; + + flex-direction: column; +} + +.mx_RoomView .mx_RoomHeader { + order: 1; + + flex: 0 0 70px; +} + +.mx_RoomView_fileDropTarget { + min-width: 0px; + max-width: 960px; + width: 100%; + font-size: 18px; + text-align: center; + + pointer-events: none; + + padding-left: 12px; + padding-right: 12px; + margin-left: -12px; + + border-top-left-radius: 10px; + border-top-right-radius: 10px; + + background-color: $droptarget-bg-color; + border: 2px #e1dddd solid; + border-bottom: none; + position: absolute; + top: 70px; + bottom: 0px; + z-index: 3000; +} + +.mx_RoomView_fileDropTargetLabel { + top: 50%; + width: 100%; + margin-top: -50px; + position: absolute; +} + +.mx_RoomView_auxPanel { + order: 2; + + min-width: 0px; + max-width: 960px; + width: 100%; + margin: 0px auto; + + overflow: auto; + border-bottom: 1px solid $primary-hairline-color; + + flex: 0 0 auto; +} + +.mx_RoomView_auxPanel_apps { + max-width: 1920px ! important; +} + + +.mx_RoomView_body { + order: 3; + flex: 1 1 0; + flex-direction: column; + display: flex; +} + +.mx_RoomView_body .mx_RoomView_topUnreadMessagesBar { + order: 1; +} + +.mx_RoomView_body .mx_RoomView_messagePanel { + order: 2; +} + +.mx_RoomView_body .mx_RoomView_messagePanelSpinner { + order: 2; + margin: auto; +} + +.mx_RoomView_body .mx_RoomView_statusArea { + order: 3; +} + +.mx_RoomView_body .mx_MessageComposer { + order: 4; +} + +.mx_RoomView_messagePanel { + width: 100%; + overflow-y: auto; +} + +.mx_RoomView_messageListWrapper { + max-width: 960px; + margin: auto; + + min-height: 100%; + + display: flex; + + flex-direction: column; + + justify-content: flex-end; +} + +.mx_RoomView_searchResultsPanel .mx_RoomView_messageListWrapper { + justify-content: flex-start; +} + +.mx_RoomView_empty { + flex: 1 1 auto; + font-size: 13px; + padding-left: 3em; + padding-right: 3em; + margin-right: 20px; + margin-top: 33%; + text-align: center; +} + +.mx_RoomView_MessageList { + width: 100%; + list-style-type: none; + padding: 0px; +} + +.mx_RoomView_MessageList li { + clear: both; +} + +li.mx_RoomView_myReadMarker_container { + height: 0px; + margin: 0px; + padding: 0px; + border: 0px; +} + +hr.mx_RoomView_myReadMarker { + border-top: solid 1px $accent-color; + border-bottom: solid 1px $accent-color; + margin-top: 0px; + position: relative; + top: -1px; + z-index: 1; +} + +.mx_RoomView_statusArea { + width: 100%; + flex: 0 0 auto; + + max-height: 0px; + background-color: $primary-bg-color; + z-index: 1000; + overflow: hidden; + + transition: all .2s ease-out; +} + +.mx_RoomView_statusArea_expanded { + max-height: 100px; +} + +.mx_RoomView_statusAreaBox { + max-width: 960px; + margin: auto; + min-height: 50px; +} + +.mx_RoomView_statusAreaBox_line { + margin-left: 65px; + border-top: 1px solid $primary-hairline-color; + height: 1px; +} + +.mx_RoomView_callStatusBar .mx_UploadBar_uploadProgressInner { + background-color: $primary-bg-color; +} + +.mx_RoomView_callStatusBar .mx_UploadBar_uploadFilename { + color: $accent-fg-color; + opacity: 1.0; +} + +.mx_RoomView_inCall .mx_RoomView_statusAreaBox_line { + margin-top: 2px; + border: none; + height: 0px; +} + +.mx_RoomView_inCall .mx_MessageComposer_wrapper { + border-top: 2px hidden; + padding-top: 1px; +} + +.mx_RoomView_inCall .mx_RoomView_statusAreaBox { + background-color: $accent-color; + color: $accent-fg-color; + position: relative; +} + +.mx_RoomView_voipChevron { + position: absolute; + bottom: -11px; + right: 11px; +} + +.mx_RoomView_voipButton { + float: right; + margin-right: 13px; + margin-top: 10px; + cursor: pointer; +} + +.mx_RoomView_voipButton object { + pointer-events: none; +} + +.mx_RoomView .mx_MessageComposer { + width: 100%; + flex: 0 0 auto; + margin-right: 2px; +} + +.mx_RoomView_ongoingConfCallNotification { + width: 100%; + text-align: center; + background-color: $warning-color; + color: $accent-fg-color; + font-weight: bold; + padding: 6px 0; + cursor: pointer; +} + +.mx_RoomView_ongoingConfCallNotification a { + color: $accent-fg-color ! important; +} + +.mx_MatrixChat_useCompactLayout { + .mx_RoomView_MessageList { + margin-bottom: 4px; + } + + .mx_RoomView_statusAreaBox { + min-height: 42px; + } +} diff --git a/res/css/structures/_SearchBox.scss b/res/css/structures/_SearchBox.scss new file mode 100644 index 0000000000..6f08fd47b2 --- /dev/null +++ b/res/css/structures/_SearchBox.scss @@ -0,0 +1,68 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_SearchBox { + height: 24px; + margin-left: 16px; + margin-right: 16px; + padding-top: 24px; + padding-bottom: 22px; + + border-bottom: 1px solid $panel-divider-color; + + display: flex; +} + +.mx_SearchBox_searchButton { + margin-right: 10px; + margin-top: 5px; + pointer-events: none; +} + +.mx_SearchBox_closeButton { + cursor: pointer; + margin-top: -5px; +} + +.mx_SearchBox_search { + flex: 1 1 auto; + width: 0px; + font-family: $font-family; + font-size: 12px; + margin-top: -2px; + height: 24px; + border: 0px ! important; + /* border-bottom: 1px solid rgba(0, 0, 0, 0.1) ! important; */ + border: 0px; +} + +.mx_SearchBox_minimise, +.mx_SearchBox_maximise { + margin-top: 3px; + cursor: pointer; +} + +.mx_SearchBox_minimise { + margin-left: 10px; +} + +.mx_SearchBox_maximise { + margin-left: 9px; +} + +.mx_SearchBox object { + pointer-events: none; +} diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss new file mode 100644 index 0000000000..415aafd924 --- /dev/null +++ b/res/css/structures/_TagPanel.scss @@ -0,0 +1,129 @@ +/* +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. +*/ + +.mx_TagPanel { + flex: 0 0 60px; + background-color: $tertiary-accent-color; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; +} + +.mx_TagPanel_items_selected { + cursor: pointer; +} + +.mx_TagPanel .mx_TagPanel_clearButton_container { + /* Constant height within flex mx_TagPanel */ + height: 70px; + width: 60px; + + flex: none; + + display: flex; + justify-content: center; + align-items: flex-start; +} + +.mx_TagPanel .mx_TagPanel_clearButton object { + /* Same as .mx_SearchBox padding-top */ + margin-top: 24px; + pointer-events: none; +} + +.mx_TagPanel .mx_TagPanel_divider { + height: 0px; + width: 42px; + border-bottom: 1px solid $panel-divider-color; +} + +.mx_TagPanel .mx_TagPanel_scroller { + flex-grow: 1; +} + +.mx_TagPanel .mx_TagPanel_tagTileContainer { + display: flex; + flex-direction: column; + align-items: center; + + height: 100%; +} + +.mx_TagPanel .mx_TagTile { + padding: 6px 3px; + opacity: 0.5; + position: relative; +} +.mx_TagPanel .mx_TagTile:focus, +.mx_TagPanel .mx_TagTile:hover, +.mx_TagPanel .mx_TagTile.mx_TagTile_selected { + opacity: 1; +} + +.mx_TagPanel .mx_TagTile.mx_TagTile_selected { + /* To offset border of mx_TagTile_avatar */ + padding: 3px 0px; +} + +.mx_TagPanel .mx_TagTile.mx_TagTile_selected .mx_TagTile_avatar .mx_BaseAvatar { + border: 3px solid $accent-color; + background-color: $accent-color; + border-radius: 60px; + + /* In case this is a "initial" avatar */ + display: block; + height: 35px; + width: 35px; +} + +.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { + filter: none; +} + +.mx_TagTile_tooltip { + position: relative; + top: -30px; + left: 5px; +} + +.mx_TagTile_context_button { + min-width: 15px; + height: 15px; + position: absolute; + right: -5px; + top: 1px; + border-radius: 8px; + background-color: $neutral-badge-color; + color: #ffffff; + font-weight: 600; + font-size: 10px; + text-align: center; + padding-top: 1px; + padding-left: 4px; + padding-right: 4px; +} + +.mx_TagPanel_groupsButton { + margin-bottom: 17px; + margin-top: 18px; + height: 25px; +} + +.mx_TagPanel_groupsButton object { + pointer-events: none; +} diff --git a/res/css/structures/_UploadBar.scss b/res/css/structures/_UploadBar.scss new file mode 100644 index 0000000000..d76c81668c --- /dev/null +++ b/res/css/structures/_UploadBar.scss @@ -0,0 +1,61 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_UploadBar { + position: relative; +} + +.mx_UploadBar_uploadProgressOuter { + height: 5px; + margin-left: 63px; + margin-top: -1px; + padding-bottom: 5px; +} + +.mx_UploadBar_uploadProgressInner { + background-color: $accent-color; + height: 5px; +} + +.mx_UploadBar_uploadFilename { + margin-top: 5px; + margin-left: 65px; + opacity: 0.5; + color: $primary-fg-color; +} + +.mx_UploadBar_uploadIcon { + float: left; + margin-top: 5px; + margin-left: 14px; +} + +.mx_UploadBar_uploadCancel { + float: right; + margin-top: 5px; + margin-right: 10px; + position: relative; + opacity: 0.6; + cursor: pointer; + z-index: 1; +} + +.mx_UploadBar_uploadBytes { + float: right; + margin-top: 5px; + margin-right: 30px; + color: $accent-color; +} diff --git a/res/css/structures/_UserSettings.scss b/res/css/structures/_UserSettings.scss new file mode 100644 index 0000000000..6fae8d6c1a --- /dev/null +++ b/res/css/structures/_UserSettings.scss @@ -0,0 +1,257 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +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. +*/ + +.mx_UserSettings { + max-width: 960px; + width: 100%; + margin-left: auto; + margin-right: auto; + + display: flex; + flex-direction: column; +} + +.mx_UserSettings .mx_RoomHeader { + order: 1; + + flex: 0 0 70px; +} + +.mx_UserSettings_body { + order: 2; + + flex: 1 1 0; + + margin-top: -20px; + overflow-y: auto; +} + +.mx_UserSettings h3 { + clear: both; + margin-left: 63px; + text-transform: uppercase; + color: $h3-color; + font-weight: 600; + font-size: 13px; + margin-top: 26px; + margin-bottom: 10px; +} + +.mx_UserSettings_section h3 { + margin-left: 0px; +} + +.mx_UserSettings_spinner { + display: inline-block; + vertical-align: middle; + margin-right: 12px; + width: 32px; + height: 32px; +} + +.mx_UserSettings_button { + @mixin mx_DialogButton; + display: inline; + margin: auto; +} + +.mx_UserSettings_button:hover { + @mixin mx_DialogButton_hover; +} + +.mx_UserSettings_button.danger { + background-color: $warning-color; +} + +.mx_UserSettings_section { + margin-left: 63px; + margin-top: 28px; + margin-bottom: 28px; +} + +.mx_UserSettings_cryptoSection ul { + display: table; +} +.mx_UserSettings_cryptoSection li { + display: table-row; +} +.mx_UserSettings_cryptoSection label, +.mx_UserSettings_cryptoSection span { + display: table-cell; + padding-right: 1em; +} + +.mx_UserSettings_passwordWarning { + /* To move the "Sign out" button out of the way */ + clear: both; + color: $warning-color; + margin-bottom: 5px; +} + +.mx_UserSettings_importExportButtons { + padding-top: 10px; + padding-left: 40px; +} + +.mx_UserSettings_importExportButtons .mx_UserSettings_button { + margin-right: 1em; +} + +.mx_UserSettings_toggle input { + width: 16px; + margin-right: 8px; + margin-bottom: 8px; +} + +.mx_UserSettings_toggle label { + padding-bottom: 21px; +} + +.mx_UserSettings_accountTable +.mx_UserSettings_notifTable +{ + display: table; +} + +.mx_UserSettings_notifTable .mx_Spinner { + position: absolute; +} + +.mx_UserSettings_language { + width: 200px; +} + +.mx_UserSettings_webRtcDevices_dropdown { + width: 50%; +} + +.mx_UserSettings_profileTable +{ + display: table; + float: left; +} + +.mx_UserSettings_profileTableRow +{ + display: table-row; +} + +.mx_UserSettings_profileLabelCell +{ + padding-bottom: 21px; + display: table-cell; + font-weight: bold; + padding-right: 24px; +} + +.mx_UserSettings_profileInputCell { + display: table-cell; + padding-bottom: 21px; + width: 240px; +} + +.mx_UserSettings_profileInputCell input, +.mx_UserSettings_profileInputCell .mx_EditableText +{ + display: inline-block; + border: 0px; + border-bottom: 1px solid $input-underline-color; + padding: 0px; + width: 240px; + color: $input-fg-color; + font-family: 'Open Sans', Helvetica, Arial, Sans-Serif; + font-size: 16px; +} + +.mx_UserSettings_threepidButton { + display: table-cell; + padding-left: 0.5em; + position: relative; + cursor: pointer; +} + +.mx_UserSettings_phoneSection { + display:table; +} + +.mx_UserSettings_phoneCountry { + width: 70px; + display: table-cell; +} + +input.mx_UserSettings_phoneNumberField { + margin-left: 3px; + width: 172px; + border: 1px solid transparent; +} + +.mx_UserSettings_changePasswordButton { + float: right; + margin-right: 32px; + margin-left: 32px; +} + +.mx_UserSettings_logout { + float: right; + margin-right: 32px; + margin-left: 32px; +} + +.mx_UserSettings_avatarPicker { + margin-left: 32px; + margin-right: 32px; + float: right; + cursor: pointer; +} + +.mx_UserSettings_avatarPicker_img .mx_BaseAvatar_image { + object-fit: cover; +} + +.mx_UserSettings_avatarPicker_edit { + text-align: center; + margin-top: 10px; +} + +.mx_UserSettings_avatarPicker_edit img { + cursor: pointer; +} + +.mx_UserSettings_avatarPicker_edit > input { + display: none; +} + +.mx_UserSettings_avatarPicker_imgContainer { + display: inline-block; +} + +.mx_UserSettings_avatarPicker_remove { + display: inline-block; + float: right; + margin-right: -15px; +} + +.mx_UserSettings_advanced_spoiler, +.mx_UserSettings_link { + cursor: pointer; + color: $accent-color; + word-break: break-all; +} + +.mx_UserSettings_analyticsModal table { + margin: 10px 0px; +} diff --git a/res/css/structures/_ViewSource.scss b/res/css/structures/_ViewSource.scss new file mode 100644 index 0000000000..a4c7dcf58a --- /dev/null +++ b/res/css/structures/_ViewSource.scss @@ -0,0 +1,23 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_ViewSource pre { + text-align: left; + font-size: 12px; + padding: 0.5em 1em 0.5em 1em; + word-wrap: break-word; + white-space: pre-wrap; +} diff --git a/res/css/structures/login/_Login.scss b/res/css/structures/login/_Login.scss new file mode 100644 index 0000000000..84b8306a74 --- /dev/null +++ b/res/css/structures/login/_Login.scss @@ -0,0 +1,284 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_Login { + width: 100%; + height: 100%; + + display: flex; + align-items: center; + justify-content: center; + + overflow: auto; +} + +.mx_Login h2 { + font-weight: 300; + margin-top: 32px; + margin-bottom: 20px; +} + +.mx_Login_box { + width: 300px; + min-height: 450px; + padding-top: 50px; + padding-bottom: 50px; + margin: auto; +} + +.mx_Login_logo { + text-align: center; + height: 150px; + margin-bottom: 45px; +} + +.mx_Login_logo img { + max-height: 100% +} + +.mx_Login_support { + text-align: center; + font-size: 13px; + margin-top: 0px; + opacity: 0.7; +} + +.mx_Login_field { + width: 280px; + border-radius: 3px; + border: 1px solid $strong-input-border-color; + font-weight: 300; + font-size: 13px; + padding: 9px; + margin-bottom: 14px; +} + +.mx_Login_field_disabled { + opacity: 0.3; +} + +.mx_Login_fieldLabel { + margin-top: -10px; + margin-left: 8px; + margin-bottom: 14px; + font-size: 13px; + opacity: 0.8; +} + +.mx_Login_submit { + @mixin mx_DialogButton; + width: 100%; + margin-top: 35px; + margin-bottom: 24px; +} + +.mx_Login_submit:hover { + @mixin mx_DialogButton_hover; +} + +.mx_Login_submit:disabled { + opacity: 0.3; +} + +.mx_Login_label { + font-size: 13px; + opacity: 0.8; +} + +.mx_Login_checkbox, +.mx_Login_radio { + margin-right: 10px; +} + +.mx_Login_create { + display: block; + text-align: center; + width: 100%; + font-size: 13px; + opacity: 0.8; +} + +.mx_Login_create:link { + color: $primary-fg-color; +} + +.mx_Login_links { + display: block; + text-align: center; + margin-top: 15px; + width: 100%; + font-size: 13px; + opacity: 0.8; +} + +.mx_Login_links a:link { + color: $primary-fg-color; +} + +.mx_Login_prompt { + padding-top: 15px; + padding-bottom: 15px; + font-size: 13px; +} + +.mx_Login_forgot { + font-size: 15px; +} + +.mx_Login_forgot:link { + color: $primary-fg-color; +} + +.mx_Login_loader { + display: inline; + position: relative; + top: 2px; + left: 8px; +} + +.mx_Login_loader .mx_Spinner { + display: inline; +} + +.mx_Login_loader .mx_Spinner img { + width: 16px; + height: 16px; +} + +.mx_Login_error { + color: $warning-color; + font-weight: bold; + text-align: center; +/* + height: 24px; +*/ + margin-top: 12px; + margin-bottom: 12px; +} + +.mx_Login_type_container { + display: flex; + margin-bottom: 14px; +} + +.mx_Login_type_label { + flex-grow: 1; + line-height: 35px; +} + +.mx_Login_type_dropdown { + display: inline-block; + min-width: 170px; + align-self: flex-end; + flex: 1 1 auto; +} + +.mx_Login_field_group { + display: flex; +} + +.mx_Login_field_prefix { + height: 34px; + padding: 0px 5px; + line-height: 33px; + + background-color: #eee; + border: 1px solid #c7c7c7; + border-right: 0px; + border-radius: 3px 0px 0px 3px; + + text-align: center; +} + +.mx_Login_field_suffix { + height: 34px; + padding: 0px 5px; + line-height: 33px; + + background-color: #eee; + border: 1px solid #c7c7c7; + border-left: 0px; + border-radius: 0px 3px 3px 0px; + + text-align: center; + flex-grow: 1; +} + +.mx_Login_username { + height: 16px; + flex-shrink: 1; + min-width: 0px; +} + +.mx_Login_phoneNumberField { + height: 16px; +} + +.mx_Login_field_has_prefix { + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; +} + +.mx_Login_field_has_suffix { + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; +} + +.mx_Login_phoneSection { + display:flex; +} + +.mx_Login_phoneCountry { + margin-bottom: 14px; + width: 150px; + + /* To override mx_Login_field_prefix */ + text-align: left; + padding: 0px; + background-color: $primary-bg-color; +} + +.mx_Login_field_prefix .mx_Dropdown_input { + /* To use prefix border instead of dropdown border */ + border: 0; +} + +.mx_Login_phoneCountry .mx_Dropdown_option { + /* + To match height of mx_Login_field + 33px + 2px border from mx_Dropdown_option = 35px + */ + height: 33px; + line-height: 33px; +} + +.mx_Login_phoneCountry .mx_Dropdown_option img { + margin: 3px; + vertical-align: top; +} + +.mx_Login_language { + margin-left: auto; + margin-right: auto; + min-width: 60%; +} + +.mx_Login_language_div { + display: flex; + margin-top: 12px; + margin-bottom: 12px; +} + diff --git a/res/css/views/avatars/_BaseAvatar.scss b/res/css/views/avatars/_BaseAvatar.scss new file mode 100644 index 0000000000..ee2d9c190f --- /dev/null +++ b/res/css/views/avatars/_BaseAvatar.scss @@ -0,0 +1,35 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_BaseAvatar { + position: relative; +} + +.mx_BaseAvatar_initial { + position: absolute; + left: 0px; + color: $avatar-initial-color; + text-align: center; + speak: none; + pointer-events: none; + font-weight: normal; +} + +.mx_BaseAvatar_image { + border-radius: 40px; + vertical-align: top; + background-color: $avatar-bg-color; +} diff --git a/res/css/views/context_menus/_MessageContextMenu.scss b/res/css/views/context_menus/_MessageContextMenu.scss new file mode 100644 index 0000000000..85e8080c88 --- /dev/null +++ b/res/css/views/context_menus/_MessageContextMenu.scss @@ -0,0 +1,25 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_MessageContextMenu_field { + padding: 3px 6px 3px 6px; + cursor: pointer; + white-space: nowrap; +} + +.mx_MessageContextMenu_field.mx_MessageContextMenu_fieldSet { + font-weight: bold; +} diff --git a/res/css/views/context_menus/_RoomTileContextMenu.scss b/res/css/views/context_menus/_RoomTileContextMenu.scss new file mode 100644 index 0000000000..598f8ac249 --- /dev/null +++ b/res/css/views/context_menus/_RoomTileContextMenu.scss @@ -0,0 +1,114 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_RoomTileContextMenu_tag_field, .mx_RoomTileContextMenu_leave { + padding-top: 8px; + padding-right: 20px; + padding-bottom: 8px; + cursor: pointer; + white-space: nowrap; + display: flex; + align-items: center; + line-height: 16px; +} + +.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldSet { + font-weight: bold; +} + +.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldSet .mx_RoomTileContextMenu_tag_icon { + display: none; +} + +.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldSet .mx_RoomTileContextMenu_tag_icon_set { + display: inline-block; +} + +.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldDisabled { + color: rgba(0, 0, 0, 0.2); +} + +.mx_RoomTileContextMenu_tag_icon { + padding-right: 8px; + padding-left: 4px; + display: inline-block +} + +.mx_RoomTileContextMenu_tag_icon_set { + padding-right: 8px; + padding-left: 4px; + display: none; +} + +.mx_RoomTileContextMenu_separator { + margin-top: 0; + margin-bottom: 0; + border-bottom-style: none; + border-left-style: none; + border-right-style: none; + border-top-style: solid; + border-top-width: 1px; + border-color: $menu-border-color; +} + +.mx_RoomTileContextMenu_leave { + color: $warning-color; +} + +.mx_RoomTileContextMenu_tag_fieldSet .mx_RoomTileContextMenu_tag_icon { + /* Something to indicate that the icon is the set tag */ +} + +.mx_RoomTileContextMenu_notif_picker { + position: absolute; + top: 16px; + left: 5px; +} + +.mx_RoomTileContextMenu_notif_field { + padding-top: 4px; + padding-right: 6px; + padding-bottom: 10px; + padding-left: 8px; /* 20px */ + cursor: pointer; + white-space: nowrap; + display: flex; + align-items: center; +} + +.mx_RoomTileContextMenu_notif_field.mx_RoomTileContextMenu_notif_fieldSet { + font-weight: bold; +} + +.mx_RoomTileContextMenu_notif_field.mx_RoomTileContextMenu_notif_fieldDisabled { + color: rgba(0, 0, 0, 0.2); +} + +.mx_RoomTileContextMenu_notif_icon { + padding-right: 4px; + padding-left: 4px; +} + +.mx_RoomTileContextMenu_notif_activeIcon { + display: inline-block; + opacity: 0; + position: relative; + left: -5px; +} + +.mx_RoomTileContextMenu_notif_fieldSet .mx_RoomTileContextMenu_notif_activeIcon { + opacity: 1; +} diff --git a/res/css/views/context_menus/_TagTileContextMenu.scss b/res/css/views/context_menus/_TagTileContextMenu.scss new file mode 100644 index 0000000000..759b92bd68 --- /dev/null +++ b/res/css/views/context_menus/_TagTileContextMenu.scss @@ -0,0 +1,44 @@ +/* +Copyright 2018 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. +*/ + +.mx_TagTileContextMenu_item { + padding-top: 8px; + padding-right: 20px; + padding-bottom: 8px; + cursor: pointer; + white-space: nowrap; + display: flex; + align-items: center; + line-height: 16px; +} + + +.mx_TagTileContextMenu_item_icon { + padding-right: 8px; + padding-left: 4px; + display: inline-block +} + +.mx_TagTileContextMenu_separator { + margin-top: 0; + margin-bottom: 0; + border-bottom-style: none; + border-left-style: none; + border-right-style: none; + border-top-style: solid; + border-top-width: 1px; + border-color: $menu-border-color; +} diff --git a/res/css/views/dialogs/_BugReportDialog.scss b/res/css/views/dialogs/_BugReportDialog.scss new file mode 100644 index 0000000000..e00d446eda --- /dev/null +++ b/res/css/views/dialogs/_BugReportDialog.scss @@ -0,0 +1,52 @@ +/* +Copyright 2017 OpenMarket 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. +*/ + +.mx_BugReportDialog_field_container { + display: flex; +} + +.mx_BugReportDialog_field_label { + flex-basis: 150px; + + text-align: right; + + padding-top: 9px; + padding-right: 4px; + + line-height: 18px; +} + +.mx_BugReportDialog_field_input { + flex-grow: 1; + + /* taken from mx_ChatInviteDialog_inputContainer */ + border-radius: 3px; + border: solid 1px $input-border-color; + + font-size: 14px; + + padding-left: 4px; + padding-right: 4px; + padding-top: 7px; + padding-bottom: 7px; + + margin-bottom: 4px; +} + +.mx_BugReportDialog_field_input[type="text" i] { + padding-top: 9px; + padding-bottom: 9px; +} diff --git a/res/css/views/dialogs/_ChangelogDialog.scss b/res/css/views/dialogs/_ChangelogDialog.scss new file mode 100644 index 0000000000..460a5f94b1 --- /dev/null +++ b/res/css/views/dialogs/_ChangelogDialog.scss @@ -0,0 +1,24 @@ +/* +Copyright 2016 Aviral Dasgupta + +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. +*/ + +.mx_ChangelogDialog_content { + max-height: 300px; + overflow: auto; +} + +.mx_ChangelogDialog_li { + padding: 0.2em; +} diff --git a/res/css/views/dialogs/_ChatCreateOrReuseChatDialog.scss b/res/css/views/dialogs/_ChatCreateOrReuseChatDialog.scss new file mode 100644 index 0000000000..0f358a588e --- /dev/null +++ b/res/css/views/dialogs/_ChatCreateOrReuseChatDialog.scss @@ -0,0 +1,41 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +.mx_ChatCreateOrReuseDialog .mx_ChatCreateOrReuseDialog_tiles { + margin-top: 24px; +} + +.mx_ChatCreateOrReuseDialog .mx_Dialog_content { + margin-bottom: 24px; + + /* + To stop spinner that mx_ChatCreateOrReuseDialog_profile replaces from causing a + height change + */ + min-height: 100px; +} + +.mx_ChatCreateOrReuseDialog .mx_RoomTile_badge { + display: none; +} + +.mx_ChatCreateOrReuseDialog_profile { + display: flex; +} + +.mx_ChatCreateOrReuseDialog_profile_name { + padding: 14px; +} diff --git a/res/css/views/dialogs/_ChatInviteDialog.scss b/res/css/views/dialogs/_ChatInviteDialog.scss new file mode 100644 index 0000000000..6fc211743d --- /dev/null +++ b/res/css/views/dialogs/_ChatInviteDialog.scss @@ -0,0 +1,77 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +.mx_ChatInviteDialog { + /* XXX: padding-left is on mx_Dialog but padding-right has subsequently + * been added on other dialogs. Surely all our dialogs should have consistent + * right hand padding? + */ + padding-right: 58px; +} + +/* Using a textarea for this element, to circumvent autofill */ +.mx_ChatInviteDialog_input, +.mx_ChatInviteDialog_input:focus +{ + height: 26px; + font-size: 14px; + font-family: $font-family; + padding-left: 12px; + padding-right: 12px; + margin: 0 !important; + border: 0 !important; + outline: 0 !important; + width: 1000%; /* Pretend that this is an "input type=text" */ + resize: none; + overflow: hidden; + vertical-align: middle; + box-sizing: border-box; + word-wrap: nowrap; +} + +.mx_ChatInviteDialog .mx_Dialog_content { + min-height: 50px +} + +.mx_ChatInviteDialog_inputContainer { + border-radius: 3px; + border: solid 1px $input-border-color; + line-height: 36px; + padding-left: 4px; + padding-right: 4px; + padding-top: 1px; + padding-bottom: 1px; + max-height: 150px; + overflow-x: hidden; + overflow-y: auto; +} + +.mx_ChatInviteDialog_error { + margin-top: 10px; + color: $warning-color; +} + +.mx_ChatInviteDialog_cancel { + position: absolute; + right: 11px; + top: 13px; + cursor: pointer; +} + +.mx_ChatInviteDialog_cancel object { + pointer-events: none; +} + diff --git a/res/css/views/dialogs/_ConfirmUserActionDialog.scss b/res/css/views/dialogs/_ConfirmUserActionDialog.scss new file mode 100644 index 0000000000..b859d6bf4d --- /dev/null +++ b/res/css/views/dialogs/_ConfirmUserActionDialog.scss @@ -0,0 +1,53 @@ +/* +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. +*/ + +.mx_ConfirmUserActionDialog .mx_Dialog_content { + min-height: 48px; + margin-bottom: 24px; +} + +.mx_ConfirmUserActionDialog_avatar { + float: left; + margin-right: 20px; + margin-top: -2px; +} + +.mx_ConfirmUserActionDialog_name { + font-size: 18px; +} + +.mx_ConfirmUserActionDialog_userId { + font-size: 13px; +} + +.mx_ConfirmUserActionDialog_reasonField { + font-family: $font-family; + font-size: 14px; + color: $primary-fg-color; + background-color: $primary-bg-color; + + border-radius: 3px; + border: solid 1px $input-border-color; + line-height: 36px; + padding-left: 16px; + padding-right: 16px; + padding-top: 1px; + padding-bottom: 1px; + + margin-bottom: 24px; + + width: 90%; +} diff --git a/res/css/views/dialogs/_CreateGroupDialog.scss b/res/css/views/dialogs/_CreateGroupDialog.scss new file mode 100644 index 0000000000..500e12ee49 --- /dev/null +++ b/res/css/views/dialogs/_CreateGroupDialog.scss @@ -0,0 +1,62 @@ +/* +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. +*/ + +.mx_CreateGroupDialog_inputRow { + margin-top: 10px; + margin-bottom: 10px; +} + +.mx_CreateGroupDialog_label { + text-align: left; + padding-bottom: 12px; +} + +.mx_CreateGroupDialog_input { + font-size: 15px; + border-radius: 3px; + border: 1px solid $input-border-color; + padding: 9px; + color: $primary-fg-color; + background-color: $primary-bg-color; +} + +.mx_CreateGroupDialog_input_hasPrefixAndSuffix { + border-radius: 0px; +} + +.mx_CreateGroupDialog_input_group { + display: flex; +} + +.mx_CreateGroupDialog_prefix, +.mx_CreateGroupDialog_suffix { + height: 35px; + padding: 0px 5px; + line-height: 37px; + background-color: $input-border-color; + border: 1px solid $input-border-color; + text-align: center; +} + +.mx_CreateGroupDialog_prefix { + border-right: 0px; + border-radius: 3px 0px 0px 3px; +} + +.mx_CreateGroupDialog_suffix { + border-left: 0px; + border-radius: 0px 3px 3px 0px; +} diff --git a/res/css/views/dialogs/_CreateRoomDialog.scss b/res/css/views/dialogs/_CreateRoomDialog.scss new file mode 100644 index 0000000000..05d5bfcebf --- /dev/null +++ b/res/css/views/dialogs/_CreateRoomDialog.scss @@ -0,0 +1,38 @@ +/* +Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_CreateRoomDialog_details_summary { + outline: none; +} + +.mx_CreateRoomDialog_label { + text-align: left; + padding-bottom: 12px; +} + +.mx_CreateRoomDialog_input_container { + padding-right: 20px; +} + +.mx_CreateRoomDialog_input { + font-size: 15px; + border-radius: 3px; + border: 1px solid $input-border-color; + padding: 9px; + color: $primary-fg-color; + background-color: $primary-bg-color; + width: 100%; +} diff --git a/res/css/views/dialogs/_DeactivateAccountDialog.scss b/res/css/views/dialogs/_DeactivateAccountDialog.scss new file mode 100644 index 0000000000..dc76da5b15 --- /dev/null +++ b/res/css/views/dialogs/_DeactivateAccountDialog.scss @@ -0,0 +1,23 @@ +/* +Copyright 2018 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. +*/ + +.mx_DeactivateAccountDialog .mx_Dialog_content { + margin-bottom: 30px; +} + +.mx_DeactivateAccountDialog .mx_DeactivateAccountDialog_input_section { + margin-top: 60px; +} diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss new file mode 100644 index 0000000000..a4a868bd11 --- /dev/null +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -0,0 +1,177 @@ +/* +Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_DevTools_content { + margin: 10px 0; +} + +.mx_DevTools_RoomStateExplorer_button, .mx_DevTools_RoomStateExplorer_query { + margin-bottom: 10px; + width: 100%; +} + +.mx_DevTools_label_left { + float: left; +} + +.mx_DevTools_label_right { + float: right; +} + +.mx_DevTools_label_bottom { + clear: both; + border-bottom: 1px solid #e5e5e5; +} + +.mx_DevTools_inputRow +{ + display: table-row; +} + +.mx_DevTools_inputLabelCell +{ + display: table-cell; + font-weight: bold; + padding-right: 24px; +} + +.mx_DevTools_inputCell { + display: table-cell; + width: 240px; +} + +.mx_DevTools_inputCell input +{ + display: inline-block; + border: 0; + border-bottom: 1px solid $input-underline-color; + padding: 0; + width: 240px; + color: $input-fg-color; + font-family: 'Open Sans', Helvetica, Arial, Sans-Serif; + font-size: 16px; +} + +.mx_DevTools_textarea { + font-size: 12px; + max-width: 624px; + min-height: 250px; + padding: 10px; + width: 100%; +} + +.mx_DevTools_tgl { + display: none; + + // add default box-sizing for this scope + &, + &:after, + &:before, + & *, + & *:after, + & *:before, + & + .mx_DevTools_tgl-btn { + box-sizing: border-box; + &::selection { + background: none; + } + } + + + .mx_DevTools_tgl-btn { + outline: 0; + display: block; + width: 7em; + height: 2em; + position: relative; + cursor: pointer; + user-select: none; + &:after, + &:before { + position: relative; + display: block; + content: ""; + width: 50%; + height: 100%; + } + + &:after { + left: 0; + } + + &:before { + display: none; + } + } + + &:checked + .mx_DevTools_tgl-btn:after { + left: 50%; + } +} + +.mx_DevTools_tgl-flip { + + .mx_DevTools_tgl-btn { + padding: 2px; + transition: all .2s ease; + font-family: sans-serif; + perspective: 100px; + &:after, + &:before { + display: inline-block; + transition: all .4s ease; + width: 100%; + text-align: center; + position: absolute; + line-height: 2em; + font-weight: bold; + color: #fff; + top: 0; + left: 0; + backface-visibility: hidden; + border-radius: 4px; + } + + &:after { + content: attr(data-tg-on); + background: #02C66F; + transform: rotateY(-180deg); + } + + &:before { + background: #FF3A19; + content: attr(data-tg-off); + } + + &:active:before { + transform: rotateY(-20deg); + } + } + + &:checked + .mx_DevTools_tgl-btn { + &:before { + transform: rotateY(180deg); + } + + &:after { + transform: rotateY(0); + left: 0; + background: #7FC6A6; + } + + &:active:after { + transform: rotateY(20deg); + } + } +} diff --git a/res/css/views/dialogs/_EncryptedEventDialog.scss b/res/css/views/dialogs/_EncryptedEventDialog.scss new file mode 100644 index 0000000000..b4dd353370 --- /dev/null +++ b/res/css/views/dialogs/_EncryptedEventDialog.scss @@ -0,0 +1,27 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +.mx_EncryptedEventDialog .mx_MemberDeviceInfo { + float: right; + padding: 0px; + margin-right: 42px; +} + +.mx_EncryptedEventDialog .mx_MemberDeviceInfo_textButton { + @mixin mx_DialogButton; + background-color: $primary-bg-color; + color: $accent-color; +} \ No newline at end of file diff --git a/res/css/views/dialogs/_GroupAddressPicker.scss b/res/css/views/dialogs/_GroupAddressPicker.scss new file mode 100644 index 0000000000..d6c961c0ec --- /dev/null +++ b/res/css/views/dialogs/_GroupAddressPicker.scss @@ -0,0 +1,25 @@ +/* +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. +*/ + +.mx_GroupAddressPicker_checkboxContainer{ + margin-top: 10px; + display: flex; +} + +.mx_GroupAddressPicker_checkboxContainer input[type="checkbox"] { + /* Stop flex from shrinking the checkbox */ + width: 20px; +} diff --git a/res/css/views/dialogs/_QuestionDialog.scss b/res/css/views/dialogs/_QuestionDialog.scss new file mode 100644 index 0000000000..3d47f17592 --- /dev/null +++ b/res/css/views/dialogs/_QuestionDialog.scss @@ -0,0 +1,18 @@ +/* +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. +*/ +.mx_QuestionDialog { + padding-right: 58px; +} diff --git a/res/css/views/dialogs/_RoomUpgradeDialog.scss b/res/css/views/dialogs/_RoomUpgradeDialog.scss new file mode 100644 index 0000000000..2e3ac5fdea --- /dev/null +++ b/res/css/views/dialogs/_RoomUpgradeDialog.scss @@ -0,0 +1,19 @@ +/* +Copyright 2018 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. +*/ + +.mx_RoomUpgradeDialog { + padding-right: 70px; +} diff --git a/res/css/views/dialogs/_SetEmailDialog.scss b/res/css/views/dialogs/_SetEmailDialog.scss new file mode 100644 index 0000000000..588f10c9cb --- /dev/null +++ b/res/css/views/dialogs/_SetEmailDialog.scss @@ -0,0 +1,36 @@ +/* +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. +*/ + +.mx_SetEmailDialog_email_input { + border-radius: 3px; + border: 1px solid $input-border-color; + padding: 9px; + color: $input-fg-color; + background-color: $primary-bg-color; + font-size: 15px; + width: 100%; + max-width: 280px; + margin-bottom: 10px; +} + +.mx_SetEmailDialog_email_input:focus { + outline: none; + box-shadow: none; + border: 1px solid $accent-color; +} + +.mx_SetEmailDialog_email_input_placeholder { +} diff --git a/res/css/views/dialogs/_SetMxIdDialog.scss b/res/css/views/dialogs/_SetMxIdDialog.scss new file mode 100644 index 0000000000..f7d8a3d001 --- /dev/null +++ b/res/css/views/dialogs/_SetMxIdDialog.scss @@ -0,0 +1,50 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +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. +*/ + +.mx_SetMxIdDialog .mx_Dialog_title { + padding-right: 40px; +} + +.mx_SetMxIdDialog_input_group { + display: flex; +} + +.mx_SetMxIdDialog_input { + border-radius: 3px; + border: 1px solid $input-border-color; + padding: 9px; + color: $primary-fg-color; + background-color: $primary-bg-color; + font-size: 15px; + width: 100%; + max-width: 280px; +} + +.mx_SetMxIdDialog_input.error, +.mx_SetMxIdDialog_input.error:focus { + border: 1px solid $warning-color; +} + +.mx_SetMxIdDialog_input_group .mx_Spinner { + height: 37px; + padding-left: 10px; + justify-content: flex-start; +} + +.mx_SetMxIdDialog .success { + color: $accent-color; +} diff --git a/res/css/views/dialogs/_SetPasswordDialog.scss b/res/css/views/dialogs/_SetPasswordDialog.scss new file mode 100644 index 0000000000..28a8b7c9d7 --- /dev/null +++ b/res/css/views/dialogs/_SetPasswordDialog.scss @@ -0,0 +1,35 @@ +/* +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. +*/ + +.mx_SetPasswordDialog_change_password input { + border-radius: 3px; + border: 1px solid $input-border-color; + padding: 9px; + color: $primary-fg-color; + background-color: $primary-bg-color; + font-size: 15px; + width: 100%; + max-width: 280px; + margin-bottom: 10px; +} + +.mx_SetPasswordDialog_change_password_button { + margin-top: 68px; +} + +.mx_SetPasswordDialog .mx_Dialog_content { + margin-bottom: 0px; +} diff --git a/res/css/views/dialogs/_ShareDialog.scss b/res/css/views/dialogs/_ShareDialog.scss new file mode 100644 index 0000000000..116bef8dfd --- /dev/null +++ b/res/css/views/dialogs/_ShareDialog.scss @@ -0,0 +1,89 @@ +/* +Copyright 2018 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. +*/ + +.mx_ShareDialog { + // this is to center the content + padding-right: 58px; +} + +.mx_ShareDialog hr { + margin-top: 25px; + margin-bottom: 25px; + border-color: $light-fg-color; +} + +.mx_ShareDialog_content { + margin: 10px 0; +} + +.mx_ShareDialog_matrixto { + display: flex; + justify-content: space-between; + border-radius: 5px; + border: solid 1px $light-fg-color; + margin-bottom: 10px; + margin-top: 30px; + padding: 10px; +} + +.mx_ShareDialog_matrixto a { + text-decoration: none; +} + +.mx_ShareDialog_matrixto_link { + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; +} + +.mx_ShareDialog_matrixto_copy { + flex-shrink: 0; + cursor: pointer; + margin-left: 20px; + display: inherit; +} +.mx_ShareDialog_matrixto_copy > div { + background-image: url($copy-button-url); + margin-left: 5px; + width: 20px; + height: 20px; +} + +.mx_ShareDialog_split { + display: flex; + flex-wrap: wrap; +} + +.mx_ShareDialog_qrcode_container { + float: left; + background-color: #ffffff; + padding: 5px; // makes qr code more readable in dark theme + border-radius: 5px; + height: 256px; + width: 256px; + margin-right: 64px; +} + +.mx_ShareDialog_social_container { + display: inline-block; + width: 299px; +} + +.mx_ShareDialog_social_icon { + display: inline-grid; + margin-right: 10px; + margin-bottom: 10px; +} diff --git a/res/css/views/dialogs/_UnknownDeviceDialog.scss b/res/css/views/dialogs/_UnknownDeviceDialog.scss new file mode 100644 index 0000000000..3457e50b92 --- /dev/null +++ b/res/css/views/dialogs/_UnknownDeviceDialog.scss @@ -0,0 +1,54 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +// CSS voodoo to support a gemini-scrollbar for the contents of the dialog +.mx_Dialog_unknownDevice .mx_Dialog { + // ideally we'd shrink the height to fit when needed, but in practice this + // is a pain in the ass. plus might as well make the dialog big given how + // important it is. + height: 100%; + + // position the gemini scrollbar nicely + padding-right: 58px; +} + +.mx_UnknownDeviceDialog { + height: 100%; + display: flex; + flex-direction: column; +} + +.mx_UnknownDeviceDialog .mx_Dialog_content { + margin-bottom: 24px; +} + +.mx_UnknownDeviceDialog .mx_MemberDeviceInfo { + float: right; + clear: both; + padding: 0px; + padding-top: 8px; +} + +.mx_UnknownDeviceDialog .mx_MemberDeviceInfo_textButton { + @mixin mx_DialogButton_small; + background-color: $primary-bg-color; + color: $accent-color; +} + +.mx_UnknownDeviceDialog .mx_UnknownDeviceDialog_deviceList li { + height: 40px; + border-bottom: 1px solid $primary-hairline-color; +} \ No newline at end of file diff --git a/res/css/views/directory/_NetworkDropdown.scss b/res/css/views/directory/_NetworkDropdown.scss new file mode 100644 index 0000000000..9850379597 --- /dev/null +++ b/res/css/views/directory/_NetworkDropdown.scss @@ -0,0 +1,84 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_NetworkDropdown { + position: relative; +} + +.mx_NetworkDropdown_input { + position: relative; + border-radius: 3px; + border: 1px solid $strong-input-border-color; + font-weight: 300; + font-size: 13px; + user-select: none; +} + +.mx_NetworkDropdown_arrow { + border-color: $primary-fg-color transparent transparent; + border-style: solid; + border-width: 5px 5px 0; + display: block; + height: 0; + position: absolute; + right: 10px; + top: 14px; + width: 0 +} + +.mx_NetworkDropdown_networkoption { + height: 35px; + line-height: 35px; + padding-left: 8px; + padding-right: 8px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.mx_NetworkDropdown_networkoption img { + margin: 5px; + width: 25px; + vertical-align: middle; +} + +input.mx_NetworkDropdown_networkoption, input.mx_NetworkDropdown_networkoption:focus { + border: 0; + padding-top: 0; + padding-bottom: 0; +} + +.mx_NetworkDropdown_menu { + position: absolute; + left: -1px; + right: -1px; + top: 100%; + z-index: 2; + margin: 0; + padding: 0px; + border-radius: 3px; + border: 1px solid $accent-color; + background-color: $primary-bg-color; +} + +.mx_NetworkDropdown_menu .mx_NetworkDropdown_networkoption:hover { + background-color: $focus-bg-color; +} + +.mx_NetworkDropdown_menu_network { + font-weight: bold; +} + diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss new file mode 100644 index 0000000000..edf455049b --- /dev/null +++ b/res/css/views/elements/_AccessibleButton.scss @@ -0,0 +1,24 @@ +/* +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. +*/ + +.mx_AccessibleButton:focus { + outline: 0; + filter: brightness($focus-brightness); +} + +.mx_AccessibleButton { + cursor: pointer; +} \ No newline at end of file diff --git a/res/css/views/elements/_AddressSelector.scss b/res/css/views/elements/_AddressSelector.scss new file mode 100644 index 0000000000..9871a7e881 --- /dev/null +++ b/res/css/views/elements/_AddressSelector.scss @@ -0,0 +1,45 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +.mx_AddressSelector { + position: absolute; + background-color: $primary-bg-color; + width: 485px; + max-height: 116px; + overflow-y: auto; + border-radius: 3px; + background-color: $primary-bg-color; + border: solid 1px $accent-color; + cursor: pointer; +} + +.mx_AddressSelector.mx_AddressSelector_empty { + display: none; +} + +.mx_AddressSelector_addressListElement .mx_AddressTile { + background-color: $primary-bg-color; + border: solid 1px $primary-bg-color; +} + +.mx_AddressSelector_addressListElement.mx_AddressSelector_selected { + background-color: $selected-color; +} + +.mx_AddressSelector_addressListElement.mx_AddressSelector_selected .mx_AddressTile { + background-color: $selected-color; + border: solid 1px $selected-color; +} diff --git a/res/css/views/elements/_AddressTile.scss b/res/css/views/elements/_AddressTile.scss new file mode 100644 index 0000000000..0ecfb17c83 --- /dev/null +++ b/res/css/views/elements/_AddressTile.scss @@ -0,0 +1,138 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_AddressTile { + display: inline-block; + border-radius: 3px; + background-color: rgba(74, 73, 74, 0.1); + border: solid 1px $input-border-color; + line-height: 26px; + color: $primary-fg-color; + font-size: 14px; + font-weight: normal; + margin-right: 4px; +} + +.mx_AddressTile.mx_AddressTile_error { + background-color: rgba(255, 0, 100, 0.1); + color: $warning-color; + border-color: $warning-color; +} + +.mx_AddressTile_network { + display: inline-block; + position: relative; + padding-left: 2px; + padding-right: 4px; + vertical-align: middle; +} + +.mx_AddressTile_avatar { + display: inline-block; + position: relative; + padding-left: 2px; + padding-right: 7px; + vertical-align: middle; +} + +.mx_AddressTile_mx { + display: inline-block; + margin: 0; + border: 0; + padding: 0; +} + +.mx_AddressTile_name { + display: inline-block; + padding-right: 4px; + font-weight: 600; + overflow: hidden; + height: 26px; + vertical-align: middle; +} + +.mx_AddressTile_name.mx_AddressTile_justified { + width: 180px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: middle; +} + +.mx_AddressTile_id { + display: inline-block; + padding-right: 11px; +} + +.mx_AddressTile_id.mx_AddressTile_justified { + width: 200px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: middle; +} + +.mx_AddressTile_unknownMx { + display: inline-block; + font-weight: 600; + padding-right: 11px; +} + +.mx_AddressTile_unknownMxl.mx_AddressTile_justified { + width: 380px; /* name + id width */ + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: middle; +} + +.mx_AddressTile_email { + display: inline-block; + font-weight: 600; + padding-right: 11px; +} + +.mx_AddressTile_email.mx_AddressTile_justified { + width: 200px; /* same as id width */ + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: middle; +} + +.mx_AddressTile_unknown { + display: inline-block; + padding-right: 11px; +} + +.mx_AddressTile_unknown.mx_AddressTile_justified { + width: 380px; /* name + id width */ + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: middle; +} + +.mx_AddressTile_dismiss { + display: inline-block; + padding-right: 11px; + padding-left: 1px; + cursor: pointer; +} + +.mx_AddressTile_dismiss object { + pointer-events: none; +} diff --git a/res/css/views/elements/_DirectorySearchBox.scss b/res/css/views/elements/_DirectorySearchBox.scss new file mode 100644 index 0000000000..94a92b23ce --- /dev/null +++ b/res/css/views/elements/_DirectorySearchBox.scss @@ -0,0 +1,70 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +.mx_DirectorySearchBox { + position: relative; + border-radius: 3px; + border: 1px solid $strong-input-border-color; +} + +.mx_DirectorySearchBox_container { + display: flex; + padding-left: 9px; + padding-right: 9px; +} + +.mx_DirectorySearchBox_input { + flex-grow: 1; + border: 0; + padding: 0; + font-weight: 300; + font-size: 13px; +} +input[type=text].mx_DirectorySearchBox_input:focus { + border: 0; +} + +.mx_DirectorySearchBox_joinButton { + display: table-cell; + padding: 3px; + padding-left: 10px; + padding-right: 10px; + background-color: $plinth-bg-color; + border-radius: 3px; + background-image: url('../../img/icon-return.svg'); + background-position: 8px 70%; + background-repeat: no-repeat; + text-indent: 18px; + font-weight: 600; + font-size: 12px; + user-select: none; + cursor: pointer; +} + +.mx_DirectorySearchBox_clear_wrapper { + display: table-cell; +} + +.mx_DirectorySearchBox_clear { + display: inline-block; + vertical-align: middle; + background: url('../../img/icon_context_delete.svg'); + background-position: 0 50%; + background-repeat: no-repeat; + width: 15px; + height: 15px; + cursor: pointer; +} diff --git a/res/css/views/elements/_Dropdown.scss b/res/css/views/elements/_Dropdown.scss new file mode 100644 index 0000000000..69dd1703ee --- /dev/null +++ b/res/css/views/elements/_Dropdown.scss @@ -0,0 +1,131 @@ +/* +Copyright 2017 OpenMarket 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. +*/ + +.mx_Dropdown { + position: relative; +} + +.mx_Dropdown_disabled { + opacity: 0.3; +} + +.mx_Dropdown_input { + position: relative; + border-radius: 3px; + border: 1px solid $strong-input-border-color; + font-weight: 300; + font-size: 13px; + user-select: none; +} + +.mx_Dropdown_input:focus { + border-color: $accent-color; +} + +/* Disable dropdown highlight on focus */ +.mx_Dropdown_input.mx_AccessibleButton:focus { + filter: none; +} + +.mx_Dropdown_arrow { + border-color: $primary-fg-color transparent transparent; + border-style: solid; + border-width: 5px 5px 0; + display: block; + height: 0; + position: absolute; + right: 10px; + top: 14px; + width: 0 +} + +.mx_Dropdown.left_aligned .mx_Dropdown_arrow { + left: 10px; +} + +.mx_Dropdown_input > .mx_Dropdown_option { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mx_Dropdown.left_aligned .mx_Dropdown_input > .mx_Dropdown_option { + padding-left: 25px; +} + +.mx_Dropdown_option { + height: 35px; + line-height: 35px; + padding-left: 8px; + padding-right: 8px; +} + +.mx_Dropdown_option div { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mx_Dropdown_option img { + margin: 5px; + width: 27px; + vertical-align: middle; +} + +input.mx_Dropdown_option, input.mx_Dropdown_option:focus { + border: 0; + padding-top: 0; + padding-bottom: 0; + // XXX: hack to prevent text box being too big and pushing + // its parent out / overlapping the dropdown arrow. Only really + // works in the Country dropdown. + width: 60%; +} + +.mx_Dropdown_menu { + position: absolute; + left: -1px; + right: -1px; + top: 100%; + z-index: 2; + margin: 0; + padding: 0px; + border-radius: 3px; + border: 1px solid $accent-color; + background-color: $primary-bg-color; + max-height: 200px; + overflow-y: auto; +} + +.mx_Dropdown_menu .mx_Dropdown_option { + height: auto; + min-height: 35px; +} + +.mx_Dropdown_menu .mx_Dropdown_option_highlight { + background-color: $focus-bg-color; +} + +.mx_Dropdown_menu { + font-weight: bold; +} + +.mx_Dropdown_searchPrompt { + font-weight: normal; + margin-left: 5px; + margin-bottom: 5px; +} + diff --git a/res/css/views/elements/_EditableItemList.scss b/res/css/views/elements/_EditableItemList.scss new file mode 100644 index 0000000000..9fbb39aa17 --- /dev/null +++ b/res/css/views/elements/_EditableItemList.scss @@ -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. +*/ + +.mx_EditableItemList { + margin-top: 12px; + margin-bottom: 0px; +} + +.mx_EditableItem { + display: flex; + margin-left: 56px; +} + +.mx_EditableItem .mx_EditableItem_editable { + border: 0px; + border-bottom: 1px solid $strong-input-border-color; + padding: 0px; + min-width: 240px; + max-width: 400px; + margin-bottom: 16px; +} + +.mx_EditableItem .mx_EditableItem_editable:focus { + border-bottom: 1px solid $accent-color; + outline: none; + box-shadow: none; +} + +.mx_EditableItem .mx_EditableItem_editablePlaceholder { + color: $settings-grey-fg-color; +} + +.mx_EditableItem .mx_EditableItem_addButton, +.mx_EditableItem .mx_EditableItem_removeButton { + padding-left: 0.5em; + position: relative; + cursor: pointer; + + visibility: hidden; +} + +.mx_EditableItem:hover .mx_EditableItem_addButton, +.mx_EditableItem:hover .mx_EditableItem_removeButton { + visibility: visible; +} + +.mx_EditableItemList_label { + margin-bottom: 8px; +} diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss new file mode 100644 index 0000000000..8ed0698a72 --- /dev/null +++ b/res/css/views/elements/_ImageView.scss @@ -0,0 +1,134 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +/* This has got to be the most fragile piece of CSS ever written. + But empirically it works on Chrome/FF/Safari + */ + +.mx_ImageView { + display: flex; + width: 100%; + height: 100%; + align-items: center; +} + +.mx_ImageView_lhs { + order: 1; + flex: 1 1 10%; + min-width: 60px; + // background-color: #080; + // height: 20px; +} + +.mx_ImageView_content { + order: 2; + /* min-width hack needed for FF */ + min-width: 0px; + height: 90%; + flex: 15 15 0; + display: flex; + align-items: center; + justify-content: center; +} + +.mx_ImageView_content img { + max-width: 100%; + /* XXX: max-height interacts badly with flex on Chrome and doesn't relayout properly until you refresh */ + max-height: 100%; + /* object-fit hack needed for Chrome due to Chrome not re-laying-out until you refresh */ + object-fit: contain; + /* background-image: url('../../img/trans.png'); */ + pointer-events: all; +} + +.mx_ImageView_labelWrapper { + position: absolute; + top: 0px; + right: 0px; + height: 100%; + overflow: auto; + pointer-events: all; +} + +.mx_ImageView_label { + text-align: left; + display: flex; + justify-content: center; + flex-direction: column; + padding-left: 30px; + padding-right: 30px; + min-height: 100%; + max-width: 240px; + color: $lightbox-fg-color; +} + +.mx_ImageView_cancel { + position: absolute; + top: 0px; + right: 0px; + padding: 35px; + cursor: pointer; +} + +.mx_ImageView_name { + font-size: 18px; + margin-bottom: 6px; + word-wrap: break-word; +} + +.mx_ImageView_metadata { + font-size: 15px; + opacity: 0.5; +} + +.mx_ImageView_download { + display: table; + margin-top: 24px; + margin-bottom: 6px; + border-radius: 5px; + background-color: $lightbox-bg-color; + font-size: 14px; + padding: 9px; + border: 1px solid $lightbox-border-color; +} + +.mx_ImageView_size { + font-size: 11px; +} + +.mx_ImageView_link { + color: $lightbox-fg-color ! important; + text-decoration: none ! important; +} + +.mx_ImageView_button { + font-size: 15px; + opacity: 0.5; + margin-top: 18px; + cursor: pointer; +} + +.mx_ImageView_shim { + height: 30px; +} + +.mx_ImageView_rhs { + order: 3; + flex: 1 1 10%; + min-width: 300px; + // background-color: #800; + // height: 20px; +} diff --git a/res/css/views/elements/_InlineSpinner.scss b/res/css/views/elements/_InlineSpinner.scss new file mode 100644 index 0000000000..612b6209c6 --- /dev/null +++ b/res/css/views/elements/_InlineSpinner.scss @@ -0,0 +1,24 @@ +/* +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. +*/ + +.mx_InlineSpinner { + display: inline; +} + +.mx_InlineSpinner img { + margin: 0px 6px; + vertical-align: -3px; +} diff --git a/res/css/views/elements/_MemberEventListSummary.scss b/res/css/views/elements/_MemberEventListSummary.scss new file mode 100644 index 0000000000..02ecb5d84a --- /dev/null +++ b/res/css/views/elements/_MemberEventListSummary.scss @@ -0,0 +1,71 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +.mx_MemberEventListSummary { + position: relative; +} + +.mx_TextualEvent.mx_MemberEventListSummary_summary { + font-size: 14px; + display: inline-flex; +} + +.mx_MemberEventListSummary_avatars { + display: inline-block; + margin-right: 8px; + padding-top: 8px; + line-height: 12px; +} + +.mx_MemberEventListSummary_avatars .mx_BaseAvatar { + margin-right: -4px; + cursor: pointer; +} + +.mx_MemberEventListSummary_toggle { + color: $accent-color; + cursor: pointer; + float: right; + margin-right: 10px; + margin-top: 8px; +} + +.mx_MemberEventListSummary_line { + border-bottom: 1px solid $primary-hairline-color; + margin-left: 63px; + line-height: 30px; +} + +.mx_MatrixChat_useCompactLayout { + .mx_MemberEventListSummary { + font-size: 13px; + .mx_EventTile_line { + line-height: 20px; + } + } + + .mx_MemberEventListSummary_line { + line-height: 22px; + } + + .mx_MemberEventListSummary_toggle { + margin-top: 3px; + } + + .mx_TextualEvent.mx_MemberEventListSummary_summary { + font-size: 13px; + } +} diff --git a/res/css/views/elements/_ProgressBar.scss b/res/css/views/elements/_ProgressBar.scss new file mode 100644 index 0000000000..a3fee232d0 --- /dev/null +++ b/res/css/views/elements/_ProgressBar.scss @@ -0,0 +1,25 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_ProgressBar { + height: 5px; + border: 1px solid $progressbar-color; +} + +.mx_ProgressBar_fill { + height: 100%; + background-color: $progressbar-color; +} diff --git a/res/css/views/elements/_ReplyThread.scss b/res/css/views/elements/_ReplyThread.scss new file mode 100644 index 0000000000..bf44a11728 --- /dev/null +++ b/res/css/views/elements/_ReplyThread.scss @@ -0,0 +1,37 @@ +/* +Copyright 2018 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. +*/ + +.mx_ReplyThread { + margin-top: 0; +} + +.mx_ReplyThread .mx_DateSeparator { + font-size: 1em !important; + margin-top: 0; + margin-bottom: 0; + padding-bottom: 1px; + bottom: -5px; +} + +.mx_ReplyThread_show { + cursor: pointer; +} + +blockquote.mx_ReplyThread { + margin-left: 0; + padding-left: 10px; + border-left: 4px solid $blockquote-bar-color; +} diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss new file mode 100644 index 0000000000..cea4b7897d --- /dev/null +++ b/res/css/views/elements/_RichText.scss @@ -0,0 +1,92 @@ +// XXX: bleurgh, what is this? These classes totally break the component +// naming scheme; it's completely unclear where or how they're being used +// --Matthew + +.mx_UserPill, +.mx_RoomPill, +.mx_GroupPill, +.mx_AtRoomPill { + border-radius: 16px; + display: inline-block; + height: 20px; + line-height: 20px; + padding-left: 5px; +} + +.mx_EventTile_body .mx_UserPill, +.mx_EventTile_body .mx_RoomPill, +.mx_EventTile_body .mx_GroupPill { + cursor: pointer; +} + +/* More specific to override `.markdown-body a` color */ +.mx_EventTile_content .markdown-body a.mx_UserPill, +.mx_UserPill { + color: $primary-fg-color; + background-color: $other-user-pill-bg-color; + padding-right: 5px; +} + +.mx_UserPill_selected { + background-color: $accent-color ! important; +} + +.mx_EventTile_highlight .mx_EventTile_content .markdown-body a.mx_UserPill_me, +.mx_EventTile_content .mx_AtRoomPill, +.mx_MessageComposer_input .mx_AtRoomPill { + color: $accent-fg-color; + background-color: $mention-user-pill-bg-color; + padding-right: 5px; +} + +/* More specific to override `.markdown-body a` color */ +.mx_EventTile_content .markdown-body a.mx_RoomPill, +.mx_EventTile_content .markdown-body a.mx_GroupPill, +.mx_RoomPill, +.mx_GroupPill { + color: $accent-fg-color; + background-color: $rte-room-pill-color; + padding-right: 5px; +} + +/* More specific to override `.markdown-body a` color */ +.mx_EventTile_content .markdown-body a.mx_GroupPill, +.mx_GroupPill { + color: $accent-fg-color; + background-color: $rte-group-pill-color; + padding-right: 5px; +} + +.mx_UserPill .mx_BaseAvatar, +.mx_RoomPill .mx_BaseAvatar, +.mx_GroupPill .mx_BaseAvatar, +.mx_AtRoomPill .mx_BaseAvatar { + position: relative; + left: -3px; + top: 2px; +} + +.mx_Markdown_BOLD { + font-weight: bold; +} + +.mx_Markdown_ITALIC { + font-style: italic; +} + +.mx_Markdown_CODE { + padding: .2em 0; + margin: 0; + font-size: 85%; + background-color: $rte-code-bg-color; + border-radius: 3px; +} + +.mx_Markdown_HR { + display: block; + background: $rte-bg-color; +} + +.mx_Markdown_STRIKETHROUGH { + text-decoration: line-through; +} diff --git a/res/css/views/elements/_RoleButton.scss b/res/css/views/elements/_RoleButton.scss new file mode 100644 index 0000000000..094e0b9b1b --- /dev/null +++ b/res/css/views/elements/_RoleButton.scss @@ -0,0 +1,33 @@ +/* +Copyright 2107 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. +*/ + +.mx_RoleButton { + margin-left: 4px; + margin-right: 4px; + cursor: pointer; + display: inline-block; +} + +.mx_RoleButton object { + pointer-events: none; +} + +.mx_RoleButton_tooltip { + display: inline-block; + position: relative; + top: -25px; + left: 6px; +} diff --git a/res/css/views/elements/_Spinner.scss b/res/css/views/elements/_Spinner.scss new file mode 100644 index 0000000000..aea5737918 --- /dev/null +++ b/res/css/views/elements/_Spinner.scss @@ -0,0 +1,28 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_Spinner { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + flex: 1; +} + +.mx_MatrixChat_middlePanel .mx_Spinner { + height: auto; +} \ No newline at end of file diff --git a/res/css/views/elements/_SyntaxHighlight.scss b/res/css/views/elements/_SyntaxHighlight.scss new file mode 100644 index 0000000000..e97401a160 --- /dev/null +++ b/res/css/views/elements/_SyntaxHighlight.scss @@ -0,0 +1,21 @@ +/* +Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_SyntaxHighlight { + /* inhibit hljs styling */ + background: none !important; + color: $light-fg-color !important; +} diff --git a/res/css/views/elements/_ToolTipButton.scss b/res/css/views/elements/_ToolTipButton.scss new file mode 100644 index 0000000000..c496e67515 --- /dev/null +++ b/res/css/views/elements/_ToolTipButton.scss @@ -0,0 +1,51 @@ +/* +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. +*/ + +.mx_ToolTipButton { + display: inline-block; + width: 11px; + height: 11px; + margin-left: 5px; + + border: 2px solid $neutral-badge-color; + border-radius: 20px; + color: $neutral-badge-color; + + transition: opacity 0.2s ease-in; + opacity: 0.6; + + line-height: 11px; + text-align: center; + + cursor: pointer; +} + +.mx_ToolTipButton:hover { + opacity: 1.0; +} + +.mx_ToolTipButton_container { + position: relative; + top: -18px; + left: 4px; +} + +.mx_ToolTipButton_helpText { + width: 400px; + text-align: start; + line-height: 17px !important; +} + diff --git a/res/css/views/globals/_MatrixToolbar.scss b/res/css/views/globals/_MatrixToolbar.scss new file mode 100644 index 0000000000..1791d619ae --- /dev/null +++ b/res/css/views/globals/_MatrixToolbar.scss @@ -0,0 +1,74 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_MatrixToolbar { + background-color: $accent-color; + color: $accent-fg-color; + + display: flex; + align-items: center; +} + +.mx_MatrixToolbar_warning { + margin-left: 16px; + margin-right: 8px; + margin-top: -2px; +} + +.mx_MatrixToolbar_info { + padding-left: 16px; + padding-right: 8px; + background-color: $info-bg-color; +} + +.mx_MatrixToolbar_error { + padding-left: 16px; + padding-right: 8px; + background-color: $warning-bg-color; +} + +.mx_MatrixToolbar_content { + flex: 1; +} + +.mx_MatrixToolbar_link +{ + color: $accent-fg-color ! important; + text-decoration: underline ! important; + cursor: pointer; +} + +.mx_MatrixToolbar_clickable { + cursor: pointer; +} + +.mx_MatrixToolbar_close { + cursor: pointer; +} + +.mx_MatrixToolbar_close img { + display: block; + float: right; + margin-right: 10px; +} + +.mx_MatrixToolbar_action { + margin-right: 16px; +} + +.mx_MatrixToolbar_changelog { + white-space: pre; +} diff --git a/res/css/views/groups/_GroupPublicityToggle.scss b/res/css/views/groups/_GroupPublicityToggle.scss new file mode 100644 index 0000000000..3ea4aa07d6 --- /dev/null +++ b/res/css/views/groups/_GroupPublicityToggle.scss @@ -0,0 +1,42 @@ +/* +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. +*/ + +.mx_GroupPublicity_toggle { + display: flex; + align-items: center; + margin: 8px; +} + +.mx_GroupPublicity_toggle > label { + display: flex; + align-items: flex-start; +} + +.mx_GroupPublicity_toggle > label, +.mx_GroupPublicity_toggle .mx_GroupTile { + width: 50%; +} + +.mx_GroupPublicity_toggle input { + margin-right: 8px; + vertical-align: -4px; +} + +.mx_GroupPublicity_toggle .mx_GroupTile { + display: flex; + align-items: flex-start; + cursor: pointer; +} diff --git a/res/css/views/groups/_GroupRoomList.scss b/res/css/views/groups/_GroupRoomList.scss new file mode 100644 index 0000000000..fb41ebaa9e --- /dev/null +++ b/res/css/views/groups/_GroupRoomList.scss @@ -0,0 +1,21 @@ +/* +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. +*/ + +.mx_GroupRoomTile { + position: relative; + color: $primary-fg-color; + cursor: pointer; +} diff --git a/res/css/views/groups/_GroupUserSettings.scss b/res/css/views/groups/_GroupUserSettings.scss new file mode 100644 index 0000000000..0c909b7cf7 --- /dev/null +++ b/res/css/views/groups/_GroupUserSettings.scss @@ -0,0 +1,23 @@ +/* +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. +*/ + +.mx_GroupUserSettings_groupPublicity_scrollbox { + height: 200px; + border: 1px solid $primary-hairline-color; + border-radius: 3px; + margin-right: 32px; + overflow: hidden; +} diff --git a/res/css/views/login/_InteractiveAuthEntryComponents.scss b/res/css/views/login/_InteractiveAuthEntryComponents.scss new file mode 100644 index 0000000000..183b5cd251 --- /dev/null +++ b/res/css/views/login/_InteractiveAuthEntryComponents.scss @@ -0,0 +1,42 @@ +/* +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. +*/ + +.mx_InteractiveAuthEntryComponents_msisdnWrapper { + text-align: center; +} + +.mx_InteractiveAuthEntryComponents_msisdnEntry { + font-size: 200%; + font-weight: bold; + border: 1px solid $strong-input-border-color; + border-radius: 3px; + width: 6em; +} + +.mx_InteractiveAuthEntryComponents_msisdnEntry:focus { + border: 1px solid $accent-color; +} + +.mx_InteractiveAuthEntryComponents_msisdnSubmit { + margin-top: 4px; + margin-bottom: 5px; +} + +// XXX: This should be a common button class +.mx_InteractiveAuthEntryComponents_msisdnSubmit:disabled { + background-color: $light-fg-color; + cursor: default; +} diff --git a/res/css/views/login/_ServerConfig.scss b/res/css/views/login/_ServerConfig.scss new file mode 100644 index 0000000000..894ce19827 --- /dev/null +++ b/res/css/views/login/_ServerConfig.scss @@ -0,0 +1,36 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_ServerConfig { + margin-top: 7px; +} + +.mx_ServerConfig .mx_Login_field { + margin-top: 4px; + margin-bottom: 5px; +} + +.mx_ServerConfig_help:link { + opacity: 0.8; + font-size: 13px; + font-weight: 300; + color: $primary-fg-color; +} + +.mx_ServerConfig_selector { + text-align: center; + width: 302px; // for fr i18n +} \ No newline at end of file diff --git a/res/css/views/messages/_CreateEvent.scss b/res/css/views/messages/_CreateEvent.scss new file mode 100644 index 0000000000..c095fc26af --- /dev/null +++ b/res/css/views/messages/_CreateEvent.scss @@ -0,0 +1,37 @@ +/* +Copyright 2018 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. +*/ + +.mx_CreateEvent { + background-color: $info-plinth-bg-color; + padding-left: 20px; + padding-right: 20px; + padding-top: 10px; + padding-bottom: 10px; +} + +.mx_CreateEvent_image { + float: left; + padding-right: 20px; + width: 72px; + height: 34px; +} + +.mx_CreateEvent_header { + font-weight: bold; +} + +.mx_CreateEvent_link { +} diff --git a/res/css/views/messages/_DateSeparator.scss b/res/css/views/messages/_DateSeparator.scss new file mode 100644 index 0000000000..f676d24bef --- /dev/null +++ b/res/css/views/messages/_DateSeparator.scss @@ -0,0 +1,25 @@ +/* +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. +*/ + +.mx_DateSeparator { + clear: both; + margin-top: 32px; + margin-bottom: 8px; + margin-left: 63px; + padding-bottom: 6px; + border-bottom: 1px solid $primary-hairline-color; +} + diff --git a/res/css/views/messages/_MEmoteBody.scss b/res/css/views/messages/_MEmoteBody.scss new file mode 100644 index 0000000000..cf722e5ae8 --- /dev/null +++ b/res/css/views/messages/_MEmoteBody.scss @@ -0,0 +1,23 @@ +/* +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. +*/ + +.mx_MEmoteBody { + white-space: pre-wrap; +} + +.mx_MEmoteBody_sender { + cursor: pointer; +} diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss new file mode 100644 index 0000000000..6cbce68745 --- /dev/null +++ b/res/css/views/messages/_MFileBody.scss @@ -0,0 +1,47 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_MFileBody_download { + color: $accent-color; +} + +.mx_MFileBody_download a { + color: $accent-color; + text-decoration: none; + cursor: pointer; +} + +.mx_MFileBody_download object { + margin-left: -16px; + padding-right: 4px; + margin-top: -4px; + vertical-align: middle; + pointer-events: none; +} + +/* Remove the border and padding for iframes for download links. */ +.mx_MFileBody_download iframe { + margin: 0px; + padding: 0px; + border: none; + width: 100%; + /* Set the height of the iframe to be 1 line of text. + * Iframes don't automatically size themselves to fit their content. + * So either we have to fix the height of the iframe using CSS or + * use javascript's cross-origin postMessage API to communicate how + * big the content of the iframe is. */ + height: 1.5em; +} diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss new file mode 100644 index 0000000000..4c763c5991 --- /dev/null +++ b/res/css/views/messages/_MImageBody.scss @@ -0,0 +1,48 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_MImageBody { + display: block; + margin-right: 34px; +} + +.mx_MImageBody_thumbnail { + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; +} + +.mx_MImageBody_thumbnail_container { + // Prevent the padding-bottom (added inline in MImageBody.js) from + // affecting elements below the container. + overflow: hidden; + + // Make sure the _thumbnail is positioned relative to the _container + position: relative; +} + +.mx_MImageBody_thumbnail_spinner { + position: absolute; + left: 50%; + top: 50%; +} + +// Inner img and TintableSvg should be centered around 0, 0 +.mx_MImageBody_thumbnail_spinner > * { + transform: translate(-50%, -50%); +} diff --git a/res/css/views/messages/_MNoticeBody.scss b/res/css/views/messages/_MNoticeBody.scss new file mode 100644 index 0000000000..a88c20863d --- /dev/null +++ b/res/css/views/messages/_MNoticeBody.scss @@ -0,0 +1,20 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_MNoticeBody { + white-space: pre-wrap; + opacity: 0.6; +} diff --git a/res/css/views/messages/_MStickerBody.scss b/res/css/views/messages/_MStickerBody.scss new file mode 100644 index 0000000000..e4977bcc34 --- /dev/null +++ b/res/css/views/messages/_MStickerBody.scss @@ -0,0 +1,24 @@ +/* +Copyright 2018 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. +*/ + +.mx_MStickerBody_wrapper { + padding: 20px 0px; +} + +.mx_MStickerBody_tooltip { + position: absolute; + top: 50%; +} diff --git a/res/css/views/messages/_MTextBody.scss b/res/css/views/messages/_MTextBody.scss new file mode 100644 index 0000000000..93a89ad1b7 --- /dev/null +++ b/res/css/views/messages/_MTextBody.scss @@ -0,0 +1,19 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_MTextBody { + white-space: pre-wrap; +} diff --git a/res/css/views/messages/_MessageTimestamp.scss b/res/css/views/messages/_MessageTimestamp.scss new file mode 100644 index 0000000000..e21189c59e --- /dev/null +++ b/res/css/views/messages/_MessageTimestamp.scss @@ -0,0 +1,18 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_MessageTimestamp { +} diff --git a/res/css/views/messages/_RoomAvatarEvent.scss b/res/css/views/messages/_RoomAvatarEvent.scss new file mode 100644 index 0000000000..9adce42eef --- /dev/null +++ b/res/css/views/messages/_RoomAvatarEvent.scss @@ -0,0 +1,26 @@ +/* +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. +*/ + +.mx_RoomAvatarEvent { + opacity: 0.5; + overflow-y: hidden; +} + +.mx_RoomAvatarEvent_avatar { + display: inline; + position: relative; + top: 5px; +} \ No newline at end of file diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss new file mode 100644 index 0000000000..060709b82e --- /dev/null +++ b/res/css/views/messages/_SenderProfile.scss @@ -0,0 +1,15 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ diff --git a/res/css/views/messages/_TextualEvent.scss b/res/css/views/messages/_TextualEvent.scss new file mode 100644 index 0000000000..be7565b3c5 --- /dev/null +++ b/res/css/views/messages/_TextualEvent.scss @@ -0,0 +1,20 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_TextualEvent { + opacity: 0.5; + overflow-y: hidden; +} diff --git a/res/css/views/messages/_UnknownBody.scss b/res/css/views/messages/_UnknownBody.scss new file mode 100644 index 0000000000..9036e12bf0 --- /dev/null +++ b/res/css/views/messages/_UnknownBody.scss @@ -0,0 +1,16 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_UnknownBody { + white-space: pre-wrap; +} diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss new file mode 100644 index 0000000000..4a46063376 --- /dev/null +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -0,0 +1,312 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_AppsDrawer { + margin: 5px; +} + +.mx_AppsDrawer_hidden { + display: none; +} + +.mx_AppsContainer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.mx_AddWidget_button { + order: 2; + cursor: pointer; + padding-right: 12px; + padding: 0; + margin: 5px auto 5px auto; + color: $accent-color; + font-size: 12px; +} + +.mx_AddWidget_button_full_width { + max-width: 960px; +} + +.mx_SetAppURLDialog_input { + border-radius: 3px; + border: 1px solid $input-border-color; + padding: 9px; + color: $primary-hairline-color; + background-color: $primary-bg-color; + font-size: 15px; +} + +.mx_AppTile { + max-width: 960px; + width: 50%; + margin-right: 5px; + border: 1px solid $primary-hairline-color; + border-radius: 2px; + background-color: $dialog-background-bg-color; +} + +.mx_AppTile:last-child { + margin-right: 1px; +} + +.mx_AppTileFullWidth { + max-width: 960px; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + border: 1px solid $primary-hairline-color; + border-radius: 2px; +} + +.mx_AppTile_mini { + max-width: 960px; + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +.mx_AppTile_persistedWrapper { + height: 280px; +} + +.mx_AppTile_mini .mx_AppTile_persistedWrapper { + height: 114px; +} + +.mx_AppTileMenuBar { + margin: 0; + padding: 2px 10px; + border-bottom: 1px solid $primary-hairline-color; + font-size: 10px; + background-color: $widget-menu-bar-bg-color; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + cursor: pointer; +} + +.mx_AppTileMenuBarTitle { + display: flex; + flex-direction: row; + align-items: center; + pointer-events: none; +} + +.mx_AppTileMenuBarWidgets { + float: right; + display: flex; + flex-direction: row; + align-items: center; +} + +.mx_AppTileMenuBarWidget { + cursor: pointer; + width: 10px; + height: 10px; + padding: 1px; + transition-duration: 500ms; + border: 1px solid transparent; +} + +.mx_AppTileMenuBarWidgetDelete { + filter: none; +} + +.mx_AppTileMenuBarWidget:hover { + border: 1px solid $primary-fg-color; + border-radius: 2px; +} + +.mx_AppTileBody{ + height: 280px; + width: 100%; + overflow: hidden; +} + +.mx_AppTileBody_mini { + height: 112px; + width: 100%; + overflow: hidden; +} + +.mx_AppTileBody_mini iframe { + border: none; + width: 100%; + height: 100%; +} + +.mx_AppTileBody iframe { + width: 100%; + height: 280px; + overflow: hidden; + border: none; + padding: 0; + margin: 0; + display: block; +} + +.mx_AppTileMenuBarWidgetPadding { + margin-right: 5px; +} + +.mx_AppIconTile { + background-color: $lightbox-bg-color; + border: 1px solid rgba(0, 0, 0, 0); + width: 200px; + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + transition: 0.3s; + border-radius: 3px; + margin: 5px; + display: inline-block; +} + +.mx_AppIconTile.mx_AppIconTile_active { + color: $accent-color; + border-color: $accent-color; +} + +.mx_AppIconTile:hover { + border: 1px solid $accent-color; + box-shadow: 0 0 10px 5px rgba(200,200,200,0.5); +} + +.mx_AppIconTile_content { + padding: 2px 16px; + height: 60px; + overflow: hidden; +} + +.mx_AppIconTile_content h4 { + margin-top: 5px; + margin-bottom: 2px; +} + +.mx_AppIconTile_content p { + margin-top: 0; + margin-bottom: 5px; + font-size: smaller; +} + +.mx_AppIconTile_image { + padding: 10px; + width: 75%; + max-width:100px; + max-height:100px; + width: auto; + height: auto; +} + +.mx_AppIconTile_imageContainer { + text-align: center; + width: 100%; + background-color: white; + border-radius: 3px 3px 0 0; + height: 155px; + display: flex; + justify-content: center; + align-items: center; +} + +form.mx_Custom_Widget_Form div { + margin-top: 10px; + margin-bottom: 10px; +} + +.mx_AppPermissionWarning { + text-align: center; + background-color: $primary-bg-color; + display: flex; + height: 100%; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.mx_AppPermissionWarningImage { + margin: 10px 0; +} + +.mx_AppPermissionWarningImage img { + width: 100px; +} + +.mx_AppPermissionWarningText { + max-width: 400px; + margin: 10px auto 10px auto; + color: $primary-fg-color; +} + +.mx_AppPermissionWarningTextLabel { + font-weight: bold; + display: block; +} + +.mx_AppPermissionWarningTextURL { + color: $accent-color; +} + +.mx_AppPermissionButton { + padding: 5px; + border-radius: 5px; + color: $warning-color; + background-color: $primary-bg-color; +} + +.mx_AppPermissionButton:hover { + background-color: $primary-fg-color; + cursor: pointer; +} + +.mx_AppLoading { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-weight: bold; + position: relative; + height: 280px; +} + +.mx_AppLoading .mx_Spinner { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} + +.mx_AppLoading_spinner_fadeIn { + animation-fill-mode: backwards; + animation-duration: 200ms; + animation-delay: 500ms; + animation-name: mx_AppLoading_spinner_fadeIn_animation; +} + +@keyframes mx_AppLoading_spinner_fadeIn_animation { + from { opacity: 0 } + to { opacity: 1 } +} + + +.mx_AppLoading iframe { + display: none; +} diff --git a/res/css/views/rooms/_Autocomplete.scss b/res/css/views/rooms/_Autocomplete.scss new file mode 100644 index 0000000000..3e1016f60d --- /dev/null +++ b/res/css/views/rooms/_Autocomplete.scss @@ -0,0 +1,94 @@ +.mx_Autocomplete { + position: absolute; + bottom: 0; + z-index: 1001; + width: 100%; + border: 1px solid $primary-hairline-color; + background: $primary-bg-color; + border-bottom: none; + border-radius: 4px 4px 0 0; + max-height: 50vh; + overflow: auto +} + +.mx_Autocomplete_ProviderSection { + border-bottom: 1px solid $primary-hairline-color; +} + +.mx_Autocomplete_Completion_container_pill { + margin: 12px; + display: flex; +} + +/* a "block" completion takes up a whole line */ +.mx_Autocomplete_Completion_block { + height: 34px; + display: flex; + padding: 0 12px; + user-select: none; + cursor: pointer; + align-items: center; + color: $primary-fg-color; +} + +.mx_Autocomplete_Completion_block * { + margin: 0 3px; +} + +.mx_Autocomplete_Completion_pill { + border-radius: 17px; + height: 34px; + padding: 0px 5px; + display: flex; + user-select: none; + cursor: pointer; + align-items: center; + color: $primary-fg-color; +} + +.mx_Autocomplete_Completion_pill > * { + margin: 0 3px; +} + +.mx_Autocomplete_Completion_container_truncate { + .mx_Autocomplete_Completion_title, + .mx_Autocomplete_Completion_subtitle, + .mx_Autocomplete_Completion_description { + /* Ellipsis for long names/subtitles/descriptions*/ + max-width: 150px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +/* container for pill-style completions */ +.mx_Autocomplete_Completion_container_pill { + margin: 12px; + display: flex; + flex-flow: wrap; +} + +.mx_Autocomplete_Completion.selected, +.mx_Autocomplete_Completion:hover { + background: $menu-bg-color; + outline: none; +} + +.mx_Autocomplete_provider_name { + margin: 12px; + color: $primary-fg-color; + font-weight: 400; + opacity: 0.4; +} + +/* styling for common completion elements */ +.mx_Autocomplete_Completion_subtitle { + font-style: italic; + flex: 1; +} + +.mx_Autocomplete_Completion_description { + color: gray; +} + diff --git a/res/css/views/rooms/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss new file mode 100644 index 0000000000..031894afde --- /dev/null +++ b/res/css/views/rooms/_EntityTile.scss @@ -0,0 +1,114 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_EntityTile { + display: table-row; + position: relative; + color: $primary-fg-color; + cursor: pointer; +} + +.mx_EntityTile_invite { + display: table-cell; + vertical-align: middle; + margin-left: 10px; + width: 26px; +} + +.mx_EntityTile_avatar, +.mx_GroupRoomTile_avatar { + display: table-cell; + padding-left: 3px; + padding-right: 12px; + padding-top: 4px; + padding-bottom: 4px; + vertical-align: middle; + width: 36px; + height: 36px; + position: relative; +} + +.mx_EntityTile_power { + position: absolute; + width: 16px; + height: 17px; + top: 0px; + right: 6px; +} + +.mx_EntityTile_name, +.mx_GroupRoomTile_name { + display: table-cell; + vertical-align: middle; + overflow: hidden; + font-size: 14px; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 155px; +} + +.mx_EntityTile_details { + display: table-cell; + padding-right: 14px; + vertical-align: middle; +} + +.mx_EntityTile_name_hover { + font-size: 13px; +} + +.mx_EntityTile_chevron { + margin-top: 8px; + margin-right: -4px; + margin-left: 6px; + float: right; +} + +.mx_EntityTile_ellipsis .mx_EntityTile_name { + font-style: italic; + color: $primary-fg-color; +} + +.mx_EntityTile_invitePlaceholder .mx_EntityTile_name { + font-style: italic; + color: $primary-fg-color; +} + +.mx_EntityTile_unavailable .mx_EntityTile_avatar, +.mx_EntityTile_unavailable .mx_EntityTile_name, +.mx_EntityTile_unavailable .mx_EntityTile_name_hover, +.mx_EntityTile_offline_beenactive .mx_EntityTile_avatar, +.mx_EntityTile_offline_beenactive .mx_EntityTile_name, +.mx_EntityTile_offline_beenactive .mx_EntityTile_name_hover +{ + opacity: 0.66; +} + +.mx_EntityTile_offline_neveractive .mx_EntityTile_avatar, +.mx_EntityTile_offline_neveractive .mx_EntityTile_name, +.mx_EntityTile_offline_neveractive .mx_EntityTile_name_hover +{ + opacity: 0.25; +} + +.mx_EntityTile_unknown .mx_EntityTile_avatar, +.mx_EntityTile_unknown .mx_EntityTile_name, +.mx_EntityTile_unknown .mx_EntityTile_name_hover +{ + opacity: 0.25; +} + + diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss new file mode 100644 index 0000000000..f74e2e0850 --- /dev/null +++ b/res/css/views/rooms/_EventTile.scss @@ -0,0 +1,542 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_EventTile { + max-width: 100%; + clear: both; + padding-top: 18px; + font-size: 14px; + position: relative; +} + +.mx_EventTile.mx_EventTile_info { + padding-top: 0px; +} + +.mx_EventTile_avatar { + position: absolute; + top: 14px; + left: 8px; + cursor: pointer; +} + +.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { + top: 8px; + left: 65px; +} + +.mx_EventTile_continuation { + padding-top: 0px ! important; +} + +.mx_EventTile .mx_SenderProfile { + color: $primary-fg-color; + font-size: 14px; + display: block; /* anti-zalgo, with overflow hidden */ + overflow-y: hidden; + cursor: pointer; + padding-left: 65px; /* left gutter */ + padding-bottom: 0px; + padding-top: 0px; + margin: 0px; + line-height: 22px; +} + +.mx_EventTile .mx_SenderProfile .mx_SenderProfile_name, +.mx_EventTile .mx_SenderProfile .mx_SenderProfile_aux { + opacity: 0.5; +} + +.mx_EventTile .mx_SenderProfile .mx_Flair { + opacity: 0.7; + margin-left: 5px; +} + +.mx_EventTile .mx_SenderProfile .mx_Flair img { + vertical-align: -2px; + margin-right: 2px; + border-radius: 8px; +} + +.mx_EventTile .mx_MessageTimestamp { + display: block; + visibility: hidden; + white-space: nowrap; + color: $event-timestamp-color; + font-size: 10px; + left: 0px; + width: 46px; /* 8 + 30 (avatar) + 8 */ + text-align: center; + position: absolute; +} + +.mx_EventTile_line, .mx_EventTile_reply { + position: relative; + /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ + margin-right: 110px; + padding-left: 65px; /* left gutter */ + padding-top: 4px; + padding-bottom: 2px; + border-radius: 4px; + min-height: 24px; + line-height: 22px; +} + +.mx_EventTile_reply { + margin-right: 10px; +} + +.mx_EventTile_info .mx_EventTile_line { + padding-left: 83px; +} + +/* HACK to override line-height which is already marked important elsewhere */ +.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji { + font-size: 48px ! important; + line-height: 48px ! important; +} + +/* this is used for the tile for the event which is selected via the URL. + * TODO: ultimately we probably want some transition on here. + */ +.mx_EventTile_selected > .mx_EventTile_line { + border-left: $accent-color 5px solid; + padding-left: 60px; + background-color: $event-selected-color; +} + +.mx_EventTile:hover .mx_EventTile_line, +.mx_EventTile.menu .mx_EventTile_line +{ + background-color: $event-selected-color; +} + +.mx_EventTile_searchHighlight { + background-color: $accent-color; + color: $accent-fg-color; + border-radius: 5px; + padding-left: 2px; + padding-right: 2px; + cursor: pointer; +} + +.mx_EventTile_searchHighlight a { + background-color: $accent-color; + color: $accent-fg-color; +} + +.mx_EventTile_encrypting { + color: $event-encrypting-color ! important; +} + +.mx_EventTile_sending { + color: $event-sending-color; +} + +.mx_EventTile_sending .mx_UserPill, +.mx_EventTile_sending .mx_RoomPill, +.mx_EventTile_sending .mx_emojione { + opacity: 0.5; +} + +.mx_EventTile_notSent { + color: $event-notsent-color; +} + +.mx_EventTile_redacted .mx_EventTile_line .mx_UnknownBody, +.mx_EventTile_redacted .mx_EventTile_reply .mx_UnknownBody { + display: block; + width: 100%; + height: 22px; + width: 250px; + border-radius: 11px; + background: repeating-linear-gradient( + -45deg, + $event-redacted-fg-color, + $event-redacted-fg-color 3px, + transparent 3px, + transparent 6px + ); + box-shadow: 0px 0px 3px $event-redacted-border-color inset; +} + +.mx_EventTile_highlight, +.mx_EventTile_highlight .markdown-body + { + color: $warning-color; +} + +.mx_EventTile_contextual { + opacity: 0.4; +} + +.mx_EventTile_msgOption { + float: right; + text-align: right; + position: relative; + width: 90px; + + /* Hack to stop the height of this pushing the messages apart. + Replaces margin-top: -6px. This interacts better with a read + marker being in between. Content overflows. */ + height: 1px; + + margin-right: 10px; +} + +.mx_EventTile_msgOption a { + text-decoration: none; +} + +// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) +.mx_EventTile_last > div > a > .mx_MessageTimestamp, +.mx_EventTile:hover > div > a > .mx_MessageTimestamp, +.mx_EventTile.menu > div > a > .mx_MessageTimestamp { + visibility: visible; +} + +.mx_MessagePanel_alwaysShowTimestamps .mx_MessageTimestamp { + visibility: visible; +} + +.mx_EventTile_selected > div > a > .mx_MessageTimestamp { + left: 3px; + width: auto; +} + +.mx_EventTile_editButton { + position: absolute; + display: inline-block; + visibility: hidden; + cursor: pointer; + top: 6px; + right: 6px; + width: 19px; + height: 19px; + background-image: url($edit-button-url); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.mx_EventTile:hover .mx_EventTile_editButton, +.mx_EventTile.menu .mx_EventTile_editButton { + visibility: visible; +} + +.mx_EventTile_readAvatars { + position: relative; + display: inline-block; + width: 14px; + height: 14px; + top: 29px; +} + +.mx_EventTile_continuation .mx_EventTile_readAvatars, +.mx_EventTile_info .mx_EventTile_readAvatars, +.mx_EventTile_emote .mx_EventTile_readAvatars { + top: 7px; +} + +.mx_EventTile_readAvatars .mx_BaseAvatar { + position: absolute; + display: inline-block; +} + +.mx_EventTile_readAvatarRemainder { + color: $event-timestamp-color; + font-size: 11px; + position: absolute; +} + +/* all the overflow-y: hidden; are to trap Zalgos - + but they introduce an implicit overflow-x: auto. + so make that explicitly hidden too to avoid random + horizontal scrollbars occasionally appearing, like in + https://github.com/vector-im/vector-web/issues/1154 + */ +.mx_EventTile_content { + display: block; + overflow-y: hidden; + overflow-x: hidden; + margin-right: 34px; +} + +/* De-zalgoing */ +.mx_EventTile_body { + overflow-y: hidden; +} + +/* End to end encryption stuff */ + +.mx_EventTile_e2eIcon { + display: block; + position: absolute; + top: 9px; + left: 46px; + cursor: pointer; +} + +.mx_EventTile_e2eIcon_hidden { + display: none; +} + +/* always override hidden attribute for blocked and warning */ +.mx_EventTile_e2eIcon_hidden[src="img/e2e-blocked.svg"], +.mx_EventTile_e2eIcon_hidden[src="img/e2e-warning.svg"] { + display: block; +} + +.mx_EventTile_keyRequestInfo { + font-size: 12px; +} + +.mx_EventTile_keyRequestInfo_text { + opacity: 0.5; +} + +.mx_EventTile_keyRequestInfo_text a { + color: $primary-fg-color; + text-decoration: underline; + cursor: pointer; +} + +.mx_EventTile_keyRequestInfo_tooltip_contents p { + text-align: auto; + margin-left: 3px; + margin-right: 3px; +} + +.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child { + margin-top: 0px; +} + +.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child { + margin-bottom: 0px; +} + +.mx_EventTile_12hr .mx_EventTile_e2eIcon { + padding-left: 5px; +} + +.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, +.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { + padding-left: 60px; +} + +.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line, +.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, +.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line { + padding-left: 78px; +} + +.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { + border-left: $e2e-verified-color 5px solid; +} +.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { + border-left: $e2e-unverified-color 5px solid; +} + +// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) +.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, +.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp { + left: 3px; + width: auto; +} + +/* +.mx_EventTile_verified .mx_EventTile_e2eIcon { + display: none; +} +*/ + +// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) +.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon, +.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon { + display: block; + left: 41px; +} + +/* Various markdown overrides */ + +.mx_EventTile_content .markdown-body { + font-family: inherit ! important; + white-space: normal ! important; + line-height: inherit ! important; + color: inherit; // inherit the colour from the dark or light theme by default (but not for code blocks) + font-size: 14px; +} + +/* have to use overlay rather than auto otherwise Linux and Windows + Chrome gets very confused about vertical spacing: + https://github.com/vector-im/vector-web/issues/754 +*/ +.mx_EventTile_content .markdown-body pre { + overflow-x: overlay; + overflow-y: visible; + max-height: 30vh; +} + +.mx_EventTile_content .markdown-body code { + // deliberate constants as we're behind an invert filter + background-color: #f8f8f8; + color: #333; +} + +.mx_EventTile_pre_container { + // For correct positioning of _copyButton (See TextualBody) + position: relative; +} + +// Inserted adjacent to

 blocks, (See TextualBody)
+.mx_EventTile_copyButton {
+    position: absolute;
+    display: inline-block;
+    visibility: hidden;
+    cursor: pointer;
+    top: 6px;
+    right: 6px;
+    width: 19px;
+    height: 19px;
+    background-image: url($copy-button-url);
+}
+
+.mx_EventTile_body pre {
+    border: 1px solid transparent;
+}
+
+.mx_EventTile:hover .mx_EventTile_body pre
+{
+    border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter
+}
+
+.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton
+{
+    visibility: visible;
+}
+
+.mx_EventTile_content .markdown-body h1,
+.mx_EventTile_content .markdown-body h2,
+.mx_EventTile_content .markdown-body h3,
+.mx_EventTile_content .markdown-body h4,
+.mx_EventTile_content .markdown-body h5,
+.mx_EventTile_content .markdown-body h6
+{
+    font-family: inherit ! important;
+    color: inherit;
+}
+
+
+/* Make h1 and h2 the same size as h3. */
+.mx_EventTile_content .markdown-body h1,
+.mx_EventTile_content .markdown-body h2
+{
+    font-size: 1.5em;
+    border-bottom: none ! important; // override GFM
+}
+
+.mx_EventTile_content .markdown-body a {
+    color: $accent-color;
+}
+
+.mx_EventTile_content .markdown-body .hljs {
+    display: inline ! important;
+}
+
+/* end of overrides */
+
+.mx_MatrixChat_useCompactLayout {
+    .mx_EventTile {
+        padding-top: 4px;
+    }
+
+    .mx_EventTile.mx_EventTile_info {
+        // same as the padding for non-compact .mx_EventTile.mx_EventTile_info
+        padding-top: 0px;
+        font-size: 13px;
+        .mx_EventTile_line, .mx_EventTile_reply {
+            line-height: 20px;
+        }
+        .mx_EventTile_avatar {
+            top: 4px;
+        }
+    }
+
+    .mx_EventTile .mx_SenderProfile {
+        font-size: 13px;
+    }
+
+    .mx_EventTile.mx_EventTile_emote {
+        // add a bit more space for emotes so that avatars don't collide
+        padding-top: 8px;
+        .mx_EventTile_avatar {
+            top: 2px;
+        }
+        .mx_EventTile_line, .mx_EventTile_reply {
+            padding-top: 0px;
+            padding-bottom: 1px;
+        }
+    }
+
+    .mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation {
+        padding-top: 0;
+        .mx_EventTile_line, .mx_EventTile_reply {
+            padding-top: 0px;
+            padding-bottom: 0px;
+        }
+    }
+
+    .mx_EventTile_line, .mx_EventTile_reply {
+        padding-top: 0px;
+        padding-bottom: 0px;
+    }
+
+    .mx_EventTile_avatar {
+        top: 2px;
+    }
+
+    .mx_EventTile_e2eIcon {
+        top: 7px;
+    }
+
+    .mx_EventTile_editButton {
+        top: 3px;
+    }
+
+    .mx_EventTile_readAvatars {
+        top: 27px;
+    }
+
+    .mx_EventTile_continuation .mx_EventTile_readAvatars,
+    .mx_EventTile_emote .mx_EventTile_readAvatars {
+        top: 5px;
+    }
+
+    .mx_EventTile_info .mx_EventTile_readAvatars {
+        top: 4px;
+    }
+
+    .mx_RoomView_MessageList h2 {
+        margin-top: 6px;
+    }
+
+    .mx_EventTile_content .markdown-body {
+      p, ul, ol, dl, blockquote, pre, table {
+        margin-bottom: 4px; // 1/4 of the non-compact margin-bottom
+      }
+    }
+}
diff --git a/res/css/views/rooms/_LinkPreviewWidget.scss b/res/css/views/rooms/_LinkPreviewWidget.scss
new file mode 100644
index 0000000000..4495b142e6
--- /dev/null
+++ b/res/css/views/rooms/_LinkPreviewWidget.scss
@@ -0,0 +1,69 @@
+/*
+Copyright 2015, 2016 OpenMarket 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.
+*/
+
+.mx_LinkPreviewWidget {
+    margin-top: 15px;
+    margin-right: 15px;
+    margin-bottom: 15px;
+    display: flex;
+    border-left: 4px solid $preview-widget-bar-color;
+    color: $preview-widget-fg-color;
+}
+
+.mx_LinkPreviewWidget_image {
+    flex: 0 0 100px;
+    margin-left: 15px;
+    text-align: center;
+    cursor: pointer;
+}
+
+.mx_LinkPreviewWidget_caption {
+    margin-left: 15px;
+    flex: 1 1 auto;
+}
+
+.mx_LinkPreviewWidget_title {
+    display: inline;
+    font-weight: bold;
+    white-space: normal;
+}
+
+.mx_LinkPreviewWidget_siteName {
+    display: inline;
+}
+
+.mx_LinkPreviewWidget_description {
+    margin-top: 8px;
+    white-space: normal;
+    word-wrap: break-word;
+}
+
+.mx_LinkPreviewWidget_cancel {
+    visibility: hidden;
+    cursor: pointer;
+    flex: 0 0 40px;
+}
+
+.mx_LinkPreviewWidget:hover .mx_LinkPreviewWidget_cancel {
+    visibility: visible;
+}
+
+.mx_MatrixChat_useCompactLayout {
+    .mx_LinkPreviewWidget {
+        margin-top: 6px;
+        margin-bottom: 6px;
+    }
+}
diff --git a/res/css/views/rooms/_MemberDeviceInfo.scss b/res/css/views/rooms/_MemberDeviceInfo.scss
new file mode 100644
index 0000000000..5888820e0d
--- /dev/null
+++ b/res/css/views/rooms/_MemberDeviceInfo.scss
@@ -0,0 +1,74 @@
+/*
+Copyright 2016 OpenMarket 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.
+*/
+
+.mx_MemberDeviceInfo {
+    padding: 10px 0px;
+}
+
+.mx_MemberDeviceInfo.mx_DeviceVerifyButtons {
+    padding: 6px 0;
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+}
+
+.mx_MemberDeviceInfo_textButton {
+    @mixin mx_DialogButton_small;
+    margin: 2px;
+    flex: 1;
+}
+
+.mx_MemberDeviceInfo_textButton:hover {
+    @mixin mx_DialogButton_hover;
+}
+
+.mx_MemberDeviceInfo_deviceId {
+    font-size: 13px;
+}
+
+.mx_MemberDeviceInfo_deviceInfo {
+    margin-bottom: 10px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid rgba(0,0,0,0.1);
+}
+
+/* "Unblacklist" is too long for a regular button: make it wider and
+   reduce the padding. */
+.mx_EncryptedEventDialog .mx_MemberDeviceInfo_blacklist,
+.mx_EncryptedEventDialog .mx_MemberDeviceInfo_unblacklist {
+    width: 8em;
+    padding-left: 1em;
+    padding-right: 1em;
+}
+
+.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_verified,
+.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_unverified,
+.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_blacklisted {
+    float: right;
+    padding-left: 1em;
+}
+
+.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_verified {
+    color: $e2e-verified-color;
+}
+
+.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_unverified {
+    color: $e2e-unverified-color;
+}
+
+.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_blacklisted {
+    color: $e2e-warning-color;
+}
diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss
new file mode 100644
index 0000000000..5d47275efe
--- /dev/null
+++ b/res/css/views/rooms/_MemberInfo.scss
@@ -0,0 +1,112 @@
+/*
+Copyright 2015, 2016 OpenMarket 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.
+*/
+
+.mx_MemberInfo {
+    margin-top: 20px;
+    padding-right: 20px;
+    height: 100%;
+    overflow-y: auto;
+}
+
+.mx_MemberInfo h2 {
+    margin-top: 6px;
+}
+
+.mx_MemberInfo .mx_RoomTile_nameContainer {
+    width: 154px;
+}
+
+.mx_MemberInfo .mx_RoomTile_badge {
+    display: none;
+}
+
+.mx_MemberInfo .mx_RoomTile_name {
+    width: 160px;
+}
+
+.mx_MemberInfo_cancel {
+    float: right;
+    margin-right: 10px;
+    cursor: pointer;
+}
+
+.mx_MemberInfo_avatar {
+    clear: both;
+}
+
+.mx_MemberInfo_avatar .mx_BaseAvatar {
+}
+
+.mx_MemberInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image {
+    cursor: zoom-in;
+}
+
+.mx_MemberInfo_profile {
+    margin-bottom: 16px;
+}
+
+.mx_MemberInfo h3 {
+    text-transform: uppercase;
+    color: $h3-color;
+    font-weight: 600;
+    font-size: 13px;
+    margin-top: 16px;
+    margin-bottom: 14px;
+}
+
+.mx_MemberInfo_profileField {
+    font-size: 13px;
+    position: relative;
+    background-color: $primary-bg-color;
+}
+
+.mx_MemberInfo_buttons {
+    margin-bottom: 16px;
+}
+
+.mx_MemberInfo_field {
+    cursor: pointer;
+    font-size: 13px;
+    color: $accent-color;
+    margin-left: 8px;
+    line-height: 23px;
+}
+
+.mx_MemberInfo_createRoom {
+    cursor: pointer;
+}
+
+.mx_MemberInfo_createRoom_label {
+    width: initial ! important;
+    cursor: pointer;
+}
+
+.mx_MemberInfo label {
+    font-size: 13px;
+}
+
+.mx_MemberInfo label .mx_MemberInfo_label_text {
+    display: inline-block;
+    max-width: 180px;
+    vertical-align: text-top;
+}
+
+.mx_MemberInfo input[type="radio"] {
+    vertical-align: -2px;
+    margin-right: 5px;
+    margin-left: 8px;
+}
+
diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss
new file mode 100644
index 0000000000..83fc70aefb
--- /dev/null
+++ b/res/css/views/rooms/_MemberList.scss
@@ -0,0 +1,116 @@
+/*
+Copyright 2015, 2016 OpenMarket 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.
+*/
+
+.mx_MemberList,
+.mx_GroupMemberList,
+.mx_GroupRoomList {
+    height: 100%;
+
+    margin-top: 12px;
+    margin-right: 20px;
+
+    flex: 1;
+
+    display: flex;
+
+    flex-direction: column;
+}
+
+.mx_MemberList .mx_Spinner {
+    flex: 0 0 auto;
+}
+
+.mx_MemberList_chevron {
+    position: absolute;
+    right: 35px;
+    margin-top: -15px;
+}
+
+.mx_MemberList_border {
+    overflow-y: auto;
+
+    order: 1;
+    flex: 1 1 0px;
+}
+
+.mx_MemberList_query,
+.mx_GroupMemberList_query,
+.mx_GroupRoomList_query {
+    font-family: $font-family;
+    border-radius: 3px;
+    border: 1px solid $input-border-color;
+    padding: 9px;
+    color: $primary-fg-color;
+    background-color: $primary-bg-color;
+    margin-left: 3px;
+    font-size: 14px;
+    margin-bottom: 8px;
+    width: 189px;
+}
+
+.mx_MemberList_query::-moz-placeholder,
+.mx_GroupMemberList_query::-moz-placeholder,
+.mx_GroupRoomList_query::-moz-placeholder {
+    color: $primary-fg-color;
+    opacity: 0.5;
+    font-size: 14px;
+}
+
+.mx_MemberList_query::-webkit-input-placeholder,
+.mx_GroupMemberList_query::-webkit-input-placeholder,
+.mx_GroupRoomList_query::-webkit-input-placeholder {
+    color: $primary-fg-color;
+    opacity: 0.5;
+    font-size: 14px;
+}
+
+.mx_MemberList_joined {
+    order: 2;
+    flex: 1 0 0;
+
+    overflow-y: auto;
+}
+
+/*
+.mx_MemberList_invited {
+    order: 3;
+    flex: 0 0 100px;
+    overflow-y: auto;
+}
+*/
+
+.mx_GroupMemberList_invited h2,
+.mx_MemberList_invited h2 {
+    text-transform: uppercase;
+    color: $h3-color;
+    font-weight: 600;
+    font-size: 13px;
+    padding-left: 3px;
+    padding-right: 12px;
+    margin-top: 8px;
+    margin-bottom: 4px;
+}
+
+/* we have to have display: table in order for the horizontal wrapping to work */
+.mx_MemberList_wrapper {
+    display: table;
+    table-layout: fixed;
+    width: 100%;
+}
+
+.mx_MemberList_outerWrapper {
+    height: 0px;
+}
diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
new file mode 100644
index 0000000000..e6a532d072
--- /dev/null
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -0,0 +1,266 @@
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2018 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.
+*/
+
+.mx_MessageComposer_wrapper {
+    max-width: 960px;
+    vertical-align: middle;
+    margin: auto;
+    border-top: 1px solid $primary-hairline-color;
+    position: relative;
+}
+
+.mx_MessageComposer_replaced_wrapper {
+    margin-left: auto;
+    margin-right: auto;
+}
+
+.mx_MessageComposer_replaced_valign {
+    height: 60px;
+    display: table-cell;
+    vertical-align: middle;
+}
+
+.mx_MessageComposer_roomReplaced_icon {
+    float: left;
+    margin-right: 20px;
+    margin-top: 5px;
+    width: 31px;
+    height: 31px;
+}
+
+.mx_MessageComposer_roomReplaced_header {
+    font-weight: bold;
+}
+
+.mx_MessageComposer_autocomplete_wrapper {
+    position: relative;
+    height: 0;
+}
+
+.mx_MessageComposer_row {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    width: 100%;
+}
+
+.mx_MessageComposer_row > div:last-child{
+    padding-right: 0;
+}
+
+.mx_MessageComposer .mx_MessageComposer_avatar {
+    padding-left: 10px;
+    padding-right: 28px;
+}
+
+.mx_MessageComposer .mx_MessageComposer_avatar .mx_BaseAvatar {
+    display: block;
+}
+
+.mx_MessageComposer_composecontrols {
+    width: 100%;
+}
+
+.mx_MessageComposer_e2eIcon {
+    position: absolute;
+    left: 44px;
+}
+
+.mx_MessageComposer_noperm_error {
+    width: 100%;
+    height: 60px;
+    font-style: italic;
+    color: $greyed-fg-color;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.mx_MessageComposer_input_wrapper {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    cursor: text;
+}
+
+.mx_MessageComposer_input {
+    flex: 1;
+    vertical-align: middle;
+    display: flex;
+    flex-direction: column;
+    min-height: 60px;
+    justify-content: start;
+    align-items: flex-start;
+    font-size: 14px;
+    margin-right: 6px;
+}
+
+.mx_MessageComposer_editor {
+    width: 100%;
+    max-height: 120px;
+    min-height: 19px;
+    overflow: auto;
+    word-break: break-word;
+}
+
+// FIXME: rather unpleasant hack to get rid of 

margins. +// really we should be mixing in markdown-body from gfm.css instead +.mx_MessageComposer_editor > :first-child { + margin-top: 0 ! important; +} +.mx_MessageComposer_editor > :last-child { + margin-bottom: 0 ! important; +} + +@keyframes visualbell +{ + from { background-color: #faa } + to { background-color: $primary-bg-color } +} + +.mx_MessageComposer_input_error { + animation: 0.2s visualbell; +} + +.mx_MessageComposer_input blockquote { + color: $blockquote-fg-color; + margin: 0 0 16px; + padding: 0 15px; + border-left: 4px solid $blockquote-bar-color; +} + +.mx_MessageComposer_input pre { + background-color: $rte-code-bg-color; + border-radius: 3px; + padding: 10px; +} + +.mx_MessageComposer_input textarea { + display: block; + width: 100%; + padding: 0px; + margin-top: 6px; + margin-bottom: 6px; + border: 0px; + resize: none; + outline: none; + box-shadow: none; + color: $primary-fg-color; + background-color: $primary-bg-color; + font-size: 14px; + max-height: 120px; + overflow: auto; + /* needed for FF */ + font-family: $font-family; +} + +/* hack for FF as vertical alignment of custom placeholder text is broken */ +.mx_MessageComposer_input textarea::-moz-placeholder { + line-height: 100%; + color: $accent-color; + opacity: 1.0; +} +.mx_MessageComposer_input textarea::-webkit-input-placeholder { + color: $accent-color; +} + +.mx_MessageComposer_upload, +.mx_MessageComposer_hangup, +.mx_MessageComposer_voicecall, +.mx_MessageComposer_videocall, +.mx_MessageComposer_apps, +.mx_MessageComposer_stickers { + /*display: table-cell;*/ + /*vertical-align: middle;*/ + /*padding-left: 10px;*/ + padding-right: 5px; + cursor: pointer; + padding-top: 4px; +} + +.mx_MessageComposer_upload object, +.mx_MessageComposer_hangup object, +.mx_MessageComposer_voicecall object, +.mx_MessageComposer_videocall object, +.mx_MessageComposer_apps object, +.mx_MessageComposer_stickers object { + pointer-events: none; +} + +.mx_MessageComposer_formatting { + cursor: pointer; + margin: 0 11px; + width: 24px; + height: 18px; +} + +.mx_MessageComposer_formatbar_wrapper { + width: 100%; + background-color: $menu-bg-color; + box-shadow: inset 0 1px 0 0 rgba(0, 0, 0, 0.08); +} + +.mx_MessageComposer_formatbar { + margin: auto; + max-width: 960px; + display: flex; + + height: 30px; + + box-sizing: border-box; + padding-left: 62px; + + flex-direction: row; + align-items: center; + font-size: 10px; + color: $greyed-fg-color; +} + +.mx_MessageComposer_formatbar * { + margin-right: 4px; +} + +.mx_MessageComposer_format_button, +.mx_MessageComposer_formatbar_cancel, +.mx_MessageComposer_formatbar_markdown { + cursor: pointer; +} + +.mx_MessageComposer_formatbar_cancel { + margin-right: 22px; +} + +.mx_MessageComposer_formatbar_markdown { + margin-right: 64px; +} + +.mx_MessageComposer_input_markdownIndicator { + cursor: pointer; + height: 10px; + padding: 4px 4px 4px 0; + opacity: 0.8; +} + +.mx_MatrixChat_useCompactLayout { + .mx_MessageComposer_input { + min-height: 50px; + } + + .mx_MessageComposer_noperm_error { + height: 50px; + } +} diff --git a/res/css/views/rooms/_PinnedEventTile.scss b/res/css/views/rooms/_PinnedEventTile.scss new file mode 100644 index 0000000000..f7417272b6 --- /dev/null +++ b/res/css/views/rooms/_PinnedEventTile.scss @@ -0,0 +1,77 @@ +/* +Copyright 2017 Travis Ralston + +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. +*/ + +.mx_PinnedEventTile { + min-height: 40px; + margin-bottom: 5px; + width: 100%; + border-radius: 5px; // for the hover +} + +.mx_PinnedEventTile:hover { + background-color: $event-selected-color; +} + +.mx_PinnedEventTile .mx_PinnedEventTile_sender, +.mx_PinnedEventTile .mx_PinnedEventTile_timestamp { + color: #868686; + font-size: 0.8em; + vertical-align: top; + display: inline-block; + padding-bottom: 3px; +} + +.mx_PinnedEventTile .mx_PinnedEventTile_timestamp { + padding-left: 15px; + display: none; +} + +.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar { + float: left; + margin-right: 10px; +} + +.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp { + display: inline-block; +} + +.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions { + display: block; +} + +.mx_PinnedEventTile_actions { + float: right; + margin-right: 10px; + display: none; +} + +.mx_PinnedEventTile_unpinButton { + display: inline-block; + cursor: pointer; + margin-left: 10px; +} + +.mx_PinnedEventTile_gotoButton { + display: inline-block; + font-size: 0.7em; // Smaller text to avoid conflicting with the layout +} + +.mx_PinnedEventTile_message { + margin-left: 50px; + position: relative; + top: 0; + left: 0; +} \ No newline at end of file diff --git a/res/css/views/rooms/_PinnedEventsPanel.scss b/res/css/views/rooms/_PinnedEventsPanel.scss new file mode 100644 index 0000000000..663d5bdf6e --- /dev/null +++ b/res/css/views/rooms/_PinnedEventsPanel.scss @@ -0,0 +1,37 @@ +/* +Copyright 2017 Travis Ralston + +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. +*/ + +.mx_PinnedEventsPanel { + border-top: 1px solid $primary-hairline-color; +} + +.mx_PinnedEventsPanel_body { + max-height: 300px; + overflow-y: auto; + padding-bottom: 15px; +} + +.mx_PinnedEventsPanel_header { + margin: 0; + padding-top: 8px; + padding-bottom: 15px; +} + +.mx_PinnedEventsPanel_cancel { + margin: 12px; + float: right; + display: inline-block; +} diff --git a/res/css/views/rooms/_PresenceLabel.scss b/res/css/views/rooms/_PresenceLabel.scss new file mode 100644 index 0000000000..682c849cee --- /dev/null +++ b/res/css/views/rooms/_PresenceLabel.scss @@ -0,0 +1,20 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +.mx_PresenceLabel { + font-size: 11px; + opacity: 0.5; +} \ No newline at end of file diff --git a/res/css/views/rooms/_ReplyPreview.scss b/res/css/views/rooms/_ReplyPreview.scss new file mode 100644 index 0000000000..5bf4adff27 --- /dev/null +++ b/res/css/views/rooms/_ReplyPreview.scss @@ -0,0 +1,52 @@ +/* +Copyright 2018 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. +*/ + +.mx_ReplyPreview { + position: absolute; + bottom: 0; + z-index: 1000; + width: 100%; + border: 1px solid $primary-hairline-color; + background: $primary-bg-color; + border-bottom: none; + border-radius: 4px 4px 0 0; + max-height: 50vh; + overflow: auto +} + +.mx_ReplyPreview_section { + border-bottom: 1px solid $primary-hairline-color; +} + +.mx_ReplyPreview_header { + margin: 12px; + color: $primary-fg-color; + font-weight: 400; + opacity: 0.4; +} + +.mx_ReplyPreview_title { + float: left; +} + +.mx_ReplyPreview_cancel { + float: right; + cursor: pointer; +} + +.mx_ReplyPreview_clear { + clear: both; +} diff --git a/res/css/views/rooms/_RoomDropTarget.scss b/res/css/views/rooms/_RoomDropTarget.scss new file mode 100644 index 0000000000..1076a0563a --- /dev/null +++ b/res/css/views/rooms/_RoomDropTarget.scss @@ -0,0 +1,55 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_RoomDropTarget_container { + background-color: $secondary-accent-color; + padding-left: 18px; + padding-right: 18px; + padding-top: 8px; + padding-bottom: 7px; +} + +.collapsed .mx_RoomDropTarget_container { + padding-right: 10px; + padding-left: 10px; +} + +.mx_RoomDropTarget { + font-size: 13px; + padding-top: 5px; + padding-bottom: 5px; + border: 1px dashed $accent-color; + color: $primary-fg-color; + background-color: $droptarget-bg-color; + border-radius: 4px; +} + + +.mx_RoomDropTarget_label { + position: relative; + margin-top: 3px; + line-height: 21px; + z-index: 1; + text-align: center; +} + +.collapsed .mx_RoomDropTarget_avatar { + float: none; +} + +.collapsed .mx_RoomDropTarget_label { + display: none; +} diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss new file mode 100644 index 0000000000..9c1349adbc --- /dev/null +++ b/res/css/views/rooms/_RoomHeader.scss @@ -0,0 +1,245 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +/* add 20px to the height of the header when editing */ +.mx_RoomHeader_editing { + flex: 0 0 93px ! important; +} + +.mx_RoomHeader_wrapper { + max-width: 960px; + margin: auto; + height: 70px; + align-items: center; + display: flex; +} + +.mx_RoomHeader_leftRow { + margin-left: -2px; + order: 1; + flex: 1; + overflow: hidden; +} + +.mx_RoomHeader_spinner { + height: 36px; + order: 2; + padding-left: 12px; + padding-right: 12px; +} + +.mx_RoomHeader_textButton { + @mixin mx_DialogButton; + margin-right: 8px; + margin-top: -5px; + order: 2; +} + +.mx_RoomHeader_textButton:hover { + @mixin mx_DialogButton_hover; +} + +.mx_RoomHeader_textButton_danger { + background-color: $warning-color; +} + +.mx_RoomHeader_cancelButton { + order: 2; + cursor: pointer; + padding-left: 12px; + padding-right: 12px; +} + +.mx_RoomHeader_rightRow { + margin-top: 4px; + background-color: $primary-bg-color; + display: flex; + align-items: center; + order: 3; +} + +.mx_RoomHeader_info { + display: table-cell; + width: 100%; + vertical-align: middle; +} + +.mx_RoomHeader_simpleHeader { + line-height: 70px; + color: $primary-fg-color; + font-size: 22px; + font-weight: bold; + overflow: hidden; + margin-left: 63px; + text-overflow: ellipsis; + width: 100%; +} + +.mx_RoomHeader_simpleHeader .mx_RoomHeader_cancelButton { + float: right; +} + +.mx_RoomHeader_simpleHeader .mx_RoomHeader_icon { + margin-left: 14px; + margin-right: 24px; + vertical-align: -4px; +} + +.mx_RoomHeader_name { + vertical-align: middle; + width: 100%; + height: 31px; + overflow: hidden; + color: $primary-fg-color; + font-weight: bold; + font-size: 22px; + padding-left: 19px; + padding-right: 16px; + /* why isn't text-overflow working? */ + text-overflow: ellipsis; + border-bottom: 1px solid transparent; +} + +.mx_RoomHeader_nametext { + display: inline-block; +} + +.mx_RoomHeader_settingsHint { + color: $settings-grey-fg-color ! important; +} + +.mx_RoomHeader_searchStatus { + display: inline-block; + font-weight: normal; + opacity: 0.6; +} + +.mx_RoomHeader_settingsButton object { + pointer-events: none; +} + +.mx_RoomHeader_name, +.mx_RoomHeader_avatar, +.mx_RoomHeader_avatarPicker, +.mx_RoomHeader_avatarPicker_edit, +.mx_RoomHeader_avatarPicker_remove { + cursor: pointer; +} + +.mx_RoomHeader_avatarPicker_remove { + position: absolute; + top: -11px; + right: -9px; +} + +.mx_RoomHeader_name:hover div:not(.mx_RoomHeader_editable) { + color: $accent-color; +} + +.mx_RoomHeader_placeholder { + color: $settings-grey-fg-color ! important; +} + +.mx_RoomHeader_editable { + border-bottom: 1px solid $strong-input-border-color ! important; + min-width: 150px; + cursor: text; +} + +.mx_RoomHeader_editable:focus { + border-bottom: 1px solid $accent-color ! important; + outline: none; + box-shadow: none; +} + +.mx_RoomHeader_topic { + vertical-align: bottom; + float: left; + max-height: 38px; + color: $settings-grey-fg-color; + font-weight: 300; + font-size: 13px; + margin-left: 19px; + margin-right: 16px; + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 1px solid transparent; + column-width: 960px; +} + +.mx_RoomHeader_avatar { + display: table-cell; + width: 48px; + height: 50px; + vertical-align: middle; +} + +.mx_RoomHeader_avatar .mx_BaseAvatar_image { + object-fit: cover; +} + +.mx_RoomHeader_avatarPicker { + margin-top: 23px; + position: relative; +} + +.mx_RoomHeader_avatarPicker_edit { + margin-left: 16px; + margin-top: 4px; +} + +.mx_RoomHeader_avatarPicker_edit > label { + cursor: pointer; +} + +.mx_RoomHeader_avatarPicker_edit > input { + display: none; +} + +.mx_RoomHeader_button { + margin-left: 12px; + cursor: pointer; +} + +.mx_RoomHeader_button object { + pointer-events: none; +} + +.mx_RoomHeader_voipButton { + display: table-cell; +} + +.mx_RoomHeader_voipButtons { + margin-top: 18px; +} + +.mx_RoomHeader_pinnedButton { + position: relative; +} + +.mx_RoomHeader_pinsIndicator { + position: absolute; + right: 0; + bottom: 4px; + width: 8px; + height: 8px; + border-radius: 8px; + background-color: $pinned-color; +} + +.mx_RoomHeader_pinsIndicatorUnread { + background-color: $pinned-unread-color; +} diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss new file mode 100644 index 0000000000..581016d5ba --- /dev/null +++ b/res/css/views/rooms/_RoomList.scss @@ -0,0 +1,67 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2107 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. +*/ + +.mx_RoomList { + padding-bottom: 12px; + min-height: 400px; +} + +.mx_RoomList_expandButton { + margin-left: 8px; + cursor: pointer; + padding-left: 12px; + padding-right: 12px; +} + +/* Evil hacky override until Chrome fixes drop and drag table cells + and we can correctly fix horizontal wrapping in the sidebar again */ +.mx_RoomList_scrollbar .gm-scroll-view { + overflow-x: hidden ! important; + overflow-y: scroll ! important; +} + +/* Make sure the scrollbar is above the sticky headers from RoomList */ +.mx_RoomList_scrollbar .gm-scrollbar.-vertical { + z-index: 6; +} + +.mx_RoomList_emptySubListTip_container { + background-color: $secondary-accent-color; + padding-left: 18px; + padding-right: 18px; + padding-top: 8px; + padding-bottom: 7px; +} + +.mx_RoomList_emptySubListTip { + font-size: 13px; + padding: 5px; + border: 1px dashed $accent-color; + color: $primary-fg-color; + background-color: $droptarget-bg-color; + border-radius: 4px; + line-height: 16px; +} + +.mx_RoomList_emptySubListTip .mx_RoleButton { + vertical-align: -2px; +} + +.mx_RoomList_headerButtons { + position: absolute; + right: 60px; +} diff --git a/res/css/views/rooms/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss new file mode 100644 index 0000000000..331eb582ea --- /dev/null +++ b/res/css/views/rooms/_RoomPreviewBar.scss @@ -0,0 +1,58 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_RoomPreviewBar { + text-align: center; + height: 176px; + background-color: $event-selected-color; + align-items: center; + flex-direction: column; + justify-content: center; + display: flex; + background-color: $preview-bar-bg-color; + -webkit-align-items: center; +} + +.mx_RoomPreviewBar_wrapper { +} + +.mx_RoomPreviewBar_invite_text { + color: $primary-fg-color; +} + +.mx_RoomPreviewBar_join_text { + color: $warning-color; +} + +.mx_RoomPreviewBar_preview_text { + margin-top: 25px; + color: $settings-grey-fg-color; +} + +.mx_RoomPreviewBar_join_text a { + text-decoration: underline; + cursor: pointer; +} + +.mx_RoomPreviewBar_warning { + display: flex; + align-items: center; + padding: 8px; +} + +.mx_RoomPreviewBar_warningIcon { + padding: 12px; +} diff --git a/res/css/views/rooms/_RoomSettings.scss b/res/css/views/rooms/_RoomSettings.scss new file mode 100644 index 0000000000..f04042ea77 --- /dev/null +++ b/res/css/views/rooms/_RoomSettings.scss @@ -0,0 +1,253 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +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. +*/ + +.mx_RoomSettings { + margin-left: 65px; + margin-bottom: 20px; +} + +.mx_RoomSettings_upgradeButton, +.mx_RoomSettings_leaveButton, +.mx_RoomSettings_unbanButton { + @mixin mx_DialogButton; + position: relative; + margin-right: 8px; +} + +.mx_RoomSettings_upgradeButton, +.mx_RoomSettings_leaveButton:hover, +.mx_RoomSettings_unbanButton:hover { + @mixin mx_DialogButton_hover; +} + +.mx_RoomSettings_upgradeButton.danger { + @mixin mx_DialogButton_danger; +} + +.mx_RoomSettings_integrationsButton_error { + position: relative; + cursor: not-allowed; +} +.mx_RoomSettings_integrationsButton_error img { + position: absolute; + right: -5px; + top: -5px; +} +.mx_RoomSettings_leaveButton, +.mx_RoomSettings_integrationsButton_error { + float: right; +} +.mx_RoomSettings_integrationsButton_error .mx_RoomSettings_integrationsButton_errorPopup { + display: none; +} +.mx_RoomSettings_integrationsButton_error:hover .mx_RoomSettings_integrationsButton_errorPopup { + display: inline; +} +.mx_RoomSettings_integrationsButton_errorPopup { + position: absolute; + top: 110%; + left: -125%; + width: 348%; + padding: 2%; + font-size: 10pt; + line-height: 1.5em; + border-radius: 5px; + background-color: $accent-color; + color: $accent-fg-color; + text-align: center; +} +.mx_RoomSettings_unbanButton { + display: inline; +} + +.mx_RoomSettings_e2eIcon { + padding-left: 4px; + padding-right: 7px; +} + +.mx_RoomSettings_leaveButton { + margin-right: 32px; +} + +.mx_RoomSettings_powerLevels { + display: table; +} + +.mx_RoomSettings_powerLevel { + display: table-row; +} + +.mx_RoomSettings_powerLevelKey, +.mx_RoomSettings_powerLevel .mx_PowerSelector { + display: table-cell; + padding-bottom: 5px; +} + +.mx_RoomSettings_powerLevelKey { + text-align: right; + padding-right: 0.3em; +} + +.mx_RoomSettings h3 { + text-transform: uppercase; + color: $h3-color; + font-weight: 600; + font-size: 13px; + margin-top: 36px; + margin-bottom: 10px; +} + +.mx_RoomSettings .mx_RoomSettings_toggles label { + margin-bottom: 8px; + display: block; +} + +.mx_RoomSettings .mx_RoomSettings_toggles input[type="checkbox"], +.mx_RoomSettings .mx_RoomSettings_toggles input[type="radio"] { + margin-right: 7px; +} + +.mx_RoomSettings .mx_RoomSettings_tags input[type="checkbox"] { + margin-left: 1em; + margin-right: 7px; +} + +.mx_RoomSettings .mx_RoomSettings_tags { + margin-bottom: 8px; +} + +.mx_RoomSettings .mx_RoomSettings_roomColor { + display: inline-block; + position: relative; + width: 37px; + height: 37px; + border: 1px solid #979797; + margin-right: 13px; + cursor: pointer; +} + +.mx_RoomSettings .mx_RoomSettings_roomColor_selected { + position: absolute; + left: 10px; + top: 4px; + cursor: default ! important; +} + +.mx_RoomSettings .mx_RoomSettings_roomColorPrimary { + height: 10px; + position: absolute; + bottom: 0px; + width: 100%; +} + +.mx_RoomSettings .mx_RoomSettings_aliasLabel { + margin-bottom: 8px; +} + +.mx_RoomSettings .mx_RoomSettings_aliasesTable { + margin-top: 12px; + margin-bottom: 0px; + margin-left: 56px; + display: table; +} + +.mx_RoomSettings .mx_RoomSettings_aliasesTableRow { + display: table-row; + margin-bottom: 16px; +} + +.mx_RoomSettings .mx_RoomSettings_alias { + max-width: 400px; + margin-bottom: 16px; + /* + commented out so margin applies + display: table-cell; */ +} + +.mx_RoomSettings .mx_RoomSettings_addAlias, +.mx_RoomSettings .mx_RoomSettings_deleteAlias { + display: table-cell; + padding-left: 0.5em; + position: relative; + cursor: pointer; +} + +.mx_RoomSettings .mx_RoomSettings_addAlias img, +.mx_RoomSettings .mx_RoomSettings_deleteAlias img { + visibility: hidden; +} + +.mx_RoomSettings .mx_RoomSettings_aliasesTableRow:hover .mx_RoomSettings_addAlias img, +.mx_RoomSettings .mx_RoomSettings_aliasesTableRow:hover .mx_RoomSettings_deleteAlias img { + visibility: visible; +} + +.mx_RoomSettings_warning { + color: $warning-color; + font-weight: bold; + margin-top: 8px; + margin-bottom: 8px; +} + +.mx_RoomSettings_editable { + border: 0px; + border-bottom: 1px solid $strong-input-border-color; + padding: 0px; + min-width: 240px; +} + +.mx_RoomSettings_editable:focus { + border-bottom: 1px solid $accent-color; + outline: none; + box-shadow: none; +} + +.mx_RoomSettings_deleteAlias, +.mx_RoomSettings_addAlias { + display: table-cell; + visibility: visible; +} + +.mx_RoomSettings_deleteAlias:hover, +.mx_RoomSettings_addAlias:hover { + visibility: visible; +} + +.mx_RoomSettings_aliasPlaceholder { + color: $settings-grey-fg-color; +} + +.mx_RoomSettings_buttons { + text-align: right; + margin-bottom: 16px; +} + +.mx_RoomSettings_button { + display: inline; + border: 0px; + height: 36px; + border-radius: 36px; + font-weight: 400; + font-size: 15px; + color: $accent-fg-color; + background-color: $accent-color; + width: auto; + margin: auto; + padding: 6px; + padding-left: 1em; + padding-right: 1em; +} diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss new file mode 100644 index 0000000000..ccd3afe26c --- /dev/null +++ b/res/css/views/rooms/_RoomTile.scss @@ -0,0 +1,190 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_RoomTile { + position: relative; + cursor: pointer; + font-size: 13px; + display: block; + height: 34px; + + background-color: $secondary-accent-color; +} + +.mx_RoomTile_tooltip { + display: inline-block; + position: relative; + top: -54px; + left: -12px; +} + + +.mx_RoomTile_nameContainer { + display: inline-block; + width: 180px; + height: 24px; +} + +.mx_RoomTile_avatar_container { + position: relative; +} + +.mx_RoomTile_avatar { + display: inline-block; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 16px; + padding-right: 6px; + width: 24px; + height: 24px; + vertical-align: middle; +} + +.mx_RoomTile_dm { + display: block; + position: absolute; + bottom: 0; + right: -5px; + z-index: 2; +} + +.mx_RoomTile_name { + display: inline-block; + position: relative; + width: 165px; + vertical-align: middle; + padding-left: 6px; + padding-right: 6px; + padding-top: 2px; + padding-bottom: 3px; + color: $roomtile-name-color; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.mx_RoomTile_invite { +/* color: rgba(69, 69, 69, 0.5); */ +} + +.collapsed .mx_RoomTile_nameContainer { + width: 60px; /* colapsed panel width */ +} + +.collapsed .mx_RoomTile_name { + display: none; +} + +.collapsed .mx_RoomTile_badge { + top: 0px; + min-width: 12px; + border-radius: 16px; + padding: 0px 4px 0px 4px; + z-index: 3; +} + +/* Hide the bottom of speech bubble */ +.collapsed .mx_RoomTile_highlight .mx_RoomTile_badge:after { + display: none; +} + +/* This is the bottom of the speech bubble */ +.mx_RoomTile_highlight .mx_RoomTile_badge:after { + content: ""; + position: absolute; + display: block; + width: 0; + height: 0; + margin-left: 5px; + border-top: 5px solid $warning-color; + border-right: 7px solid transparent; +} + +.mx_RoomTile_badge { + display: inline-block; + min-width: 15px; + height: 15px; + position: absolute; + right: 8px; /*gutter */ + top: 9px; + border-radius: 8px; + color: $accent-fg-color; + font-weight: 600; + font-size: 10px; + text-align: center; + padding-top: 1px; + padding-left: 4px; + padding-right: 4px; +} + +.mx_RoomTile .mx_RoomTile_badge.mx_RoomTile_badgeButton, +.mx_RoomTile.mx_RoomTile_menuDisplayed .mx_RoomTile_badge { + letter-spacing: 0.1em; + opacity: 1; +} + +.mx_RoomTile.mx_RoomTile_noBadges .mx_RoomTile_badge.mx_RoomTile_badgeButton, +.mx_RoomTile.mx_RoomTile_menuDisplayed.mx_RoomTile_noBadges .mx_RoomTile_badge { + background-color: $neutral-badge-color; +} + +.mx_RoomTile_unreadNotify .mx_RoomTile_badge { + background-color: $accent-color; +} + +.mx_RoomTile_highlight .mx_RoomTile_badge { + background-color: $warning-color; +} + +.mx_RoomTile_unread, .mx_RoomTile_highlight { + font-weight: 800; +} + +.mx_RoomTile_selected { + background-color: $roomtile-selected-bg-color; +} + +.mx_DNDRoomTile { + transform: none; + transition: transform 0.2s; +} + +.mx_DNDRoomTile_dragging { + transform: scale(1.05, 1.05); +} + +.mx_RoomTile:focus { + filter: none ! important; + background-color: $roomtile-focused-bg-color; +} + +.mx_RoomTile .mx_RoomTile_name.mx_RoomTile_badgeShown { + width: 140px; +} + +.mx_RoomTile_arrow { + position: absolute; + right: 0px; +} + +.mx_RoomTile.mx_RoomTile_transparent { + background-color: transparent; +} + +.mx_RoomTile.mx_RoomTile_transparent:focus { + background-color: $roomtile-transparent-focused-color; +} + diff --git a/res/css/views/rooms/_RoomTooltip.scss b/res/css/views/rooms/_RoomTooltip.scss new file mode 100644 index 0000000000..9988425b8f --- /dev/null +++ b/res/css/views/rooms/_RoomTooltip.scss @@ -0,0 +1,54 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_RoomTooltip_chevron { + position: absolute; + left: -8px; + top: 4px; + width: 0; + height: 0; + border-top: 8px solid transparent; + border-right: 8px solid $menu-border-color; + border-bottom: 8px solid transparent; +} + +.mx_RoomTooltip_chevron:after { + content:''; + width: 0; + height: 0; + border-top: 7px solid transparent; + border-right: 7px solid $primary-bg-color; + border-bottom: 7px solid transparent; + position:absolute; + top: -7px; + left: 1px; +} + +.mx_RoomTooltip { + display: none; + position: fixed; + border: 1px solid $menu-border-color; + border-radius: 5px; + background-color: $primary-bg-color; + z-index: 2000; + padding: 5px; + pointer-events: none; + line-height: 14px; + font-size: 13px; + color: $primary-fg-color; + max-width: 600px; + margin-right: 50px; +} diff --git a/res/css/views/rooms/_RoomUpgradeWarningBar.scss b/res/css/views/rooms/_RoomUpgradeWarningBar.scss new file mode 100644 index 0000000000..82785b82d2 --- /dev/null +++ b/res/css/views/rooms/_RoomUpgradeWarningBar.scss @@ -0,0 +1,48 @@ +/* +Copyright 2018 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. +*/ + +.mx_RoomUpgradeWarningBar { + text-align: center; + height: 176px; + background-color: $event-selected-color; + align-items: center; + flex-direction: column; + justify-content: center; + display: flex; + background-color: $preview-bar-bg-color; + -webkit-align-items: center; + padding-left: 20px; + padding-right: 20px; +} + +.mx_RoomUpgradeWarningBar_header { + color: $warning-color; + font-weight: bold; +} + +.mx_RoomUpgradeWarningBar_body { + color: $warning-color; +} + +.mx_RoomUpgradeWarningBar_upgradelink { + color: $warning-color; + text-decoration: underline; +} + +.mx_RoomUpgradeWarningBar_small { + color: $greyed-fg-color; + font-size: 70%; +} diff --git a/res/css/views/rooms/_SearchBar.scss b/res/css/views/rooms/_SearchBar.scss new file mode 100644 index 0000000000..079ea16c68 --- /dev/null +++ b/res/css/views/rooms/_SearchBar.scss @@ -0,0 +1,83 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_SearchBar { + padding-top: 5px; + padding-bottom: 5px; + display: flex; + align-items: center; +} + +.mx_SearchBar_input { + display: inline block; + border-radius: 3px 0px 0px 3px; + border: 1px solid $input-border-color; + font-size: 15px; + padding: 9px; + padding-left: 11px; + width: auto; + flex: 1 1 0; +} + +.mx_SearchBar_searchButton { + cursor: pointer; + margin-right: 10px; + width: 37px; + height: 37px; + border-radius: 0px 3px 3px 0px; + background-color: $accent-color; +} + +@keyframes pulsate { + 0% { opacity: 1.0; } + 50% { opacity: 0.1; } + 100% { opacity: 1.0; } +} + +.mx_SearchBar_searching img { + animation: pulsate 0.5s ease-out; + animation-iteration-count: infinite; +} + +.mx_SearchBar_button { + display: inline; + border: 0px; + border-radius: 36px; + font-weight: 400; + font-size: 15px; + color: $accent-fg-color; + background-color: $accent-color; + width: auto; + margin: auto; + margin-left: 7px; + padding-top: 6px; + padding-bottom: 4px; + padding-left: 24px; + padding-right: 24px; + cursor: pointer; +} + +.mx_SearchBar_unselected { + background-color: $primary-bg-color; + color: $accent-color; + border: $accent-color 1px solid; +} + +.mx_SearchBar_cancel { + padding-left: 14px; + padding-right: 14px; + cursor: pointer; +} diff --git a/res/css/views/rooms/_SearchableEntityList.scss b/res/css/views/rooms/_SearchableEntityList.scss new file mode 100644 index 0000000000..37a663123d --- /dev/null +++ b/res/css/views/rooms/_SearchableEntityList.scss @@ -0,0 +1,77 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +.mx_SearchableEntityList { + display: flex; + + flex-direction: column; +} + +.mx_SearchableEntityList_query { + font-family: $font-family; + border-radius: 3px; + border: 1px solid $input-border-color; + padding: 9px; + color: $primary-fg-color; + background-color: $primary-bg-color; + margin-left: 3px; + font-size: 15px; + margin-bottom: 8px; + width: 189px; +} + +.mx_SearchableEntityList_query::-moz-placeholder { + color: $primary-fg-color; + opacity: 0.5; + font-size: 12px; +} + +.mx_SearchableEntityList_query::-webkit-input-placeholder { + color: $primary-fg-color; + opacity: 0.5; + font-size: 12px; +} + +.mx_SearchableEntityList_listWrapper { + flex: 1; + + overflow-y: auto; +} + +.mx_SearchableEntityList_list { + display: table; + table-layout: fixed; + width: 100%; +} + +.mx_SearchableEntityList_list .mx_EntityTile_chevron { + display: none; +} + +.mx_SearchableEntityList_hrWrapper { + width: 100%; + flex: 0 0 auto; +} + +.mx_SearchableEntityList hr { + height: 1px; + border: 0px; + color: $primary-fg-color; + background-color: $primary-fg-color; + margin-right: 15px; + margin-top: 11px; + margin-bottom: 11px; +} diff --git a/res/css/views/rooms/_Stickers.scss b/res/css/views/rooms/_Stickers.scss new file mode 100644 index 0000000000..669ca13545 --- /dev/null +++ b/res/css/views/rooms/_Stickers.scss @@ -0,0 +1,35 @@ +.mx_Stickers_content { + overflow: hidden; +} + +.mx_Stickers_content_container { + overflow: hidden; + height: 300px; +} + +.mx_Stickers_content .mx_AppTileFullWidth { + border: none; +} + +.mx_Stickers_contentPlaceholder { + display: flex; + flex-grow: 1; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +.mx_Stickers_contentPlaceholder p { + max-width: 200px; +} + +.mx_Stickers_addLink { + display: inline; + cursor: pointer; + text-decoration: underline; +} + +.mx_Stickers_hideStickers { + z-index: 2001; +} diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.scss b/res/css/views/rooms/_TopUnreadMessagesBar.scss new file mode 100644 index 0000000000..1ee56d9532 --- /dev/null +++ b/res/css/views/rooms/_TopUnreadMessagesBar.scss @@ -0,0 +1,53 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +.mx_TopUnreadMessagesBar { + margin: auto; /* centre horizontally */ + max-width: 960px; + padding-top: 10px; + padding-bottom: 10px; + border-bottom: 1px solid $primary-hairline-color; +} + +.mx_TopUnreadMessagesBar_scrollUp { + display: inline; + cursor: pointer; + text-decoration: underline; +} + +.mx_TopUnreadMessagesBar_scrollUp img { + padding-left: 10px; + padding-right: 31px; + vertical-align: middle; +} + +.mx_TopUnreadMessagesBar_scrollUp span { + opacity: 0.5; +} + +.mx_TopUnreadMessagesBar_close { + float: right; + padding-right: 14px; + padding-top: 3px; + cursor: pointer; +} + +.mx_MatrixChat_useCompactLayout { + .mx_TopUnreadMessagesBar { + padding-top: 4px; + padding-bottom: 4px; + } +} diff --git a/res/css/views/settings/_DevicesPanel.scss b/res/css/views/settings/_DevicesPanel.scss new file mode 100644 index 0000000000..e4856531d9 --- /dev/null +++ b/res/css/views/settings/_DevicesPanel.scss @@ -0,0 +1,51 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +.mx_DevicesPanel { + display: table; + table-layout: fixed; + width: 880px; + border-spacing: 2px; +} + +.mx_DevicesPanel_header { + display: table-header-group; + font-weight: bold; +} + +.mx_DevicesPanel_header > div { + display: table-cell; +} + +.mx_DevicesPanel_header .mx_DevicesPanel_deviceLastSeen { + width: 30%; +} + +.mx_DevicesPanel_header .mx_DevicesPanel_deviceButtons { + width: 20%; +} + +.mx_DevicesPanel_device { + display: table-row; +} + +.mx_DevicesPanel_device > div { + display: table-cell; +} + +.mx_DevicesPanel_myDevice { + font-weight: bold; +} \ No newline at end of file diff --git a/res/css/views/settings/_IntegrationsManager.scss b/res/css/views/settings/_IntegrationsManager.scss new file mode 100644 index 0000000000..93ee0e20fe --- /dev/null +++ b/res/css/views/settings/_IntegrationsManager.scss @@ -0,0 +1,31 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_IntegrationsManager .mx_Dialog { + width: 60%; + height: 70%; + overflow: hidden; + padding: 0px; + max-width: initial; + max-height: initial; +} + +.mx_IntegrationsManager iframe { + background-color: #fff; + border: 0px; + width: 100%; + height: 100%; +} diff --git a/res/css/views/settings/_Notifications.scss b/res/css/views/settings/_Notifications.scss new file mode 100644 index 0000000000..4c88e44952 --- /dev/null +++ b/res/css/views/settings/_Notifications.scss @@ -0,0 +1,70 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_UserNotifSettings_tableRow +{ + display: table-row; +} + +.mx_UserNotifSettings_inputCell { + display: table-cell; + padding-bottom: 8px; + padding-right: 8px; + width: 16px; +} + +.mx_UserNotifSettings_labelCell +{ + padding-bottom: 8px; + width: 400px; + display: table-cell; +} + +.mx_UserNotifSettings_pushRulesTableWrapper { + padding-bottom: 8px; +} + +.mx_UserNotifSettings_pushRulesTable { + width: 100%; + table-layout: fixed; +} + +.mx_UserNotifSettings_pushRulesTable thead { + font-weight: bold; + font-size: 15px; +} + +.mx_UserNotifSettings_pushRulesTable tbody th { + font-weight: 400; + font-size: 15px; +} + +.mx_UserNotifSettings_pushRulesTable tbody th:first-child { + text-align: left; +} + +.mx_UserNotifSettings_keywords { + cursor: pointer; + color: $accent-color; +} + +.mx_UserSettings_devicesTable td { + padding-left: 20px; + padding-right: 20px; +} +.mx_UserSettings_devicesTable_nodevices { + font-style: italic; +} diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss new file mode 100644 index 0000000000..deb89a837c --- /dev/null +++ b/res/css/views/voip/_CallView.scss @@ -0,0 +1,25 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_CallView_voice { + background-color: $accent-color; + color: $accent-fg-color; + cursor: pointer; + text-align: center; + padding: 6px; + font-weight: bold; + font-size: 13px; +} \ No newline at end of file diff --git a/res/css/views/voip/_IncomingCallbox.scss b/res/css/views/voip/_IncomingCallbox.scss new file mode 100644 index 0000000000..64eac25d01 --- /dev/null +++ b/res/css/views/voip/_IncomingCallbox.scss @@ -0,0 +1,69 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_IncomingCallBox { + text-align: center; + border: 1px solid #a4a4a4; + border-radius: 8px; + background-color: $primary-bg-color; + position: fixed; + z-index: 1000; + padding: 6px; + margin-top: -3px; + margin-left: -20px; + width: 200px; +} + +.mx_IncomingCallBox_chevron { + padding: 12px; + position: absolute; + left: -21px; + top: 0px; +} + +.mx_IncomingCallBox_title { + padding: 6px; + font-weight: bold; +} + +.mx_IncomingCallBox_buttons { + display: flex; +} + +.mx_IncomingCallBox_buttons_cell { + vertical-align: middle; + padding: 6px; + flex: 1; +} + +.mx_IncomingCallBox_buttons_decline, +.mx_IncomingCallBox_buttons_accept { + vertical-align: middle; + width: 80px; + height: 36px; + line-height: 36px; + border-radius: 36px; + color: $accent-fg-color; + margin: auto; +} + +.mx_IncomingCallBox_buttons_decline { + background-color: $voip-decline-color; +} + +.mx_IncomingCallBox_buttons_accept { + background-color: $voip-accept-color; +} diff --git a/res/css/views/voip/_VideoView.scss b/res/css/views/voip/_VideoView.scss new file mode 100644 index 0000000000..feb60f4763 --- /dev/null +++ b/res/css/views/voip/_VideoView.scss @@ -0,0 +1,49 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +.mx_VideoView { + width: 100%; + position: relative; + z-index: 30; +} + +.mx_VideoView video { + width: 100%; +} + +.mx_VideoView_remoteVideoFeed { + width: 100%; + background-color: #000; + z-index: 50; +} + +.mx_VideoView_localVideoFeed { + width: 25%; + height: 25%; + position: absolute; + left: 10px; + bottom: 10px; + z-index: 100; +} + +.mx_VideoView_localVideoFeed video { + width: auto; + height: 100%; +} + +.mx_VideoView_localVideoFeed.mx_VideoView_localVideoFeed_flipped video { + transform: scale(-1, 1); +} diff --git a/res/fonts/Fira_Mono/FiraMono-Bold.ttf b/res/fonts/Fira_Mono/FiraMono-Bold.ttf new file mode 100755 index 0000000000..4b8b1cfbcb Binary files /dev/null and b/res/fonts/Fira_Mono/FiraMono-Bold.ttf differ diff --git a/res/fonts/Fira_Mono/FiraMono-Regular.ttf b/res/fonts/Fira_Mono/FiraMono-Regular.ttf new file mode 100755 index 0000000000..5238c09eda Binary files /dev/null and b/res/fonts/Fira_Mono/FiraMono-Regular.ttf differ diff --git a/res/fonts/Fira_Mono/OFL.txt b/res/fonts/Fira_Mono/OFL.txt new file mode 100755 index 0000000000..ba853c049e --- /dev/null +++ b/res/fonts/Fira_Mono/OFL.txt @@ -0,0 +1,92 @@ +Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A. +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/res/fonts/Open_Sans/LICENSE.txt b/res/fonts/Open_Sans/LICENSE.txt new file mode 100755 index 0000000000..75b52484ea --- /dev/null +++ b/res/fonts/Open_Sans/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/res/fonts/Open_Sans/OpenSans-Bold.ttf b/res/fonts/Open_Sans/OpenSans-Bold.ttf new file mode 100755 index 0000000000..fd79d43bea Binary files /dev/null and b/res/fonts/Open_Sans/OpenSans-Bold.ttf differ diff --git a/res/fonts/Open_Sans/OpenSans-BoldItalic.ttf b/res/fonts/Open_Sans/OpenSans-BoldItalic.ttf new file mode 100755 index 0000000000..9bc800958a Binary files /dev/null and b/res/fonts/Open_Sans/OpenSans-BoldItalic.ttf differ diff --git a/res/fonts/Open_Sans/OpenSans-Italic.ttf b/res/fonts/Open_Sans/OpenSans-Italic.ttf new file mode 100755 index 0000000000..c90da48ff3 Binary files /dev/null and b/res/fonts/Open_Sans/OpenSans-Italic.ttf differ diff --git a/res/fonts/Open_Sans/OpenSans-Regular.ttf b/res/fonts/Open_Sans/OpenSans-Regular.ttf new file mode 100755 index 0000000000..db433349b7 Binary files /dev/null and b/res/fonts/Open_Sans/OpenSans-Regular.ttf differ diff --git a/res/fonts/Open_Sans/OpenSans-Semibold.ttf b/res/fonts/Open_Sans/OpenSans-Semibold.ttf new file mode 100755 index 0000000000..1a7679e394 Binary files /dev/null and b/res/fonts/Open_Sans/OpenSans-Semibold.ttf differ diff --git a/res/fonts/Open_Sans/OpenSans-SemiboldItalic.ttf b/res/fonts/Open_Sans/OpenSans-SemiboldItalic.ttf new file mode 100755 index 0000000000..59b6d16b06 Binary files /dev/null and b/res/fonts/Open_Sans/OpenSans-SemiboldItalic.ttf differ diff --git a/res/img/50e2c2.png b/res/img/50e2c2.png new file mode 100644 index 0000000000..ee0f855895 Binary files /dev/null and b/res/img/50e2c2.png differ diff --git a/res/img/76cfa6.png b/res/img/76cfa6.png new file mode 100644 index 0000000000..de1ea60d54 Binary files /dev/null and b/res/img/76cfa6.png differ diff --git a/res/img/80cef4.png b/res/img/80cef4.png new file mode 100644 index 0000000000..637d03f63c Binary files /dev/null and b/res/img/80cef4.png differ diff --git a/res/img/admin.svg b/res/img/admin.svg new file mode 100644 index 0000000000..7ea7459304 --- /dev/null +++ b/res/img/admin.svg @@ -0,0 +1,17 @@ + + + + icons_owner + Created with sketchtool. + + + + + + + + + + + + diff --git a/res/img/attach.png b/res/img/attach.png new file mode 100644 index 0000000000..1bcb70045d Binary files /dev/null and b/res/img/attach.png differ diff --git a/res/img/avatar-error.svg b/res/img/avatar-error.svg new file mode 100644 index 0000000000..c5e168944c --- /dev/null +++ b/res/img/avatar-error.svg @@ -0,0 +1,15 @@ + + + + 5EF602F6-A36C-41EE-BAEC-50801DFD5492 + Created with sketchtool. + + + + + + + + + + diff --git a/res/img/button-md-false.png b/res/img/button-md-false.png new file mode 100644 index 0000000000..6debbccc93 Binary files /dev/null and b/res/img/button-md-false.png differ diff --git a/res/img/button-md-false.svg b/res/img/button-md-false.svg new file mode 100644 index 0000000000..6414933d96 --- /dev/null +++ b/res/img/button-md-false.svg @@ -0,0 +1,29 @@ + + + + D335F9E8-C813-47D7-B1BE-C8DEF2C8214F + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-md-false@2x.png b/res/img/button-md-false@2x.png new file mode 100644 index 0000000000..497f5385d1 Binary files /dev/null and b/res/img/button-md-false@2x.png differ diff --git a/res/img/button-md-false@3x.png b/res/img/button-md-false@3x.png new file mode 100644 index 0000000000..1184e6b351 Binary files /dev/null and b/res/img/button-md-false@3x.png differ diff --git a/res/img/button-md-true.png b/res/img/button-md-true.png new file mode 100644 index 0000000000..2e39c55e1e Binary files /dev/null and b/res/img/button-md-true.png differ diff --git a/res/img/button-md-true.svg b/res/img/button-md-true.svg new file mode 100644 index 0000000000..2acc4f675c --- /dev/null +++ b/res/img/button-md-true.svg @@ -0,0 +1,14 @@ + + + + 2A63B135-4281-4FBB-A88C-012AE22E9594 + Created with sketchtool. + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-md-true@2x.png b/res/img/button-md-true@2x.png new file mode 100644 index 0000000000..ad9067f385 Binary files /dev/null and b/res/img/button-md-true@2x.png differ diff --git a/res/img/button-md-true@3x.png b/res/img/button-md-true@3x.png new file mode 100644 index 0000000000..d615867dc4 Binary files /dev/null and b/res/img/button-md-true@3x.png differ diff --git a/res/img/button-new-window.svg b/res/img/button-new-window.svg new file mode 100644 index 0000000000..dd1225e798 --- /dev/null +++ b/res/img/button-new-window.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/res/img/button-refresh.svg b/res/img/button-refresh.svg new file mode 100644 index 0000000000..b4990a2147 --- /dev/null +++ b/res/img/button-refresh.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/res/img/button-text-block-quote-on.svg b/res/img/button-text-block-quote-on.svg new file mode 100644 index 0000000000..f8a86125c9 --- /dev/null +++ b/res/img/button-text-block-quote-on.svg @@ -0,0 +1,17 @@ + + + + 3B24B8C7-64BE-4B3E-A748-94DB72E1210F + Created with sketchtool. + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-block-quote.svg b/res/img/button-text-block-quote.svg new file mode 100644 index 0000000000..d70c261f5d --- /dev/null +++ b/res/img/button-text-block-quote.svg @@ -0,0 +1,17 @@ + + + + BFC0418B-9081-4789-A231-B75953157748 + Created with sketchtool. + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-bold-on.svg b/res/img/button-text-bold-on.svg new file mode 100644 index 0000000000..161e740e90 --- /dev/null +++ b/res/img/button-text-bold-on.svg @@ -0,0 +1,17 @@ + + + + 01F3F9B2-8F38-4BAF-A345-AECAC3D88E79 + Created with sketchtool. + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-bold.svg b/res/img/button-text-bold.svg new file mode 100644 index 0000000000..0fd0baa07e --- /dev/null +++ b/res/img/button-text-bold.svg @@ -0,0 +1,17 @@ + + + + 9BC64A5B-F157-43FF-BCC4-02D30CDF520B + Created with sketchtool. + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-bulleted-list-on.svg b/res/img/button-text-bulleted-list-on.svg new file mode 100644 index 0000000000..d4a40e889c --- /dev/null +++ b/res/img/button-text-bulleted-list-on.svg @@ -0,0 +1,20 @@ + + + + 654917CF-20A4-49B6-B0A1-9875D7B733C8 + Created with sketchtool. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-bulleted-list.svg b/res/img/button-text-bulleted-list.svg new file mode 100644 index 0000000000..ae3e640d8e --- /dev/null +++ b/res/img/button-text-bulleted-list.svg @@ -0,0 +1,20 @@ + + + + B7D94619-44BC-4184-A60A-DBC5BB54E5F9 + Created with sketchtool. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-deleted-on.svg b/res/img/button-text-deleted-on.svg new file mode 100644 index 0000000000..2914fcabe6 --- /dev/null +++ b/res/img/button-text-deleted-on.svg @@ -0,0 +1,18 @@ + + + + 69B11088-0F3A-4E14-BD9F-4FEF4115E99B + Created with sketchtool. + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-deleted.svg b/res/img/button-text-deleted.svg new file mode 100644 index 0000000000..5f262dc350 --- /dev/null +++ b/res/img/button-text-deleted.svg @@ -0,0 +1,18 @@ + + + + A34F2223-34C6-46AE-AA47-38EC8984E9B3 + Created with sketchtool. + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-formatting.svg b/res/img/button-text-formatting.svg new file mode 100644 index 0000000000..d697010d40 --- /dev/null +++ b/res/img/button-text-formatting.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/res/img/button-text-inline-code-on.svg b/res/img/button-text-inline-code-on.svg new file mode 100644 index 0000000000..8d1439c97b --- /dev/null +++ b/res/img/button-text-inline-code-on.svg @@ -0,0 +1,25 @@ + + + + B76754AB-42E6-48D2-9443-80CBC0DE02ED + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-inline-code.svg b/res/img/button-text-inline-code.svg new file mode 100644 index 0000000000..24026cb709 --- /dev/null +++ b/res/img/button-text-inline-code.svg @@ -0,0 +1,25 @@ + + + + 4CAFF494-61AE-4916-AFE8-D1E62F7CF0DE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-italic-on.svg b/res/img/button-text-italic-on.svg new file mode 100644 index 0000000000..15fe588596 --- /dev/null +++ b/res/img/button-text-italic-on.svg @@ -0,0 +1,17 @@ + + + + 116426C2-0B55-480E-92B3-57D4B3ABAB90 + Created with sketchtool. + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-italic.svg b/res/img/button-text-italic.svg new file mode 100644 index 0000000000..b5722e827b --- /dev/null +++ b/res/img/button-text-italic.svg @@ -0,0 +1,17 @@ + + + + 9FBC844D-96CF-4DCB-B545-FCD23727218B + Created with sketchtool. + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-numbered-list-on.svg b/res/img/button-text-numbered-list-on.svg new file mode 100644 index 0000000000..869a2c2cc2 --- /dev/null +++ b/res/img/button-text-numbered-list-on.svg @@ -0,0 +1,20 @@ + + + + 294F929B-31AA-4D0C-98B3-9CA96764060D + Created with sketchtool. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-numbered-list.svg b/res/img/button-text-numbered-list.svg new file mode 100644 index 0000000000..8e5b8b87b6 --- /dev/null +++ b/res/img/button-text-numbered-list.svg @@ -0,0 +1,20 @@ + + + + F0F58459-A13A-48C5-9332-ABFB96726F05 + Created with sketchtool. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-underlined-on.svg b/res/img/button-text-underlined-on.svg new file mode 100644 index 0000000000..870be3ce6a --- /dev/null +++ b/res/img/button-text-underlined-on.svg @@ -0,0 +1,18 @@ + + + + FD84FF7C-43E4-4312-90AB-5A59AD018377 + Created with sketchtool. + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/button-text-underlined.svg b/res/img/button-text-underlined.svg new file mode 100644 index 0000000000..26f448539c --- /dev/null +++ b/res/img/button-text-underlined.svg @@ -0,0 +1,18 @@ + + + + 13E7EE68-9B16-4A3D-8F9F-31E4BAB7E438 + Created with sketchtool. + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/call.png b/res/img/call.png new file mode 100644 index 0000000000..a7805e0596 Binary files /dev/null and b/res/img/call.png differ diff --git a/res/img/call.svg b/res/img/call.svg new file mode 100644 index 0000000000..f528f9a24e --- /dev/null +++ b/res/img/call.svg @@ -0,0 +1,17 @@ + + + + icons_video + Created with bin/sketchtool. + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/camera.svg b/res/img/camera.svg new file mode 100644 index 0000000000..6519496f78 --- /dev/null +++ b/res/img/camera.svg @@ -0,0 +1,12 @@ + + + + icon_camera + Created with Sketch. + + + + + + + diff --git a/res/img/camera_green.svg b/res/img/camera_green.svg new file mode 100644 index 0000000000..5aae5502cd --- /dev/null +++ b/res/img/camera_green.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/res/img/cancel-black.png b/res/img/cancel-black.png new file mode 100644 index 0000000000..87dcfd41a8 Binary files /dev/null and b/res/img/cancel-black.png differ diff --git a/res/img/cancel-black2.png b/res/img/cancel-black2.png new file mode 100644 index 0000000000..a928c61b09 Binary files /dev/null and b/res/img/cancel-black2.png differ diff --git a/res/img/cancel-red.svg b/res/img/cancel-red.svg new file mode 100644 index 0000000000..a72a970b62 --- /dev/null +++ b/res/img/cancel-red.svg @@ -0,0 +1,10 @@ + + + + Slice 1 + Created with Sketch. + + + + + diff --git a/res/img/cancel-small.svg b/res/img/cancel-small.svg new file mode 100644 index 0000000000..e4c8cafc10 --- /dev/null +++ b/res/img/cancel-small.svg @@ -0,0 +1,13 @@ + + + + Line + Line + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/res/img/cancel-white.svg b/res/img/cancel-white.svg new file mode 100644 index 0000000000..65e14c2fbc --- /dev/null +++ b/res/img/cancel-white.svg @@ -0,0 +1,10 @@ + + + + Slice 1 + Created with Sketch. + + + + + \ No newline at end of file diff --git a/res/img/cancel.png b/res/img/cancel.png new file mode 100644 index 0000000000..2bda8ff5bf Binary files /dev/null and b/res/img/cancel.png differ diff --git a/res/img/cancel.svg b/res/img/cancel.svg new file mode 100644 index 0000000000..e32060025e --- /dev/null +++ b/res/img/cancel.svg @@ -0,0 +1,10 @@ + + + + Slice 1 + Created with Sketch. + + + + + \ No newline at end of file diff --git a/res/img/cancel_green.svg b/res/img/cancel_green.svg new file mode 100644 index 0000000000..2e3d759be2 --- /dev/null +++ b/res/img/cancel_green.svg @@ -0,0 +1,10 @@ + + + + Slice 1 + Created with Sketch. + + + + + \ No newline at end of file diff --git a/res/img/chevron-left.png b/res/img/chevron-left.png new file mode 100644 index 0000000000..efb0065de9 Binary files /dev/null and b/res/img/chevron-left.png differ diff --git a/res/img/chevron-right.png b/res/img/chevron-right.png new file mode 100644 index 0000000000..18a4684e47 Binary files /dev/null and b/res/img/chevron-right.png differ diff --git a/res/img/chevron.png b/res/img/chevron.png new file mode 100644 index 0000000000..81236f91bc Binary files /dev/null and b/res/img/chevron.png differ diff --git a/res/img/close-white.png b/res/img/close-white.png new file mode 100644 index 0000000000..d8752ed9fe Binary files /dev/null and b/res/img/close-white.png differ diff --git a/res/img/create-big.png b/res/img/create-big.png new file mode 100644 index 0000000000..b7307a11c7 Binary files /dev/null and b/res/img/create-big.png differ diff --git a/res/img/create-big.svg b/res/img/create-big.svg new file mode 100644 index 0000000000..2450542b63 --- /dev/null +++ b/res/img/create-big.svg @@ -0,0 +1,26 @@ + + + + icons_create_room + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/create.png b/res/img/create.png new file mode 100644 index 0000000000..2d6107ac50 Binary files /dev/null and b/res/img/create.png differ diff --git a/res/img/delete.png b/res/img/delete.png new file mode 100644 index 0000000000..8ff20a116d Binary files /dev/null and b/res/img/delete.png differ diff --git a/res/img/directory-big.png b/res/img/directory-big.png new file mode 100644 index 0000000000..03cab69c4a Binary files /dev/null and b/res/img/directory-big.png differ diff --git a/res/img/directory-big.svg b/res/img/directory-big.svg new file mode 100644 index 0000000000..5631a2ae3e --- /dev/null +++ b/res/img/directory-big.svg @@ -0,0 +1,22 @@ + + + + icons_directory + Created with sketchtool. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/download.png b/res/img/download.png new file mode 100644 index 0000000000..1999ebf7ab Binary files /dev/null and b/res/img/download.png differ diff --git a/res/img/download.svg b/res/img/download.svg new file mode 100644 index 0000000000..d0ea090d8a --- /dev/null +++ b/res/img/download.svg @@ -0,0 +1,18 @@ + + + + Fill 75 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/e2e-blocked.svg b/res/img/e2e-blocked.svg new file mode 100644 index 0000000000..0ab2c6efbe --- /dev/null +++ b/res/img/e2e-blocked.svg @@ -0,0 +1,12 @@ + + + + 2805649B-D39D-43EA-A357-659EF9B97BA4 + Created with sketchtool. + + + + + + + \ No newline at end of file diff --git a/res/img/e2e-encrypting.svg b/res/img/e2e-encrypting.svg new file mode 100644 index 0000000000..469611cc8d --- /dev/null +++ b/res/img/e2e-encrypting.svg @@ -0,0 +1,12 @@ + + + +48BF5D32-306C-4B20-88EB-24B1F743CAC9 +Created with sketchtool. + + + + + + + diff --git a/res/img/e2e-not_sent.svg b/res/img/e2e-not_sent.svg new file mode 100644 index 0000000000..fca79ae547 --- /dev/null +++ b/res/img/e2e-not_sent.svg @@ -0,0 +1,12 @@ + + + +48BF5D32-306C-4B20-88EB-24B1F743CAC9 +Created with sketchtool. + + + + + + + diff --git a/res/img/e2e-unencrypted.svg b/res/img/e2e-unencrypted.svg new file mode 100644 index 0000000000..1467223638 --- /dev/null +++ b/res/img/e2e-unencrypted.svg @@ -0,0 +1,23 @@ + + + + 16F5F38E-A6A3-472A-BC13-13F0F12876CF + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/e2e-verified.svg b/res/img/e2e-verified.svg new file mode 100644 index 0000000000..b65f50b2b6 --- /dev/null +++ b/res/img/e2e-verified.svg @@ -0,0 +1,12 @@ + + + + 48BF5D32-306C-4B20-88EB-24B1F743CAC9 + Created with sketchtool. + + + + + + + \ No newline at end of file diff --git a/res/img/e2e-warning.svg b/res/img/e2e-warning.svg new file mode 100644 index 0000000000..8a55f199ba --- /dev/null +++ b/res/img/e2e-warning.svg @@ -0,0 +1,12 @@ + + + + CCDDE6F6-B552-48FD-AD54-6939841CA2DD + Created with sketchtool. + + + + + + + \ No newline at end of file diff --git a/res/img/edit.png b/res/img/edit.png new file mode 100644 index 0000000000..6f373d3f3d Binary files /dev/null and b/res/img/edit.png differ diff --git a/res/img/edit.svg b/res/img/edit.svg new file mode 100644 index 0000000000..9ba0060774 --- /dev/null +++ b/res/img/edit.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/res/img/edit_green.svg b/res/img/edit_green.svg new file mode 100644 index 0000000000..f7f4c7adcb --- /dev/null +++ b/res/img/edit_green.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/res/img/ellipsis.svg b/res/img/ellipsis.svg new file mode 100644 index 0000000000..d60c844089 --- /dev/null +++ b/res/img/ellipsis.svg @@ -0,0 +1,25 @@ + + + + icons_archive + Created with Sketch. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/eol.svg b/res/img/eol.svg new file mode 100644 index 0000000000..02d1946cf4 --- /dev/null +++ b/res/img/eol.svg @@ -0,0 +1,16 @@ + + + + icon_eol + Created with sketchtool. + + + + + + + + + + + diff --git a/res/img/f4c371.png b/res/img/f4c371.png new file mode 100644 index 0000000000..ad3b8f1616 Binary files /dev/null and b/res/img/f4c371.png differ diff --git a/res/img/file.png b/res/img/file.png new file mode 100644 index 0000000000..5904ea8284 Binary files /dev/null and b/res/img/file.png differ diff --git a/res/img/filegrid.png b/res/img/filegrid.png new file mode 100644 index 0000000000..c2c2799f37 Binary files /dev/null and b/res/img/filegrid.png differ diff --git a/res/img/fileicon.png b/res/img/fileicon.png new file mode 100644 index 0000000000..af018efa6d Binary files /dev/null and b/res/img/fileicon.png differ diff --git a/res/img/filelist.png b/res/img/filelist.png new file mode 100644 index 0000000000..3cf6cb494e Binary files /dev/null and b/res/img/filelist.png differ diff --git a/res/img/files.png b/res/img/files.png new file mode 100644 index 0000000000..83932267f8 Binary files /dev/null and b/res/img/files.png differ diff --git a/res/img/files.svg b/res/img/files.svg new file mode 100644 index 0000000000..20aba851ea --- /dev/null +++ b/res/img/files.svg @@ -0,0 +1,18 @@ + + + + icons_browse_files + Created with bin/sketchtool. + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/flags/AD.png b/res/img/flags/AD.png new file mode 100644 index 0000000000..d5d59645fe Binary files /dev/null and b/res/img/flags/AD.png differ diff --git a/res/img/flags/AE.png b/res/img/flags/AE.png new file mode 100644 index 0000000000..05c7418aa4 Binary files /dev/null and b/res/img/flags/AE.png differ diff --git a/res/img/flags/AF.png b/res/img/flags/AF.png new file mode 100644 index 0000000000..bc7cef0916 Binary files /dev/null and b/res/img/flags/AF.png differ diff --git a/res/img/flags/AG.png b/res/img/flags/AG.png new file mode 100644 index 0000000000..d48facad47 Binary files /dev/null and b/res/img/flags/AG.png differ diff --git a/res/img/flags/AI.png b/res/img/flags/AI.png new file mode 100644 index 0000000000..8fd27cd39e Binary files /dev/null and b/res/img/flags/AI.png differ diff --git a/res/img/flags/AL.png b/res/img/flags/AL.png new file mode 100644 index 0000000000..883835ffb3 Binary files /dev/null and b/res/img/flags/AL.png differ diff --git a/res/img/flags/AM.png b/res/img/flags/AM.png new file mode 100644 index 0000000000..b1bb36b987 Binary files /dev/null and b/res/img/flags/AM.png differ diff --git a/res/img/flags/AO.png b/res/img/flags/AO.png new file mode 100644 index 0000000000..ae68b12c44 Binary files /dev/null and b/res/img/flags/AO.png differ diff --git a/res/img/flags/AQ.png b/res/img/flags/AQ.png new file mode 100644 index 0000000000..146e9c0a04 Binary files /dev/null and b/res/img/flags/AQ.png differ diff --git a/res/img/flags/AR.png b/res/img/flags/AR.png new file mode 100644 index 0000000000..8142adfc83 Binary files /dev/null and b/res/img/flags/AR.png differ diff --git a/res/img/flags/AS.png b/res/img/flags/AS.png new file mode 100644 index 0000000000..cc5bf30daf Binary files /dev/null and b/res/img/flags/AS.png differ diff --git a/res/img/flags/AT.png b/res/img/flags/AT.png new file mode 100644 index 0000000000..e32414bd6a Binary files /dev/null and b/res/img/flags/AT.png differ diff --git a/res/img/flags/AU.png b/res/img/flags/AU.png new file mode 100644 index 0000000000..8d1e143791 Binary files /dev/null and b/res/img/flags/AU.png differ diff --git a/res/img/flags/AW.png b/res/img/flags/AW.png new file mode 100644 index 0000000000..6ec178847e Binary files /dev/null and b/res/img/flags/AW.png differ diff --git a/res/img/flags/AX.png b/res/img/flags/AX.png new file mode 100644 index 0000000000..ba269c0453 Binary files /dev/null and b/res/img/flags/AX.png differ diff --git a/res/img/flags/AZ.png b/res/img/flags/AZ.png new file mode 100644 index 0000000000..2bf3c746e7 Binary files /dev/null and b/res/img/flags/AZ.png differ diff --git a/res/img/flags/BA.png b/res/img/flags/BA.png new file mode 100644 index 0000000000..3e3ec3fc76 Binary files /dev/null and b/res/img/flags/BA.png differ diff --git a/res/img/flags/BB.png b/res/img/flags/BB.png new file mode 100644 index 0000000000..694050ca46 Binary files /dev/null and b/res/img/flags/BB.png differ diff --git a/res/img/flags/BD.png b/res/img/flags/BD.png new file mode 100644 index 0000000000..6de2cde85b Binary files /dev/null and b/res/img/flags/BD.png differ diff --git a/res/img/flags/BE.png b/res/img/flags/BE.png new file mode 100644 index 0000000000..742ba9231f Binary files /dev/null and b/res/img/flags/BE.png differ diff --git a/res/img/flags/BF.png b/res/img/flags/BF.png new file mode 100644 index 0000000000..17f9f67d26 Binary files /dev/null and b/res/img/flags/BF.png differ diff --git a/res/img/flags/BG.png b/res/img/flags/BG.png new file mode 100644 index 0000000000..b01d3ff57b Binary files /dev/null and b/res/img/flags/BG.png differ diff --git a/res/img/flags/BH.png b/res/img/flags/BH.png new file mode 100644 index 0000000000..d0f82e8285 Binary files /dev/null and b/res/img/flags/BH.png differ diff --git a/res/img/flags/BI.png b/res/img/flags/BI.png new file mode 100644 index 0000000000..21865ac720 Binary files /dev/null and b/res/img/flags/BI.png differ diff --git a/res/img/flags/BJ.png b/res/img/flags/BJ.png new file mode 100644 index 0000000000..a7c6091434 Binary files /dev/null and b/res/img/flags/BJ.png differ diff --git a/res/img/flags/BL.png b/res/img/flags/BL.png new file mode 100644 index 0000000000..6d50a0f544 Binary files /dev/null and b/res/img/flags/BL.png differ diff --git a/res/img/flags/BM.png b/res/img/flags/BM.png new file mode 100644 index 0000000000..310a25ea23 Binary files /dev/null and b/res/img/flags/BM.png differ diff --git a/res/img/flags/BN.png b/res/img/flags/BN.png new file mode 100644 index 0000000000..bc4da8d9a6 Binary files /dev/null and b/res/img/flags/BN.png differ diff --git a/res/img/flags/BO.png b/res/img/flags/BO.png new file mode 100644 index 0000000000..144b8d32db Binary files /dev/null and b/res/img/flags/BO.png differ diff --git a/res/img/flags/BQ.png b/res/img/flags/BQ.png new file mode 100644 index 0000000000..0897943760 Binary files /dev/null and b/res/img/flags/BQ.png differ diff --git a/res/img/flags/BR.png b/res/img/flags/BR.png new file mode 100644 index 0000000000..0278492592 Binary files /dev/null and b/res/img/flags/BR.png differ diff --git a/res/img/flags/BS.png b/res/img/flags/BS.png new file mode 100644 index 0000000000..2b05a8fc7c Binary files /dev/null and b/res/img/flags/BS.png differ diff --git a/res/img/flags/BT.png b/res/img/flags/BT.png new file mode 100644 index 0000000000..1f031df071 Binary files /dev/null and b/res/img/flags/BT.png differ diff --git a/res/img/flags/BV.png b/res/img/flags/BV.png new file mode 100644 index 0000000000..aafb0f1776 Binary files /dev/null and b/res/img/flags/BV.png differ diff --git a/res/img/flags/BW.png b/res/img/flags/BW.png new file mode 100644 index 0000000000..3084016718 Binary files /dev/null and b/res/img/flags/BW.png differ diff --git a/res/img/flags/BY.png b/res/img/flags/BY.png new file mode 100644 index 0000000000..ce9de9c9c7 Binary files /dev/null and b/res/img/flags/BY.png differ diff --git a/res/img/flags/BZ.png b/res/img/flags/BZ.png new file mode 100644 index 0000000000..33620c3f31 Binary files /dev/null and b/res/img/flags/BZ.png differ diff --git a/res/img/flags/CA.png b/res/img/flags/CA.png new file mode 100644 index 0000000000..4bbf8b1169 Binary files /dev/null and b/res/img/flags/CA.png differ diff --git a/res/img/flags/CC.png b/res/img/flags/CC.png new file mode 100644 index 0000000000..fd40fc8a78 Binary files /dev/null and b/res/img/flags/CC.png differ diff --git a/res/img/flags/CD.png b/res/img/flags/CD.png new file mode 100644 index 0000000000..230aacd454 Binary files /dev/null and b/res/img/flags/CD.png differ diff --git a/res/img/flags/CF.png b/res/img/flags/CF.png new file mode 100644 index 0000000000..c58ed4f7b2 Binary files /dev/null and b/res/img/flags/CF.png differ diff --git a/res/img/flags/CG.png b/res/img/flags/CG.png new file mode 100644 index 0000000000..6c2441e3e0 Binary files /dev/null and b/res/img/flags/CG.png differ diff --git a/res/img/flags/CH.png b/res/img/flags/CH.png new file mode 100644 index 0000000000..9fd87167df Binary files /dev/null and b/res/img/flags/CH.png differ diff --git a/res/img/flags/CI.png b/res/img/flags/CI.png new file mode 100644 index 0000000000..9741b9b11f Binary files /dev/null and b/res/img/flags/CI.png differ diff --git a/res/img/flags/CK.png b/res/img/flags/CK.png new file mode 100644 index 0000000000..6cca35967c Binary files /dev/null and b/res/img/flags/CK.png differ diff --git a/res/img/flags/CL.png b/res/img/flags/CL.png new file mode 100644 index 0000000000..13b993d15d Binary files /dev/null and b/res/img/flags/CL.png differ diff --git a/res/img/flags/CM.png b/res/img/flags/CM.png new file mode 100644 index 0000000000..bca5730fb5 Binary files /dev/null and b/res/img/flags/CM.png differ diff --git a/res/img/flags/CN.png b/res/img/flags/CN.png new file mode 100644 index 0000000000..e086855c73 Binary files /dev/null and b/res/img/flags/CN.png differ diff --git a/res/img/flags/CO.png b/res/img/flags/CO.png new file mode 100644 index 0000000000..65c0aba447 Binary files /dev/null and b/res/img/flags/CO.png differ diff --git a/res/img/flags/CR.png b/res/img/flags/CR.png new file mode 100644 index 0000000000..b351c67a53 Binary files /dev/null and b/res/img/flags/CR.png differ diff --git a/res/img/flags/CU.png b/res/img/flags/CU.png new file mode 100644 index 0000000000..e7a25c60b3 Binary files /dev/null and b/res/img/flags/CU.png differ diff --git a/res/img/flags/CV.png b/res/img/flags/CV.png new file mode 100644 index 0000000000..f249bbaa46 Binary files /dev/null and b/res/img/flags/CV.png differ diff --git a/res/img/flags/CW.png b/res/img/flags/CW.png new file mode 100644 index 0000000000..e02cacd3dd Binary files /dev/null and b/res/img/flags/CW.png differ diff --git a/res/img/flags/CX.png b/res/img/flags/CX.png new file mode 100644 index 0000000000..3ea21422f0 Binary files /dev/null and b/res/img/flags/CX.png differ diff --git a/res/img/flags/CY.png b/res/img/flags/CY.png new file mode 100644 index 0000000000..3182f48bd2 Binary files /dev/null and b/res/img/flags/CY.png differ diff --git a/res/img/flags/CZ.png b/res/img/flags/CZ.png new file mode 100644 index 0000000000..5462334638 Binary files /dev/null and b/res/img/flags/CZ.png differ diff --git a/res/img/flags/DE.png b/res/img/flags/DE.png new file mode 100644 index 0000000000..93e269166b Binary files /dev/null and b/res/img/flags/DE.png differ diff --git a/res/img/flags/DJ.png b/res/img/flags/DJ.png new file mode 100644 index 0000000000..243bb7390d Binary files /dev/null and b/res/img/flags/DJ.png differ diff --git a/res/img/flags/DK.png b/res/img/flags/DK.png new file mode 100644 index 0000000000..fc74cc396c Binary files /dev/null and b/res/img/flags/DK.png differ diff --git a/res/img/flags/DM.png b/res/img/flags/DM.png new file mode 100644 index 0000000000..c3a0e9d102 Binary files /dev/null and b/res/img/flags/DM.png differ diff --git a/res/img/flags/DO.png b/res/img/flags/DO.png new file mode 100644 index 0000000000..5c4a004fef Binary files /dev/null and b/res/img/flags/DO.png differ diff --git a/res/img/flags/DZ.png b/res/img/flags/DZ.png new file mode 100644 index 0000000000..1589d0cc40 Binary files /dev/null and b/res/img/flags/DZ.png differ diff --git a/res/img/flags/EC.png b/res/img/flags/EC.png new file mode 100644 index 0000000000..4c53dead1c Binary files /dev/null and b/res/img/flags/EC.png differ diff --git a/res/img/flags/EE.png b/res/img/flags/EE.png new file mode 100644 index 0000000000..3668de7919 Binary files /dev/null and b/res/img/flags/EE.png differ diff --git a/res/img/flags/EG.png b/res/img/flags/EG.png new file mode 100644 index 0000000000..66ec709df7 Binary files /dev/null and b/res/img/flags/EG.png differ diff --git a/res/img/flags/EH.png b/res/img/flags/EH.png new file mode 100644 index 0000000000..148be93c08 Binary files /dev/null and b/res/img/flags/EH.png differ diff --git a/res/img/flags/ER.png b/res/img/flags/ER.png new file mode 100644 index 0000000000..7cb8441514 Binary files /dev/null and b/res/img/flags/ER.png differ diff --git a/res/img/flags/ES.png b/res/img/flags/ES.png new file mode 100644 index 0000000000..aae73b6fcb Binary files /dev/null and b/res/img/flags/ES.png differ diff --git a/res/img/flags/ET.png b/res/img/flags/ET.png new file mode 100644 index 0000000000..7b420f02f4 Binary files /dev/null and b/res/img/flags/ET.png differ diff --git a/res/img/flags/FI.png b/res/img/flags/FI.png new file mode 100644 index 0000000000..42f64bf360 Binary files /dev/null and b/res/img/flags/FI.png differ diff --git a/res/img/flags/FJ.png b/res/img/flags/FJ.png new file mode 100644 index 0000000000..cecc683c9c Binary files /dev/null and b/res/img/flags/FJ.png differ diff --git a/res/img/flags/FK.png b/res/img/flags/FK.png new file mode 100644 index 0000000000..6074fea09c Binary files /dev/null and b/res/img/flags/FK.png differ diff --git a/res/img/flags/FM.png b/res/img/flags/FM.png new file mode 100644 index 0000000000..45fdb66426 Binary files /dev/null and b/res/img/flags/FM.png differ diff --git a/res/img/flags/FO.png b/res/img/flags/FO.png new file mode 100644 index 0000000000..d8fd75c638 Binary files /dev/null and b/res/img/flags/FO.png differ diff --git a/res/img/flags/FR.png b/res/img/flags/FR.png new file mode 100644 index 0000000000..6d50a0f544 Binary files /dev/null and b/res/img/flags/FR.png differ diff --git a/res/img/flags/GA.png b/res/img/flags/GA.png new file mode 100644 index 0000000000..3808a61f1d Binary files /dev/null and b/res/img/flags/GA.png differ diff --git a/res/img/flags/GB.png b/res/img/flags/GB.png new file mode 100644 index 0000000000..589be70063 Binary files /dev/null and b/res/img/flags/GB.png differ diff --git a/res/img/flags/GD.png b/res/img/flags/GD.png new file mode 100644 index 0000000000..babe1e4cc6 Binary files /dev/null and b/res/img/flags/GD.png differ diff --git a/res/img/flags/GE.png b/res/img/flags/GE.png new file mode 100644 index 0000000000..d34cddeca9 Binary files /dev/null and b/res/img/flags/GE.png differ diff --git a/res/img/flags/GF.png b/res/img/flags/GF.png new file mode 100644 index 0000000000..98828a5906 Binary files /dev/null and b/res/img/flags/GF.png differ diff --git a/res/img/flags/GG.png b/res/img/flags/GG.png new file mode 100644 index 0000000000..aec8969b28 Binary files /dev/null and b/res/img/flags/GG.png differ diff --git a/res/img/flags/GH.png b/res/img/flags/GH.png new file mode 100644 index 0000000000..70b1a623de Binary files /dev/null and b/res/img/flags/GH.png differ diff --git a/res/img/flags/GI.png b/res/img/flags/GI.png new file mode 100644 index 0000000000..9aa58327e3 Binary files /dev/null and b/res/img/flags/GI.png differ diff --git a/res/img/flags/GL.png b/res/img/flags/GL.png new file mode 100644 index 0000000000..cf1645c2b5 Binary files /dev/null and b/res/img/flags/GL.png differ diff --git a/res/img/flags/GM.png b/res/img/flags/GM.png new file mode 100644 index 0000000000..ec374fb3c3 Binary files /dev/null and b/res/img/flags/GM.png differ diff --git a/res/img/flags/GN.png b/res/img/flags/GN.png new file mode 100644 index 0000000000..46874b4d98 Binary files /dev/null and b/res/img/flags/GN.png differ diff --git a/res/img/flags/GP.png b/res/img/flags/GP.png new file mode 100644 index 0000000000..81b7abdf0e Binary files /dev/null and b/res/img/flags/GP.png differ diff --git a/res/img/flags/GQ.png b/res/img/flags/GQ.png new file mode 100644 index 0000000000..7fd1015e8b Binary files /dev/null and b/res/img/flags/GQ.png differ diff --git a/res/img/flags/GR.png b/res/img/flags/GR.png new file mode 100644 index 0000000000..101de51eab Binary files /dev/null and b/res/img/flags/GR.png differ diff --git a/res/img/flags/GS.png b/res/img/flags/GS.png new file mode 100644 index 0000000000..772c2cbe6d Binary files /dev/null and b/res/img/flags/GS.png differ diff --git a/res/img/flags/GT.png b/res/img/flags/GT.png new file mode 100644 index 0000000000..d5bd8c1e46 Binary files /dev/null and b/res/img/flags/GT.png differ diff --git a/res/img/flags/GU.png b/res/img/flags/GU.png new file mode 100644 index 0000000000..8923085d5a Binary files /dev/null and b/res/img/flags/GU.png differ diff --git a/res/img/flags/GW.png b/res/img/flags/GW.png new file mode 100644 index 0000000000..20c268ce06 Binary files /dev/null and b/res/img/flags/GW.png differ diff --git a/res/img/flags/GY.png b/res/img/flags/GY.png new file mode 100644 index 0000000000..86f56635ef Binary files /dev/null and b/res/img/flags/GY.png differ diff --git a/res/img/flags/HK.png b/res/img/flags/HK.png new file mode 100644 index 0000000000..907dc59624 Binary files /dev/null and b/res/img/flags/HK.png differ diff --git a/res/img/flags/HM.png b/res/img/flags/HM.png new file mode 100644 index 0000000000..8d1e143791 Binary files /dev/null and b/res/img/flags/HM.png differ diff --git a/res/img/flags/HN.png b/res/img/flags/HN.png new file mode 100644 index 0000000000..4cf8c3112c Binary files /dev/null and b/res/img/flags/HN.png differ diff --git a/res/img/flags/HR.png b/res/img/flags/HR.png new file mode 100644 index 0000000000..413ceb1586 Binary files /dev/null and b/res/img/flags/HR.png differ diff --git a/res/img/flags/HT.png b/res/img/flags/HT.png new file mode 100644 index 0000000000..097abeb434 Binary files /dev/null and b/res/img/flags/HT.png differ diff --git a/res/img/flags/HU.png b/res/img/flags/HU.png new file mode 100644 index 0000000000..23499bf63c Binary files /dev/null and b/res/img/flags/HU.png differ diff --git a/res/img/flags/ID.png b/res/img/flags/ID.png new file mode 100644 index 0000000000..80200657c6 Binary files /dev/null and b/res/img/flags/ID.png differ diff --git a/res/img/flags/IE.png b/res/img/flags/IE.png new file mode 100644 index 0000000000..63f2220118 Binary files /dev/null and b/res/img/flags/IE.png differ diff --git a/res/img/flags/IL.png b/res/img/flags/IL.png new file mode 100644 index 0000000000..0268826321 Binary files /dev/null and b/res/img/flags/IL.png differ diff --git a/res/img/flags/IM.png b/res/img/flags/IM.png new file mode 100644 index 0000000000..c777acc490 Binary files /dev/null and b/res/img/flags/IM.png differ diff --git a/res/img/flags/IN.png b/res/img/flags/IN.png new file mode 100644 index 0000000000..85fa9bfe72 Binary files /dev/null and b/res/img/flags/IN.png differ diff --git a/res/img/flags/IO.png b/res/img/flags/IO.png new file mode 100644 index 0000000000..1675d8e7db Binary files /dev/null and b/res/img/flags/IO.png differ diff --git a/res/img/flags/IQ.png b/res/img/flags/IQ.png new file mode 100644 index 0000000000..f2c21f7260 Binary files /dev/null and b/res/img/flags/IQ.png differ diff --git a/res/img/flags/IR.png b/res/img/flags/IR.png new file mode 100644 index 0000000000..0b8e67506c Binary files /dev/null and b/res/img/flags/IR.png differ diff --git a/res/img/flags/IS.png b/res/img/flags/IS.png new file mode 100644 index 0000000000..5ee3e63c5c Binary files /dev/null and b/res/img/flags/IS.png differ diff --git a/res/img/flags/IT.png b/res/img/flags/IT.png new file mode 100644 index 0000000000..53b967be99 Binary files /dev/null and b/res/img/flags/IT.png differ diff --git a/res/img/flags/JE.png b/res/img/flags/JE.png new file mode 100644 index 0000000000..a1437aba78 Binary files /dev/null and b/res/img/flags/JE.png differ diff --git a/res/img/flags/JM.png b/res/img/flags/JM.png new file mode 100644 index 0000000000..0d462fa3ae Binary files /dev/null and b/res/img/flags/JM.png differ diff --git a/res/img/flags/JO.png b/res/img/flags/JO.png new file mode 100644 index 0000000000..8934db7eca Binary files /dev/null and b/res/img/flags/JO.png differ diff --git a/res/img/flags/JP.png b/res/img/flags/JP.png new file mode 100644 index 0000000000..6f92d52365 Binary files /dev/null and b/res/img/flags/JP.png differ diff --git a/res/img/flags/KE.png b/res/img/flags/KE.png new file mode 100644 index 0000000000..866b3f15dc Binary files /dev/null and b/res/img/flags/KE.png differ diff --git a/res/img/flags/KG.png b/res/img/flags/KG.png new file mode 100644 index 0000000000..56b433c756 Binary files /dev/null and b/res/img/flags/KG.png differ diff --git a/res/img/flags/KH.png b/res/img/flags/KH.png new file mode 100644 index 0000000000..e1ddd5f84c Binary files /dev/null and b/res/img/flags/KH.png differ diff --git a/res/img/flags/KI.png b/res/img/flags/KI.png new file mode 100644 index 0000000000..8b7c54bc0f Binary files /dev/null and b/res/img/flags/KI.png differ diff --git a/res/img/flags/KM.png b/res/img/flags/KM.png new file mode 100644 index 0000000000..227a3b3396 Binary files /dev/null and b/res/img/flags/KM.png differ diff --git a/res/img/flags/KN.png b/res/img/flags/KN.png new file mode 100644 index 0000000000..bc6189bed1 Binary files /dev/null and b/res/img/flags/KN.png differ diff --git a/res/img/flags/KP.png b/res/img/flags/KP.png new file mode 100644 index 0000000000..c92248b910 Binary files /dev/null and b/res/img/flags/KP.png differ diff --git a/res/img/flags/KR.png b/res/img/flags/KR.png new file mode 100644 index 0000000000..ab1cb94943 Binary files /dev/null and b/res/img/flags/KR.png differ diff --git a/res/img/flags/KW.png b/res/img/flags/KW.png new file mode 100644 index 0000000000..0b41c7a532 Binary files /dev/null and b/res/img/flags/KW.png differ diff --git a/res/img/flags/KY.png b/res/img/flags/KY.png new file mode 100644 index 0000000000..7af5290d31 Binary files /dev/null and b/res/img/flags/KY.png differ diff --git a/res/img/flags/KZ.png b/res/img/flags/KZ.png new file mode 100644 index 0000000000..e10a1255a0 Binary files /dev/null and b/res/img/flags/KZ.png differ diff --git a/res/img/flags/LA.png b/res/img/flags/LA.png new file mode 100644 index 0000000000..6ad67d4255 Binary files /dev/null and b/res/img/flags/LA.png differ diff --git a/res/img/flags/LB.png b/res/img/flags/LB.png new file mode 100644 index 0000000000..865df57a42 Binary files /dev/null and b/res/img/flags/LB.png differ diff --git a/res/img/flags/LC.png b/res/img/flags/LC.png new file mode 100644 index 0000000000..e83a2d08bc Binary files /dev/null and b/res/img/flags/LC.png differ diff --git a/res/img/flags/LI.png b/res/img/flags/LI.png new file mode 100644 index 0000000000..57034d367c Binary files /dev/null and b/res/img/flags/LI.png differ diff --git a/res/img/flags/LK.png b/res/img/flags/LK.png new file mode 100644 index 0000000000..6e7ad58254 Binary files /dev/null and b/res/img/flags/LK.png differ diff --git a/res/img/flags/LR.png b/res/img/flags/LR.png new file mode 100644 index 0000000000..46c3b84a92 Binary files /dev/null and b/res/img/flags/LR.png differ diff --git a/res/img/flags/LS.png b/res/img/flags/LS.png new file mode 100644 index 0000000000..79b505d490 Binary files /dev/null and b/res/img/flags/LS.png differ diff --git a/res/img/flags/LT.png b/res/img/flags/LT.png new file mode 100644 index 0000000000..7740cdc0a0 Binary files /dev/null and b/res/img/flags/LT.png differ diff --git a/res/img/flags/LU.png b/res/img/flags/LU.png new file mode 100644 index 0000000000..8f383e674e Binary files /dev/null and b/res/img/flags/LU.png differ diff --git a/res/img/flags/LV.png b/res/img/flags/LV.png new file mode 100644 index 0000000000..a0f36d89c4 Binary files /dev/null and b/res/img/flags/LV.png differ diff --git a/res/img/flags/LY.png b/res/img/flags/LY.png new file mode 100644 index 0000000000..2884c4c0a9 Binary files /dev/null and b/res/img/flags/LY.png differ diff --git a/res/img/flags/MA.png b/res/img/flags/MA.png new file mode 100644 index 0000000000..1f76cfc9bd Binary files /dev/null and b/res/img/flags/MA.png differ diff --git a/res/img/flags/MC.png b/res/img/flags/MC.png new file mode 100644 index 0000000000..06fc2ad166 Binary files /dev/null and b/res/img/flags/MC.png differ diff --git a/res/img/flags/MD.png b/res/img/flags/MD.png new file mode 100644 index 0000000000..8e54c2b815 Binary files /dev/null and b/res/img/flags/MD.png differ diff --git a/res/img/flags/ME.png b/res/img/flags/ME.png new file mode 100644 index 0000000000..97424d4ec2 Binary files /dev/null and b/res/img/flags/ME.png differ diff --git a/res/img/flags/MF.png b/res/img/flags/MF.png new file mode 100644 index 0000000000..6d50a0f544 Binary files /dev/null and b/res/img/flags/MF.png differ diff --git a/res/img/flags/MG.png b/res/img/flags/MG.png new file mode 100644 index 0000000000..28bfccc9e8 Binary files /dev/null and b/res/img/flags/MG.png differ diff --git a/res/img/flags/MH.png b/res/img/flags/MH.png new file mode 100644 index 0000000000..e482a65924 Binary files /dev/null and b/res/img/flags/MH.png differ diff --git a/res/img/flags/MK.png b/res/img/flags/MK.png new file mode 100644 index 0000000000..84e2e65e76 Binary files /dev/null and b/res/img/flags/MK.png differ diff --git a/res/img/flags/ML.png b/res/img/flags/ML.png new file mode 100644 index 0000000000..38fec34796 Binary files /dev/null and b/res/img/flags/ML.png differ diff --git a/res/img/flags/MM.png b/res/img/flags/MM.png new file mode 100644 index 0000000000..70a03c6b14 Binary files /dev/null and b/res/img/flags/MM.png differ diff --git a/res/img/flags/MN.png b/res/img/flags/MN.png new file mode 100644 index 0000000000..1e1bbe6089 Binary files /dev/null and b/res/img/flags/MN.png differ diff --git a/res/img/flags/MO.png b/res/img/flags/MO.png new file mode 100644 index 0000000000..3833d683e7 Binary files /dev/null and b/res/img/flags/MO.png differ diff --git a/res/img/flags/MP.png b/res/img/flags/MP.png new file mode 100644 index 0000000000..63119096b0 Binary files /dev/null and b/res/img/flags/MP.png differ diff --git a/res/img/flags/MQ.png b/res/img/flags/MQ.png new file mode 100644 index 0000000000..9cab441aec Binary files /dev/null and b/res/img/flags/MQ.png differ diff --git a/res/img/flags/MR.png b/res/img/flags/MR.png new file mode 100644 index 0000000000..c144de17f7 Binary files /dev/null and b/res/img/flags/MR.png differ diff --git a/res/img/flags/MS.png b/res/img/flags/MS.png new file mode 100644 index 0000000000..1221707042 Binary files /dev/null and b/res/img/flags/MS.png differ diff --git a/res/img/flags/MT.png b/res/img/flags/MT.png new file mode 100644 index 0000000000..7963aa618a Binary files /dev/null and b/res/img/flags/MT.png differ diff --git a/res/img/flags/MU.png b/res/img/flags/MU.png new file mode 100644 index 0000000000..d5d4d4008d Binary files /dev/null and b/res/img/flags/MU.png differ diff --git a/res/img/flags/MV.png b/res/img/flags/MV.png new file mode 100644 index 0000000000..0f2ecb4389 Binary files /dev/null and b/res/img/flags/MV.png differ diff --git a/res/img/flags/MW.png b/res/img/flags/MW.png new file mode 100644 index 0000000000..d0a5d24f55 Binary files /dev/null and b/res/img/flags/MW.png differ diff --git a/res/img/flags/MX.png b/res/img/flags/MX.png new file mode 100644 index 0000000000..096cb1111f Binary files /dev/null and b/res/img/flags/MX.png differ diff --git a/res/img/flags/MY.png b/res/img/flags/MY.png new file mode 100644 index 0000000000..17f18ac519 Binary files /dev/null and b/res/img/flags/MY.png differ diff --git a/res/img/flags/MZ.png b/res/img/flags/MZ.png new file mode 100644 index 0000000000..66be6563c6 Binary files /dev/null and b/res/img/flags/MZ.png differ diff --git a/res/img/flags/NA.png b/res/img/flags/NA.png new file mode 100644 index 0000000000..7ecfd317c7 Binary files /dev/null and b/res/img/flags/NA.png differ diff --git a/res/img/flags/NC.png b/res/img/flags/NC.png new file mode 100644 index 0000000000..11126ade77 Binary files /dev/null and b/res/img/flags/NC.png differ diff --git a/res/img/flags/NE.png b/res/img/flags/NE.png new file mode 100644 index 0000000000..d584fa8429 Binary files /dev/null and b/res/img/flags/NE.png differ diff --git a/res/img/flags/NF.png b/res/img/flags/NF.png new file mode 100644 index 0000000000..c054042591 Binary files /dev/null and b/res/img/flags/NF.png differ diff --git a/res/img/flags/NG.png b/res/img/flags/NG.png new file mode 100644 index 0000000000..73aee15b3f Binary files /dev/null and b/res/img/flags/NG.png differ diff --git a/res/img/flags/NI.png b/res/img/flags/NI.png new file mode 100644 index 0000000000..fd044933e4 Binary files /dev/null and b/res/img/flags/NI.png differ diff --git a/res/img/flags/NL.png b/res/img/flags/NL.png new file mode 100644 index 0000000000..0897943760 Binary files /dev/null and b/res/img/flags/NL.png differ diff --git a/res/img/flags/NO.png b/res/img/flags/NO.png new file mode 100644 index 0000000000..aafb0f1776 Binary files /dev/null and b/res/img/flags/NO.png differ diff --git a/res/img/flags/NP.png b/res/img/flags/NP.png new file mode 100644 index 0000000000..744458e17e Binary files /dev/null and b/res/img/flags/NP.png differ diff --git a/res/img/flags/NR.png b/res/img/flags/NR.png new file mode 100644 index 0000000000..58c2afb228 Binary files /dev/null and b/res/img/flags/NR.png differ diff --git a/res/img/flags/NU.png b/res/img/flags/NU.png new file mode 100644 index 0000000000..007c99eca5 Binary files /dev/null and b/res/img/flags/NU.png differ diff --git a/res/img/flags/NZ.png b/res/img/flags/NZ.png new file mode 100644 index 0000000000..839368dd7b Binary files /dev/null and b/res/img/flags/NZ.png differ diff --git a/res/img/flags/OM.png b/res/img/flags/OM.png new file mode 100644 index 0000000000..63a893367f Binary files /dev/null and b/res/img/flags/OM.png differ diff --git a/res/img/flags/PA.png b/res/img/flags/PA.png new file mode 100644 index 0000000000..3515d95d37 Binary files /dev/null and b/res/img/flags/PA.png differ diff --git a/res/img/flags/PE.png b/res/img/flags/PE.png new file mode 100644 index 0000000000..58f70b8d18 Binary files /dev/null and b/res/img/flags/PE.png differ diff --git a/res/img/flags/PF.png b/res/img/flags/PF.png new file mode 100644 index 0000000000..2f33f2574f Binary files /dev/null and b/res/img/flags/PF.png differ diff --git a/res/img/flags/PG.png b/res/img/flags/PG.png new file mode 100644 index 0000000000..c796f587c6 Binary files /dev/null and b/res/img/flags/PG.png differ diff --git a/res/img/flags/PH.png b/res/img/flags/PH.png new file mode 100644 index 0000000000..0d98de0386 Binary files /dev/null and b/res/img/flags/PH.png differ diff --git a/res/img/flags/PK.png b/res/img/flags/PK.png new file mode 100644 index 0000000000..87f4e2f492 Binary files /dev/null and b/res/img/flags/PK.png differ diff --git a/res/img/flags/PL.png b/res/img/flags/PL.png new file mode 100644 index 0000000000..273869dfc6 Binary files /dev/null and b/res/img/flags/PL.png differ diff --git a/res/img/flags/PM.png b/res/img/flags/PM.png new file mode 100644 index 0000000000..b74c396d92 Binary files /dev/null and b/res/img/flags/PM.png differ diff --git a/res/img/flags/PN.png b/res/img/flags/PN.png new file mode 100644 index 0000000000..e34c62d598 Binary files /dev/null and b/res/img/flags/PN.png differ diff --git a/res/img/flags/PR.png b/res/img/flags/PR.png new file mode 100644 index 0000000000..8efdb91252 Binary files /dev/null and b/res/img/flags/PR.png differ diff --git a/res/img/flags/PS.png b/res/img/flags/PS.png new file mode 100644 index 0000000000..7a0cceec00 Binary files /dev/null and b/res/img/flags/PS.png differ diff --git a/res/img/flags/PT.png b/res/img/flags/PT.png new file mode 100644 index 0000000000..49e290827c Binary files /dev/null and b/res/img/flags/PT.png differ diff --git a/res/img/flags/PW.png b/res/img/flags/PW.png new file mode 100644 index 0000000000..6cb2e1e70d Binary files /dev/null and b/res/img/flags/PW.png differ diff --git a/res/img/flags/PY.png b/res/img/flags/PY.png new file mode 100644 index 0000000000..a61c42c423 Binary files /dev/null and b/res/img/flags/PY.png differ diff --git a/res/img/flags/QA.png b/res/img/flags/QA.png new file mode 100644 index 0000000000..bb091cc88c Binary files /dev/null and b/res/img/flags/QA.png differ diff --git a/res/img/flags/RE.png b/res/img/flags/RE.png new file mode 100644 index 0000000000..6d50a0f544 Binary files /dev/null and b/res/img/flags/RE.png differ diff --git a/res/img/flags/RO.png b/res/img/flags/RO.png new file mode 100644 index 0000000000..4495d29eb0 Binary files /dev/null and b/res/img/flags/RO.png differ diff --git a/res/img/flags/RS.png b/res/img/flags/RS.png new file mode 100644 index 0000000000..ebb0f28a7b Binary files /dev/null and b/res/img/flags/RS.png differ diff --git a/res/img/flags/RU.png b/res/img/flags/RU.png new file mode 100644 index 0000000000..64532ffa58 Binary files /dev/null and b/res/img/flags/RU.png differ diff --git a/res/img/flags/RW.png b/res/img/flags/RW.png new file mode 100644 index 0000000000..64b3cfff04 Binary files /dev/null and b/res/img/flags/RW.png differ diff --git a/res/img/flags/SA.png b/res/img/flags/SA.png new file mode 100644 index 0000000000..250de6f6f5 Binary files /dev/null and b/res/img/flags/SA.png differ diff --git a/res/img/flags/SB.png b/res/img/flags/SB.png new file mode 100644 index 0000000000..5833c130eb Binary files /dev/null and b/res/img/flags/SB.png differ diff --git a/res/img/flags/SC.png b/res/img/flags/SC.png new file mode 100644 index 0000000000..ce5248f434 Binary files /dev/null and b/res/img/flags/SC.png differ diff --git a/res/img/flags/SD.png b/res/img/flags/SD.png new file mode 100644 index 0000000000..d8711a83d6 Binary files /dev/null and b/res/img/flags/SD.png differ diff --git a/res/img/flags/SE.png b/res/img/flags/SE.png new file mode 100644 index 0000000000..81880931f3 Binary files /dev/null and b/res/img/flags/SE.png differ diff --git a/res/img/flags/SG.png b/res/img/flags/SG.png new file mode 100644 index 0000000000..6f00e57923 Binary files /dev/null and b/res/img/flags/SG.png differ diff --git a/res/img/flags/SH.png b/res/img/flags/SH.png new file mode 100644 index 0000000000..055dde68bc Binary files /dev/null and b/res/img/flags/SH.png differ diff --git a/res/img/flags/SI.png b/res/img/flags/SI.png new file mode 100644 index 0000000000..9635983406 Binary files /dev/null and b/res/img/flags/SI.png differ diff --git a/res/img/flags/SJ.png b/res/img/flags/SJ.png new file mode 100644 index 0000000000..aafb0f1776 Binary files /dev/null and b/res/img/flags/SJ.png differ diff --git a/res/img/flags/SK.png b/res/img/flags/SK.png new file mode 100644 index 0000000000..84c7021f0a Binary files /dev/null and b/res/img/flags/SK.png differ diff --git a/res/img/flags/SL.png b/res/img/flags/SL.png new file mode 100644 index 0000000000..c5ed199141 Binary files /dev/null and b/res/img/flags/SL.png differ diff --git a/res/img/flags/SM.png b/res/img/flags/SM.png new file mode 100644 index 0000000000..1af1ca284f Binary files /dev/null and b/res/img/flags/SM.png differ diff --git a/res/img/flags/SN.png b/res/img/flags/SN.png new file mode 100644 index 0000000000..d0b1843561 Binary files /dev/null and b/res/img/flags/SN.png differ diff --git a/res/img/flags/SO.png b/res/img/flags/SO.png new file mode 100644 index 0000000000..64e2970b9d Binary files /dev/null and b/res/img/flags/SO.png differ diff --git a/res/img/flags/SR.png b/res/img/flags/SR.png new file mode 100644 index 0000000000..b072dda835 Binary files /dev/null and b/res/img/flags/SR.png differ diff --git a/res/img/flags/SS.png b/res/img/flags/SS.png new file mode 100644 index 0000000000..83933d4521 Binary files /dev/null and b/res/img/flags/SS.png differ diff --git a/res/img/flags/ST.png b/res/img/flags/ST.png new file mode 100644 index 0000000000..c102721a86 Binary files /dev/null and b/res/img/flags/ST.png differ diff --git a/res/img/flags/SV.png b/res/img/flags/SV.png new file mode 100644 index 0000000000..80de92e556 Binary files /dev/null and b/res/img/flags/SV.png differ diff --git a/res/img/flags/SX.png b/res/img/flags/SX.png new file mode 100644 index 0000000000..dd52215c5d Binary files /dev/null and b/res/img/flags/SX.png differ diff --git a/res/img/flags/SY.png b/res/img/flags/SY.png new file mode 100644 index 0000000000..78f45b7c0b Binary files /dev/null and b/res/img/flags/SY.png differ diff --git a/res/img/flags/SZ.png b/res/img/flags/SZ.png new file mode 100644 index 0000000000..2182f4ff93 Binary files /dev/null and b/res/img/flags/SZ.png differ diff --git a/res/img/flags/TC.png b/res/img/flags/TC.png new file mode 100644 index 0000000000..3e3e19d4b3 Binary files /dev/null and b/res/img/flags/TC.png differ diff --git a/res/img/flags/TD.png b/res/img/flags/TD.png new file mode 100644 index 0000000000..753bec22b0 Binary files /dev/null and b/res/img/flags/TD.png differ diff --git a/res/img/flags/TF.png b/res/img/flags/TF.png new file mode 100644 index 0000000000..6d50a0f544 Binary files /dev/null and b/res/img/flags/TF.png differ diff --git a/res/img/flags/TG.png b/res/img/flags/TG.png new file mode 100644 index 0000000000..8501ada655 Binary files /dev/null and b/res/img/flags/TG.png differ diff --git a/res/img/flags/TH.png b/res/img/flags/TH.png new file mode 100644 index 0000000000..0c884c329e Binary files /dev/null and b/res/img/flags/TH.png differ diff --git a/res/img/flags/TJ.png b/res/img/flags/TJ.png new file mode 100644 index 0000000000..3c9026fa0f Binary files /dev/null and b/res/img/flags/TJ.png differ diff --git a/res/img/flags/TK.png b/res/img/flags/TK.png new file mode 100644 index 0000000000..fd605749ea Binary files /dev/null and b/res/img/flags/TK.png differ diff --git a/res/img/flags/TL.png b/res/img/flags/TL.png new file mode 100644 index 0000000000..b4c834b1d6 Binary files /dev/null and b/res/img/flags/TL.png differ diff --git a/res/img/flags/TM.png b/res/img/flags/TM.png new file mode 100644 index 0000000000..d18cb939a9 Binary files /dev/null and b/res/img/flags/TM.png differ diff --git a/res/img/flags/TN.png b/res/img/flags/TN.png new file mode 100644 index 0000000000..21c4b98be7 Binary files /dev/null and b/res/img/flags/TN.png differ diff --git a/res/img/flags/TO.png b/res/img/flags/TO.png new file mode 100644 index 0000000000..c828206e35 Binary files /dev/null and b/res/img/flags/TO.png differ diff --git a/res/img/flags/TR.png b/res/img/flags/TR.png new file mode 100644 index 0000000000..f2a5bd22c8 Binary files /dev/null and b/res/img/flags/TR.png differ diff --git a/res/img/flags/TT.png b/res/img/flags/TT.png new file mode 100644 index 0000000000..66d698334b Binary files /dev/null and b/res/img/flags/TT.png differ diff --git a/res/img/flags/TV.png b/res/img/flags/TV.png new file mode 100644 index 0000000000..7a127f51ae Binary files /dev/null and b/res/img/flags/TV.png differ diff --git a/res/img/flags/TW.png b/res/img/flags/TW.png new file mode 100644 index 0000000000..2353ba1b0a Binary files /dev/null and b/res/img/flags/TW.png differ diff --git a/res/img/flags/TZ.png b/res/img/flags/TZ.png new file mode 100644 index 0000000000..7949f65d8a Binary files /dev/null and b/res/img/flags/TZ.png differ diff --git a/res/img/flags/UA.png b/res/img/flags/UA.png new file mode 100644 index 0000000000..687e305294 Binary files /dev/null and b/res/img/flags/UA.png differ diff --git a/res/img/flags/UG.png b/res/img/flags/UG.png new file mode 100644 index 0000000000..0a21ad15c3 Binary files /dev/null and b/res/img/flags/UG.png differ diff --git a/res/img/flags/US.png b/res/img/flags/US.png new file mode 100644 index 0000000000..c3a245b767 Binary files /dev/null and b/res/img/flags/US.png differ diff --git a/res/img/flags/UY.png b/res/img/flags/UY.png new file mode 100644 index 0000000000..21a347c6fc Binary files /dev/null and b/res/img/flags/UY.png differ diff --git a/res/img/flags/UZ.png b/res/img/flags/UZ.png new file mode 100644 index 0000000000..643b6ae0cf Binary files /dev/null and b/res/img/flags/UZ.png differ diff --git a/res/img/flags/VA.png b/res/img/flags/VA.png new file mode 100644 index 0000000000..63a13c0e81 Binary files /dev/null and b/res/img/flags/VA.png differ diff --git a/res/img/flags/VC.png b/res/img/flags/VC.png new file mode 100644 index 0000000000..da991a9344 Binary files /dev/null and b/res/img/flags/VC.png differ diff --git a/res/img/flags/VE.png b/res/img/flags/VE.png new file mode 100644 index 0000000000..e75e17c9f0 Binary files /dev/null and b/res/img/flags/VE.png differ diff --git a/res/img/flags/VG.png b/res/img/flags/VG.png new file mode 100644 index 0000000000..46f93cad1e Binary files /dev/null and b/res/img/flags/VG.png differ diff --git a/res/img/flags/VI.png b/res/img/flags/VI.png new file mode 100644 index 0000000000..8c849a733e Binary files /dev/null and b/res/img/flags/VI.png differ diff --git a/res/img/flags/VN.png b/res/img/flags/VN.png new file mode 100644 index 0000000000..6ea2122f9d Binary files /dev/null and b/res/img/flags/VN.png differ diff --git a/res/img/flags/VU.png b/res/img/flags/VU.png new file mode 100644 index 0000000000..bad3ba4d46 Binary files /dev/null and b/res/img/flags/VU.png differ diff --git a/res/img/flags/WF.png b/res/img/flags/WF.png new file mode 100644 index 0000000000..d94359dcc4 Binary files /dev/null and b/res/img/flags/WF.png differ diff --git a/res/img/flags/WS.png b/res/img/flags/WS.png new file mode 100644 index 0000000000..f8b80e5ba9 Binary files /dev/null and b/res/img/flags/WS.png differ diff --git a/res/img/flags/YE.png b/res/img/flags/YE.png new file mode 100644 index 0000000000..8b9bbd8942 Binary files /dev/null and b/res/img/flags/YE.png differ diff --git a/res/img/flags/YT.png b/res/img/flags/YT.png new file mode 100644 index 0000000000..328879361e Binary files /dev/null and b/res/img/flags/YT.png differ diff --git a/res/img/flags/ZA.png b/res/img/flags/ZA.png new file mode 100644 index 0000000000..7f0a52d3b2 Binary files /dev/null and b/res/img/flags/ZA.png differ diff --git a/res/img/flags/ZM.png b/res/img/flags/ZM.png new file mode 100644 index 0000000000..87adc3afaa Binary files /dev/null and b/res/img/flags/ZM.png differ diff --git a/res/img/flags/ZW.png b/res/img/flags/ZW.png new file mode 100644 index 0000000000..742c9f7e71 Binary files /dev/null and b/res/img/flags/ZW.png differ diff --git a/res/img/fullscreen.svg b/res/img/fullscreen.svg new file mode 100644 index 0000000000..e333abb6fb --- /dev/null +++ b/res/img/fullscreen.svg @@ -0,0 +1,23 @@ + + + + Zoom + Created with Sketch. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/hangup.svg b/res/img/hangup.svg new file mode 100644 index 0000000000..be038d2b30 --- /dev/null +++ b/res/img/hangup.svg @@ -0,0 +1,15 @@ + + + + Fill 72 + Path 98 + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/res/img/hide.png b/res/img/hide.png new file mode 100644 index 0000000000..c5aaf0dd0d Binary files /dev/null and b/res/img/hide.png differ diff --git a/res/img/icon-address-delete.svg b/res/img/icon-address-delete.svg new file mode 100644 index 0000000000..1289d5aafc --- /dev/null +++ b/res/img/icon-address-delete.svg @@ -0,0 +1,15 @@ + + + + 943783E9-DBD7-4D4E-BAC9-35437C17C2C4 + Created with sketchtool. + + + + + + + + + + diff --git a/res/img/icon-call.svg b/res/img/icon-call.svg new file mode 100644 index 0000000000..0ca5c29e9d --- /dev/null +++ b/res/img/icon-call.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/res/img/icon-context-delete.svg b/res/img/icon-context-delete.svg new file mode 100644 index 0000000000..fba9fa117b --- /dev/null +++ b/res/img/icon-context-delete.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/img/icon-context-fave-on.svg b/res/img/icon-context-fave-on.svg new file mode 100644 index 0000000000..2ae172d8eb --- /dev/null +++ b/res/img/icon-context-fave-on.svg @@ -0,0 +1,15 @@ + + + + DAE17B64-40B5-478A-8E8D-97AD1A6E25C8 + Created with sketchtool. + + + + + + + + + + diff --git a/res/img/icon-context-fave.svg b/res/img/icon-context-fave.svg new file mode 100644 index 0000000000..451e1849c8 --- /dev/null +++ b/res/img/icon-context-fave.svg @@ -0,0 +1,15 @@ + + + + 8A6E1837-F0F1-432E-A0DA-6F3741F71EBF + Created with sketchtool. + + + + + + + + + + diff --git a/res/img/icon-context-low-on.svg b/res/img/icon-context-low-on.svg new file mode 100644 index 0000000000..7578c6335c --- /dev/null +++ b/res/img/icon-context-low-on.svg @@ -0,0 +1,15 @@ + + + + CD51482C-F2D4-4F63-AF9E-86513F9AF87F + Created with sketchtool. + + + + + + + + + + diff --git a/res/img/icon-context-low.svg b/res/img/icon-context-low.svg new file mode 100644 index 0000000000..663f3ca9eb --- /dev/null +++ b/res/img/icon-context-low.svg @@ -0,0 +1,15 @@ + + + + B160345F-40D3-4BE6-A860-6D04BF223EF7 + Created with sketchtool. + + + + + + + + + + diff --git a/res/img/icon-context-mute-mentions.svg b/res/img/icon-context-mute-mentions.svg new file mode 100644 index 0000000000..3693b7a82a --- /dev/null +++ b/res/img/icon-context-mute-mentions.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/icon-context-mute-off-copy.svg b/res/img/icon-context-mute-off-copy.svg new file mode 100644 index 0000000000..861f2975de --- /dev/null +++ b/res/img/icon-context-mute-off-copy.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/img/icon-context-mute-off.svg b/res/img/icon-context-mute-off.svg new file mode 100644 index 0000000000..d801823b5d --- /dev/null +++ b/res/img/icon-context-mute-off.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/res/img/icon-context-mute.svg b/res/img/icon-context-mute.svg new file mode 100644 index 0000000000..f53b868a76 --- /dev/null +++ b/res/img/icon-context-mute.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/res/img/icon-delete-pink.svg b/res/img/icon-delete-pink.svg new file mode 100644 index 0000000000..aafa87f1b2 --- /dev/null +++ b/res/img/icon-delete-pink.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/res/img/icon-email-user.svg b/res/img/icon-email-user.svg new file mode 100644 index 0000000000..2d41e06f98 --- /dev/null +++ b/res/img/icon-email-user.svg @@ -0,0 +1,17 @@ + + + + 6F488856-F8EF-479C-9747-AB6E0945C7DE + Created with sketchtool. + + + + + + + + + + + + diff --git a/res/img/icon-invite-people.svg b/res/img/icon-invite-people.svg new file mode 100644 index 0000000000..f13a03ed70 --- /dev/null +++ b/res/img/icon-invite-people.svg @@ -0,0 +1,24 @@ + + + + 9BA71BF4-DC4F-42D2-B2D0-9EAE0F7F8D45 + Created with sketchtool. + + + + + + + + + + + + + + + + + + + diff --git a/res/img/icon-mx-user.svg b/res/img/icon-mx-user.svg new file mode 100644 index 0000000000..5780277f38 --- /dev/null +++ b/res/img/icon-mx-user.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/res/img/icon-return.svg b/res/img/icon-return.svg new file mode 100644 index 0000000000..80da0f82aa --- /dev/null +++ b/res/img/icon-return.svg @@ -0,0 +1,18 @@ + + + + B542A09B-DBBF-41D4-A5FD-D05EE1E6BBC4 + Created with sketchtool. + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/icon-text-cancel.svg b/res/img/icon-text-cancel.svg new file mode 100644 index 0000000000..ce28d128aa --- /dev/null +++ b/res/img/icon-text-cancel.svg @@ -0,0 +1,15 @@ + + + + 28D80248-63BA-4A5F-9216-4CFE72784BAC + Created with sketchtool. + + + + + + + + + + \ No newline at end of file diff --git a/res/img/icon_context_delete.svg b/res/img/icon_context_delete.svg new file mode 100644 index 0000000000..896b94ad13 --- /dev/null +++ b/res/img/icon_context_delete.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/icon_context_fave.svg b/res/img/icon_context_fave.svg new file mode 100644 index 0000000000..da7b14a1f4 --- /dev/null +++ b/res/img/icon_context_fave.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/res/img/icon_context_fave_on.svg b/res/img/icon_context_fave_on.svg new file mode 100644 index 0000000000..e22e92d36e --- /dev/null +++ b/res/img/icon_context_fave_on.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/icon_context_low.svg b/res/img/icon_context_low.svg new file mode 100644 index 0000000000..ea579ef4c5 --- /dev/null +++ b/res/img/icon_context_low.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/icon_context_low_on.svg b/res/img/icon_context_low_on.svg new file mode 100644 index 0000000000..28300f9a74 --- /dev/null +++ b/res/img/icon_context_low_on.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/icon_context_message.svg b/res/img/icon_context_message.svg new file mode 100644 index 0000000000..f2ceccfa78 --- /dev/null +++ b/res/img/icon_context_message.svg @@ -0,0 +1,15 @@ + + + + ED5D3E59-2561-4AC1-9B43-82FBC51767FC + Created with sketchtool. + + + + + + + + + + diff --git a/res/img/icon_context_message_dark.svg b/res/img/icon_context_message_dark.svg new file mode 100644 index 0000000000..b4336cc377 --- /dev/null +++ b/res/img/icon_context_message_dark.svg @@ -0,0 +1,15 @@ + + + + ED5D3E59-2561-4AC1-9B43-82FBC51767FC + Created with sketchtool. + + + + + + + + + + diff --git a/res/img/icon_context_person.svg b/res/img/icon_context_person.svg new file mode 100644 index 0000000000..fff019d377 --- /dev/null +++ b/res/img/icon_context_person.svg @@ -0,0 +1,85 @@ + + + + + + image/svg+xml + + 81230A28-D944-4572-B5DB-C03CAA2B1FCA + + + + + + 81230A28-D944-4572-B5DB-C03CAA2B1FCA + Created with sketchtool. + + + + + + + + + + diff --git a/res/img/icon_context_person_on.svg b/res/img/icon_context_person_on.svg new file mode 100644 index 0000000000..362944332d --- /dev/null +++ b/res/img/icon_context_person_on.svg @@ -0,0 +1,85 @@ + + + + + + image/svg+xml + + 81230A28-D944-4572-B5DB-C03CAA2B1FCA + + + + + + 81230A28-D944-4572-B5DB-C03CAA2B1FCA + Created with sketchtool. + + + + + + + + + + diff --git a/res/img/icon_copy_message.svg b/res/img/icon_copy_message.svg new file mode 100644 index 0000000000..8d8887bb22 --- /dev/null +++ b/res/img/icon_copy_message.svg @@ -0,0 +1,86 @@ + + + + + + image/svg+xml + + ED5D3E59-2561-4AC1-9B43-82FBC51767FC + + + + + + ED5D3E59-2561-4AC1-9B43-82FBC51767FC + Created with sketchtool. + + + + + + + + + diff --git a/res/img/icon_copy_message_dark.svg b/res/img/icon_copy_message_dark.svg new file mode 100644 index 0000000000..b81e617d8c --- /dev/null +++ b/res/img/icon_copy_message_dark.svg @@ -0,0 +1,77 @@ + + + + + + image/svg+xml + + ED5D3E59-2561-4AC1-9B43-82FBC51767FC + + + + + + ED5D3E59-2561-4AC1-9B43-82FBC51767FC + Created with sketchtool. + + + + + + + diff --git a/res/img/icon_person.svg b/res/img/icon_person.svg new file mode 100644 index 0000000000..4be70df0db --- /dev/null +++ b/res/img/icon_person.svg @@ -0,0 +1,23 @@ + + + + 815EF7DE-169A-4322-AE2A-B65CBE91DCED + Created with sketchtool. + + + + + + + + + + + + + + + + + + diff --git a/res/img/icons-apps-active.svg b/res/img/icons-apps-active.svg new file mode 100644 index 0000000000..ea222d0511 --- /dev/null +++ b/res/img/icons-apps-active.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/icons-apps.svg b/res/img/icons-apps.svg new file mode 100644 index 0000000000..affd8e6408 --- /dev/null +++ b/res/img/icons-apps.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/res/img/icons-close-button.svg b/res/img/icons-close-button.svg new file mode 100644 index 0000000000..f960d73a3c --- /dev/null +++ b/res/img/icons-close-button.svg @@ -0,0 +1,15 @@ + + + + 206C270A-EB00-48E4-8CC3-5D403C59177C + Created with sketchtool. + + + + + + + + + + diff --git a/res/img/icons-close.svg b/res/img/icons-close.svg new file mode 100644 index 0000000000..453b51082f --- /dev/null +++ b/res/img/icons-close.svg @@ -0,0 +1,23 @@ + + + + +icons_create_room +Created with sketchtool. + + + + + + + + + + + + + + diff --git a/res/img/icons-create-room.svg b/res/img/icons-create-room.svg new file mode 100644 index 0000000000..252bd2df3b --- /dev/null +++ b/res/img/icons-create-room.svg @@ -0,0 +1,18 @@ + + + + 0F9BCC43-B3A7-4C9F-8E34-1F38194362C2 + Created with sketchtool. + + + + + + + + + + + + + diff --git a/res/img/icons-directory.svg b/res/img/icons-directory.svg new file mode 100644 index 0000000000..2688b84713 --- /dev/null +++ b/res/img/icons-directory.svg @@ -0,0 +1,24 @@ + + + + icons_directory + Created with Sketch. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/icons-files.svg b/res/img/icons-files.svg new file mode 100644 index 0000000000..97ba4228e3 --- /dev/null +++ b/res/img/icons-files.svg @@ -0,0 +1,29 @@ + + + + 7C98C075-AB4D-45A3-85F9-CCD46F84DA7F + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/icons-groups.svg b/res/img/icons-groups.svg new file mode 100644 index 0000000000..8f89ba83c4 --- /dev/null +++ b/res/img/icons-groups.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/icons-hide-apps.svg b/res/img/icons-hide-apps.svg new file mode 100644 index 0000000000..b622e97f71 --- /dev/null +++ b/res/img/icons-hide-apps.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/icons-hide-stickers.svg b/res/img/icons-hide-stickers.svg new file mode 100644 index 0000000000..f28e8646e6 --- /dev/null +++ b/res/img/icons-hide-stickers.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/res/img/icons-home.svg b/res/img/icons-home.svg new file mode 100644 index 0000000000..eb5484c883 --- /dev/null +++ b/res/img/icons-home.svg @@ -0,0 +1,27 @@ + + + + + + 81230A28-D944-4572-B5DB-C03CAA2B1FCA + Created with sketchtool. + + + + + + + + + + + + + + + + + diff --git a/res/img/icons-notifications.svg b/res/img/icons-notifications.svg new file mode 100644 index 0000000000..66a49d6c0c --- /dev/null +++ b/res/img/icons-notifications.svg @@ -0,0 +1,19 @@ + + + + 5E723325-BD0B-454D-BE25-638AF09A97AC + Created with sketchtool. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/icons-people.svg b/res/img/icons-people.svg new file mode 100644 index 0000000000..8854506127 --- /dev/null +++ b/res/img/icons-people.svg @@ -0,0 +1,22 @@ + + + + 81230A28-D944-4572-B5DB-C03CAA2B1FCA + Created with sketchtool. + + + + + + + + + + + + + + + + + diff --git a/res/img/icons-pin.svg b/res/img/icons-pin.svg new file mode 100644 index 0000000000..a6fbf13baa --- /dev/null +++ b/res/img/icons-pin.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/res/img/icons-room-add.svg b/res/img/icons-room-add.svg new file mode 100644 index 0000000000..fc0ab750b6 --- /dev/null +++ b/res/img/icons-room-add.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/res/img/icons-room.svg b/res/img/icons-room.svg new file mode 100644 index 0000000000..d2abb21301 --- /dev/null +++ b/res/img/icons-room.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/res/img/icons-search-copy.svg b/res/img/icons-search-copy.svg new file mode 100644 index 0000000000..b026718b84 --- /dev/null +++ b/res/img/icons-search-copy.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/img/icons-search.svg b/res/img/icons-search.svg new file mode 100644 index 0000000000..d85709e66c --- /dev/null +++ b/res/img/icons-search.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/res/img/icons-settings-room.svg b/res/img/icons-settings-room.svg new file mode 100644 index 0000000000..117d134c95 --- /dev/null +++ b/res/img/icons-settings-room.svg @@ -0,0 +1,15 @@ + + + + 69011392-CE9D-4404-A85C-A8548C5D850B + Created with sketchtool. + + + + + + + + + + diff --git a/res/img/icons-settings.svg b/res/img/icons-settings.svg new file mode 100644 index 0000000000..3ca2b655f4 --- /dev/null +++ b/res/img/icons-settings.svg @@ -0,0 +1,15 @@ + + + + 4D42A2A7-7430-4D4F-A0A2-E19278CF66E3 + Created with sketchtool. + + + + + + + + + + diff --git a/res/img/icons-share.svg b/res/img/icons-share.svg new file mode 100644 index 0000000000..b27616d5d5 --- /dev/null +++ b/res/img/icons-share.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/icons-show-apps.svg b/res/img/icons-show-apps.svg new file mode 100644 index 0000000000..3438157301 --- /dev/null +++ b/res/img/icons-show-apps.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/icons-show-stickers.svg b/res/img/icons-show-stickers.svg new file mode 100644 index 0000000000..26779a3940 --- /dev/null +++ b/res/img/icons-show-stickers.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/res/img/icons-upload.svg b/res/img/icons-upload.svg new file mode 100644 index 0000000000..b0101e87a0 --- /dev/null +++ b/res/img/icons-upload.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/res/img/icons-video.svg b/res/img/icons-video.svg new file mode 100644 index 0000000000..d367f49609 --- /dev/null +++ b/res/img/icons-video.svg @@ -0,0 +1,20 @@ + + + + 05D354CE-86A7-4B6F-B9BE-F1CEBBD81B21 + Created with sketchtool. + + + + + + + + + + + + + + + diff --git a/res/img/icons_ellipsis.svg b/res/img/icons_ellipsis.svg new file mode 100644 index 0000000000..ba600ccacc --- /dev/null +++ b/res/img/icons_ellipsis.svg @@ -0,0 +1 @@ + diff --git a/res/img/icons_global.svg b/res/img/icons_global.svg new file mode 100644 index 0000000000..6c07d3c48e --- /dev/null +++ b/res/img/icons_global.svg @@ -0,0 +1,19 @@ + + + + icons_global copy 4 + Created with Sketch. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/info.png b/res/img/info.png new file mode 100644 index 0000000000..699fd64e01 Binary files /dev/null and b/res/img/info.png differ diff --git a/res/img/leave.svg b/res/img/leave.svg new file mode 100644 index 0000000000..1acbe59313 --- /dev/null +++ b/res/img/leave.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/img/list-close.png b/res/img/list-close.png new file mode 100644 index 0000000000..82b322f9d4 Binary files /dev/null and b/res/img/list-close.png differ diff --git a/res/img/list-close.svg b/res/img/list-close.svg new file mode 100644 index 0000000000..cd88b2a88f --- /dev/null +++ b/res/img/list-close.svg @@ -0,0 +1,15 @@ + + + + +Slice 1 +Created with Sketch. + + + + diff --git a/res/img/list-open.png b/res/img/list-open.png new file mode 100644 index 0000000000..f8c8063197 Binary files /dev/null and b/res/img/list-open.png differ diff --git a/res/img/list-open.svg b/res/img/list-open.svg new file mode 100644 index 0000000000..e180be8870 --- /dev/null +++ b/res/img/list-open.svg @@ -0,0 +1,15 @@ + + + + +Slice 1 +Created with Sketch. + + + + diff --git a/res/img/matrix-m.svg b/res/img/matrix-m.svg new file mode 100644 index 0000000000..ccb1df0fc5 --- /dev/null +++ b/res/img/matrix-m.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/res/img/maximise.svg b/res/img/maximise.svg new file mode 100644 index 0000000000..79c6c0ab8b --- /dev/null +++ b/res/img/maximise.svg @@ -0,0 +1,19 @@ + + + +minimise +Created with sketchtool. + + + + + + + + + + + + diff --git a/res/img/maximize.svg b/res/img/maximize.svg new file mode 100644 index 0000000000..4f9e10191f --- /dev/null +++ b/res/img/maximize.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/res/img/member_chevron.png b/res/img/member_chevron.png new file mode 100644 index 0000000000..cbbd289dcf Binary files /dev/null and b/res/img/member_chevron.png differ diff --git a/res/img/menu.png b/res/img/menu.png new file mode 100755 index 0000000000..b45f88950f Binary files /dev/null and b/res/img/menu.png differ diff --git a/res/img/minimise.svg b/res/img/minimise.svg new file mode 100644 index 0000000000..491756b15a --- /dev/null +++ b/res/img/minimise.svg @@ -0,0 +1,18 @@ + + + + minimise + Created with sketchtool. + + + + + + + + + + + + + diff --git a/res/img/minimize.svg b/res/img/minimize.svg new file mode 100644 index 0000000000..410b0bc08e --- /dev/null +++ b/res/img/minimize.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/res/img/mod.svg b/res/img/mod.svg new file mode 100644 index 0000000000..847baf98f9 --- /dev/null +++ b/res/img/mod.svg @@ -0,0 +1,16 @@ + + + + icons_admin + Created with sketchtool. + + + + + + + + + + + diff --git a/res/img/network-matrix.svg b/res/img/network-matrix.svg new file mode 100644 index 0000000000..bb8278ae39 --- /dev/null +++ b/res/img/network-matrix.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/res/img/newmessages.png b/res/img/newmessages.png new file mode 100644 index 0000000000..a22156ab21 Binary files /dev/null and b/res/img/newmessages.png differ diff --git a/res/img/newmessages.svg b/res/img/newmessages.svg new file mode 100644 index 0000000000..a2ffca9020 --- /dev/null +++ b/res/img/newmessages.svg @@ -0,0 +1,15 @@ + + + + icon_newmessages + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/res/img/notif-active.svg b/res/img/notif-active.svg new file mode 100644 index 0000000000..9eb279f851 --- /dev/null +++ b/res/img/notif-active.svg @@ -0,0 +1,20 @@ + + + + E15782FC-B5FA-472A-AE12-CFFF484E7253 + Created with sketchtool. + + + + + + + + + + + + + + + diff --git a/res/img/notif-slider.svg b/res/img/notif-slider.svg new file mode 100644 index 0000000000..55fa06d11a --- /dev/null +++ b/res/img/notif-slider.svg @@ -0,0 +1,22 @@ + + + + 16CB4618-0BD3-4568-BB20-FC56EBC46046 + Created with sketchtool. + + + + + + + + + + + + + + + + + diff --git a/res/img/placeholder.png b/res/img/placeholder.png new file mode 100644 index 0000000000..7da32f259c Binary files /dev/null and b/res/img/placeholder.png differ diff --git a/res/img/plus.svg b/res/img/plus.svg new file mode 100644 index 0000000000..e1d59ec6f4 --- /dev/null +++ b/res/img/plus.svg @@ -0,0 +1,13 @@ + + + + Line + Line + Created with Sketch. + + + + + + + + diff --git a/res/img/right_search.svg b/res/img/right_search.svg new file mode 100644 index 0000000000..b430a6be19 --- /dev/null +++ b/res/img/right_search.svg @@ -0,0 +1,17 @@ + + + + right_search + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/room-continuation.svg b/res/img/room-continuation.svg new file mode 100644 index 0000000000..dc7e15462a --- /dev/null +++ b/res/img/room-continuation.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/room_replaced.svg b/res/img/room_replaced.svg new file mode 100644 index 0000000000..fa5abd1c9f --- /dev/null +++ b/res/img/room_replaced.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/res/img/scrolldown.svg b/res/img/scrolldown.svg new file mode 100644 index 0000000000..d6599c5fc7 --- /dev/null +++ b/res/img/scrolldown.svg @@ -0,0 +1,15 @@ + + + + icon_newmessages + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/res/img/scrollto.svg b/res/img/scrollto.svg new file mode 100644 index 0000000000..75df053a68 --- /dev/null +++ b/res/img/scrollto.svg @@ -0,0 +1,21 @@ + + + + Slice 1 + Created with Sketch. + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/scrollup.svg b/res/img/scrollup.svg new file mode 100644 index 0000000000..1692f2a6c0 --- /dev/null +++ b/res/img/scrollup.svg @@ -0,0 +1,91 @@ + + + + + + image/svg+xml + + + + + + + icon_newmessages + Created with Sketch. + + + + + + + + + + diff --git a/res/img/search-button.svg b/res/img/search-button.svg new file mode 100644 index 0000000000..f4808842ff --- /dev/null +++ b/res/img/search-button.svg @@ -0,0 +1,15 @@ + + + + icon_search + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/res/img/search-icon-vector.svg b/res/img/search-icon-vector.svg new file mode 100644 index 0000000000..5780277f38 --- /dev/null +++ b/res/img/search-icon-vector.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/res/img/search.png b/res/img/search.png new file mode 100644 index 0000000000..2f98d29048 Binary files /dev/null and b/res/img/search.png differ diff --git a/res/img/search.svg b/res/img/search.svg new file mode 100644 index 0000000000..bd4cd9200c --- /dev/null +++ b/res/img/search.svg @@ -0,0 +1,17 @@ + + + + icons_search + Created with bin/sketchtool. + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/selected.png b/res/img/selected.png new file mode 100644 index 0000000000..8931cba75f Binary files /dev/null and b/res/img/selected.png differ diff --git a/res/img/settings-big.png b/res/img/settings-big.png new file mode 100644 index 0000000000..cb2e0a62d0 Binary files /dev/null and b/res/img/settings-big.png differ diff --git a/res/img/settings-big.svg b/res/img/settings-big.svg new file mode 100644 index 0000000000..c9587d58c2 --- /dev/null +++ b/res/img/settings-big.svg @@ -0,0 +1,18 @@ + + + + icons_settings + Created with sketchtool. + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/settings.png b/res/img/settings.png new file mode 100644 index 0000000000..264b3c9bc3 Binary files /dev/null and b/res/img/settings.png differ diff --git a/res/img/settings.svg b/res/img/settings.svg new file mode 100644 index 0000000000..4190c7b8de --- /dev/null +++ b/res/img/settings.svg @@ -0,0 +1,12 @@ + + + + icon_settings_small + Created with bin/sketchtool. + + + + + + + \ No newline at end of file diff --git a/res/img/social/email-1.png b/res/img/social/email-1.png new file mode 100644 index 0000000000..193cb659da Binary files /dev/null and b/res/img/social/email-1.png differ diff --git a/res/img/social/facebook.png b/res/img/social/facebook.png new file mode 100644 index 0000000000..457ef761a1 Binary files /dev/null and b/res/img/social/facebook.png differ diff --git a/res/img/social/linkedin.png b/res/img/social/linkedin.png new file mode 100644 index 0000000000..4c92adb56b Binary files /dev/null and b/res/img/social/linkedin.png differ diff --git a/res/img/social/reddit.png b/res/img/social/reddit.png new file mode 100644 index 0000000000..1310168470 Binary files /dev/null and b/res/img/social/reddit.png differ diff --git a/res/img/social/twitter-2.png b/res/img/social/twitter-2.png new file mode 100644 index 0000000000..9f6e7c602b Binary files /dev/null and b/res/img/social/twitter-2.png differ diff --git a/res/img/sound-indicator.svg b/res/img/sound-indicator.svg new file mode 100644 index 0000000000..9b8de53d81 --- /dev/null +++ b/res/img/sound-indicator.svg @@ -0,0 +1,17 @@ + + + + sound_indicator + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/spinner.gif b/res/img/spinner.gif new file mode 100644 index 0000000000..ab4871214b Binary files /dev/null and b/res/img/spinner.gif differ diff --git a/res/img/stickerpack-placeholder.png b/res/img/stickerpack-placeholder.png new file mode 100644 index 0000000000..7980114438 Binary files /dev/null and b/res/img/stickerpack-placeholder.png differ diff --git a/res/img/tick.svg b/res/img/tick.svg new file mode 100644 index 0000000000..6177f15f5e --- /dev/null +++ b/res/img/tick.svg @@ -0,0 +1,12 @@ + + + + icon_tick + Created with sketchtool. + + + + + + + diff --git a/res/img/trans.png b/res/img/trans.png new file mode 100644 index 0000000000..8ba2310a06 Binary files /dev/null and b/res/img/trans.png differ diff --git a/res/img/typing.png b/res/img/typing.png new file mode 100644 index 0000000000..066a0ce8fd Binary files /dev/null and b/res/img/typing.png differ diff --git a/res/img/upload-big.png b/res/img/upload-big.png new file mode 100644 index 0000000000..c11c0c452d Binary files /dev/null and b/res/img/upload-big.png differ diff --git a/res/img/upload-big.svg b/res/img/upload-big.svg new file mode 100644 index 0000000000..6099c2e976 --- /dev/null +++ b/res/img/upload-big.svg @@ -0,0 +1,19 @@ + + + + icons_upload_drop + Created with bin/sketchtool. + + + + + + + + + + + + + + diff --git a/res/img/upload.png b/res/img/upload.png new file mode 100644 index 0000000000..7457bcd0f1 Binary files /dev/null and b/res/img/upload.png differ diff --git a/res/img/upload.svg b/res/img/upload.svg new file mode 100644 index 0000000000..039014a2f3 --- /dev/null +++ b/res/img/upload.svg @@ -0,0 +1,19 @@ + + + + icons_upload + Created with bin/sketchtool. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/video-mute.svg b/res/img/video-mute.svg new file mode 100644 index 0000000000..6de60ba39b --- /dev/null +++ b/res/img/video-mute.svg @@ -0,0 +1,17 @@ + + + + icons_video copy + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/video-unmute.svg b/res/img/video-unmute.svg new file mode 100644 index 0000000000..a6c6c3b681 --- /dev/null +++ b/res/img/video-unmute.svg @@ -0,0 +1,18 @@ + + + + icons_video copy + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/img/video.png b/res/img/video.png new file mode 100644 index 0000000000..2a788f6fa4 Binary files /dev/null and b/res/img/video.png differ diff --git a/res/img/voice-mute.svg b/res/img/voice-mute.svg new file mode 100644 index 0000000000..336641078e --- /dev/null +++ b/res/img/voice-mute.svg @@ -0,0 +1,14 @@ + + + + Audio + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/res/img/voice-unmute.svg b/res/img/voice-unmute.svg new file mode 100644 index 0000000000..0d7e6f429f --- /dev/null +++ b/res/img/voice-unmute.svg @@ -0,0 +1,15 @@ + + + + Audio + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/res/img/voice.png b/res/img/voice.png new file mode 100644 index 0000000000..5ba765b0f4 Binary files /dev/null and b/res/img/voice.png differ diff --git a/res/img/voice.svg b/res/img/voice.svg new file mode 100644 index 0000000000..ff87270ba5 --- /dev/null +++ b/res/img/voice.svg @@ -0,0 +1,13 @@ + + + + icon_voice + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/res/img/voip-chevron.svg b/res/img/voip-chevron.svg new file mode 100644 index 0000000000..5f7cbe7153 --- /dev/null +++ b/res/img/voip-chevron.svg @@ -0,0 +1,12 @@ + + + + Triangle 1 + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/res/img/voip-mute.png b/res/img/voip-mute.png new file mode 100644 index 0000000000..a16d1001e5 Binary files /dev/null and b/res/img/voip-mute.png differ diff --git a/res/img/voip.png b/res/img/voip.png new file mode 100644 index 0000000000..e8f05bcc37 Binary files /dev/null and b/res/img/voip.png differ diff --git a/res/img/warning.png b/res/img/warning.png new file mode 100644 index 0000000000..c5553530a8 Binary files /dev/null and b/res/img/warning.png differ diff --git a/res/img/warning.svg b/res/img/warning.svg new file mode 100644 index 0000000000..b9a96a88e5 --- /dev/null +++ b/res/img/warning.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/warning2.png b/res/img/warning2.png new file mode 100644 index 0000000000..db0fd4a897 Binary files /dev/null and b/res/img/warning2.png differ diff --git a/res/img/warning_yellow.svg b/res/img/warning_yellow.svg new file mode 100644 index 0000000000..4d227517d2 --- /dev/null +++ b/res/img/warning_yellow.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/zoom.png b/res/img/zoom.png new file mode 100644 index 0000000000..f05ea959b4 Binary files /dev/null and b/res/img/zoom.png differ diff --git a/res/media/busy.mp3 b/res/media/busy.mp3 new file mode 100644 index 0000000000..fec27ba4c5 Binary files /dev/null and b/res/media/busy.mp3 differ diff --git a/res/media/busy.ogg b/res/media/busy.ogg new file mode 100644 index 0000000000..5d64a7d0d9 Binary files /dev/null and b/res/media/busy.ogg differ diff --git a/res/media/callend.mp3 b/res/media/callend.mp3 new file mode 100644 index 0000000000..50c34e5640 Binary files /dev/null and b/res/media/callend.mp3 differ diff --git a/res/media/callend.ogg b/res/media/callend.ogg new file mode 100644 index 0000000000..927ce1f634 Binary files /dev/null and b/res/media/callend.ogg differ diff --git a/res/media/message.mp3 b/res/media/message.mp3 new file mode 100644 index 0000000000..b87eeda7c2 Binary files /dev/null and b/res/media/message.mp3 differ diff --git a/res/media/message.ogg b/res/media/message.ogg new file mode 100644 index 0000000000..adc74437d0 Binary files /dev/null and b/res/media/message.ogg differ diff --git a/res/media/ring.mp3 b/res/media/ring.mp3 new file mode 100644 index 0000000000..36200cd89d Binary files /dev/null and b/res/media/ring.mp3 differ diff --git a/res/media/ring.ogg b/res/media/ring.ogg new file mode 100644 index 0000000000..708213bfac Binary files /dev/null and b/res/media/ring.ogg differ diff --git a/res/media/ringback.mp3 b/res/media/ringback.mp3 new file mode 100644 index 0000000000..6ee34bf395 Binary files /dev/null and b/res/media/ringback.mp3 differ diff --git a/res/media/ringback.ogg b/res/media/ringback.ogg new file mode 100644 index 0000000000..7dbfdcd017 Binary files /dev/null and b/res/media/ringback.ogg differ diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss new file mode 100644 index 0000000000..8ab338790e --- /dev/null +++ b/res/themes/dark/css/_dark.scss @@ -0,0 +1,206 @@ + +// typical text (dark-on-white in light skin) +$primary-fg-color: #dddddd; +$primary-bg-color: #2d2d2d; + +// used for focusing form controls +$focus-bg-color: #101010; + +// used for dialog box text +$light-fg-color: #747474; + +// button UI (white-on-green in light skin) +$accent-fg-color: $primary-bg-color; +$accent-color: #76CFA6; + +$selection-fg-color: $primary-fg-color; + +$focus-brightness: 200%; + +// red warning colour +$warning-color: #ff0064; +$warning-bg-color: #DF2A8B; +$info-bg-color: #2A9EDF; + +// groups +$info-plinth-bg-color: #454545; + +$other-user-pill-bg-color: rgba(255, 255, 255, 0.1); + +$preview-bar-bg-color: #333; + +// left-panel style muted accent color +$secondary-accent-color: $primary-bg-color; +$tertiary-accent-color: #454545; + +// stop the tinter trying to change the secondary accent color +// by overriding the key to something untintable +// XXX: this is a bit of a hack. +#mx_theme_secondaryAccentColor { + color: #c0ffee ! important; +} + +#mx_theme_tertiaryAccentColor { + color: #c0ffee ! important; +} + +// used by RoomDirectory permissions +$plinth-bg-color: #474747; + +// used by RoomDropTarget +$droptarget-bg-color: rgba(45,45,45,0.5); + +// used by AddressSelector +$selected-color: #000000; + +// selected for hoverover & selected event tiles +$event-selected-color: #353535; + +// used for the hairline dividers in RoomView +$primary-hairline-color: #474747; + +// used for the border of input text fields +$input-border-color: #3a3a3a; + +// apart from login forms, which have stronger border +$strong-input-border-color: #656565; + +// used for UserSettings EditableText +$input-underline-color: $primary-fg-color; +$input-fg-color: $primary-fg-color; + +// context menus +$menu-border-color: rgba(187, 187, 187, 0.5); +$menu-bg-color: #373737; + +$avatar-initial-color: #2d2d2d; +$avatar-bg-color: #ffffff; + +$h3-color: $primary-fg-color; + +$dialog-background-bg-color: #000; +$lightbox-background-bg-color: #000; + +$greyed-fg-color: #888; + +$neutral-badge-color: #888; + +$preview-widget-bar-color: $menu-bg-color; +$preview-widget-fg-color: $greyed-fg-color; + +$blockquote-bar-color: #ddd; +$blockquote-fg-color: #777; + +$settings-grey-fg-color: #a2a2a2; + +$voip-decline-color: #f48080; +$voip-accept-color: #80f480; + +$rte-bg-color: #353535; +$rte-code-bg-color: #000; + +// ******************** + +$roomtile-name-color: rgba(186, 186, 186, 0.8); +$roomtile-selected-bg-color: #333; +$roomtile-focused-bg-color: rgba(255, 255, 255, 0.2); + +$roomsublist-background: rgba(0, 0, 0, 0.2); +$roomsublist-label-fg-color: $h3-color; +$roomsublist-label-bg-color: $tertiary-accent-color; +$roomsublist-chevron-color: $accent-color; + +$panel-divider-color: rgba(118, 207, 166, 0.2); + +// ******************** + +$widget-menu-bar-bg-color: $tertiary-accent-color; + +// ******************** + +// event tile lifecycle +$event-encrypting-color: rgba(171, 221, 188, 0.4); +$event-sending-color: #888; +$event-notsent-color: #f44; + +// event redaction +$event-redacted-fg-color: #606060; +$event-redacted-border-color: #000000; + +// event timestamp +$event-timestamp-color: #acacac; + +$edit-button-url: "../../img/icon_context_message_dark.svg"; +$copy-button-url: "../../img/icon_copy_message_dark.svg"; + +// e2e +$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color +$e2e-unverified-color: #e8bf37; +$e2e-warning-color: #ba6363; + +/*** ImageView ***/ +$lightbox-bg-color: #454545; +$lightbox-fg-color: #ffffff; +$lightbox-border-color: #ffffff; + +// unused? +$progressbar-color: #000; + +// XXX: copypasted from _base in order to pick up the right FG color... +@define-mixin mx_DialogButton { + /* align images in buttons (eg spinners) */ + vertical-align: middle; + border: 0px; + border-radius: 36px; + font-family: $font-family; + font-size: 14px; + color: $accent-fg-color; + background-color: $accent-color; + width: auto; + padding: 7px; + padding-left: 1.5em; + padding-right: 1.5em; + cursor: pointer; + display: inline-block; + outline: none; +} + +// Nasty hacks to apply a filter to arbitrary monochrome artwork to make it +// better match the theme. Typically applied to dark grey 'off' buttons or +// light grey 'on' buttons. +.mx_filterFlipColor { + filter: invert(1); +} + +.gm-scrollbar .thumb { + filter: invert(1); +} + +// markdown overrides: +.mx_EventTile_content .markdown-body pre:hover { + border-color: #808080 !important; // inverted due to rules below +} +.mx_EventTile_content .markdown-body { + pre, code { + filter: invert(1); + } + + pre code { + filter: none; + } + + table { + tr { + background-color: #000000; + } + + tr:nth-child(2n) { + background-color: #080808; + } + } +} + +// Add a line to the right side of the left panel to distinguish it from the middle panel +.mx_LeftPanel { + border-right: 1px solid $tertiary-accent-color; +} diff --git a/res/themes/dark/css/dark.scss b/res/themes/dark/css/dark.scss new file mode 100644 index 0000000000..b69f096db7 --- /dev/null +++ b/res/themes/dark/css/dark.scss @@ -0,0 +1,4 @@ +@import "../../light/css/_base.scss"; +@import "_dark.scss"; +@import "../../../../res/css/_components.scss"; + diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss new file mode 100644 index 0000000000..c7fd38259c --- /dev/null +++ b/res/themes/light/css/_base.scss @@ -0,0 +1,185 @@ +/* Open Sans lacks combining diacritics, so these will fall through + to the next font. Helevetica's diacritics however do not combine + nicely with Open Sans (on OSX, at least) and result in a huge + horizontal mess. Arial empirically gets it right, hence prioritising + Arial here. */ +$font-family: 'Open Sans', Arial, Helvetica, Sans-Serif; + +// typical text (dark-on-white in light skin) +$primary-fg-color: #454545; +$primary-bg-color: #ffffff; + +// used for dialog box text +$light-fg-color: #747474; + +// used for focusing form controls +$focus-bg-color: #dddddd; + +// button UI (white-on-green in light skin) +$accent-fg-color: #ffffff; +$accent-color: #76CFA6; + +$selection-fg-color: $primary-bg-color; + +$focus-brightness: 125%; + +// red warning colour +$warning-color: #ff0064; +// background colour for warnings +$warning-bg-color: #DF2A8B; +$info-bg-color: #2A9EDF; +$mention-user-pill-bg-color: #ff0064; +$other-user-pill-bg-color: rgba(0, 0, 0, 0.1); + +// pinned events indicator +$pinned-unread-color: #ff0064; // $warning-color +$pinned-color: #888; + +// informational plinth +$info-plinth-bg-color: #f7f7f7; +$info-plinth-fg-color: #888; + +$preview-bar-bg-color: #f7f7f7; + +// left-panel style muted accent color +$secondary-accent-color: #eaf5f0; +$tertiary-accent-color: #d3efe1; + +// used by RoomDirectory permissions +$plinth-bg-color: $secondary-accent-color; + +// used by RoomDropTarget +$droptarget-bg-color: rgba(255,255,255,0.5); + +// used by AddressSelector +$selected-color: $secondary-accent-color; + +// selected for hoverover & selected event tiles +$event-selected-color: #f7f7f7; + +// used for the hairline dividers in RoomView +$primary-hairline-color: #e5e5e5; + +// used for the border of input text fields +$input-border-color: #f0f0f0; + +// apart from login forms, which have stronger border +$strong-input-border-color: #c7c7c7; + +// used for UserSettings EditableText +$input-underline-color: rgba(151, 151, 151, 0.5); +$input-fg-color: rgba(74, 74, 74, 0.9); + +// context menus +$menu-border-color: rgba(187, 187, 187, 0.5); +$menu-bg-color: #f6f6f6; + +$avatar-initial-color: #ffffff; +$avatar-bg-color: #ffffff; + +$h3-color: #3d3b39; + +$dialog-background-bg-color: #e9e9e9; +$lightbox-background-bg-color: #000; + +$greyed-fg-color: #888; + +$neutral-badge-color: #dbdbdb; + +$preview-widget-bar-color: #ddd; +$preview-widget-fg-color: $greyed-fg-color; + +$blockquote-bar-color: #ddd; +$blockquote-fg-color: #777; + +$settings-grey-fg-color: #a2a2a2; + +$voip-decline-color: #f48080; +$voip-accept-color: #80f480; + +$rte-bg-color: #e9e9e9; +$rte-code-bg-color: rgba(0, 0, 0, 0.04); +$rte-room-pill-color: #aaa; +$rte-group-pill-color: #aaa; + +// ******************** + +$roomtile-name-color: rgba(69, 69, 69, 0.8); +$roomtile-selected-bg-color: rgba(255, 255, 255, 0.8); +$roomtile-focused-bg-color: rgba(255, 255, 255, 0.9); + +$roomtile-transparent-focused-color: rgba(0, 0, 0, 0.1); + +$roomsublist-background: rgba(0, 0, 0, 0.05); +$roomsublist-label-fg-color: $h3-color; +$roomsublist-label-bg-color: $tertiary-accent-color; +$roomsublist-chevron-color: $accent-color; + +$panel-divider-color: rgba(118, 207, 166, 0.2); + +// ******************** + +$widget-menu-bar-bg-color: $tertiary-accent-color; + +// ******************** + +// event tile lifecycle +$event-encrypting-color: #abddbc; +$event-sending-color: #ddd; +$event-notsent-color: #f44; + +// event redaction +$event-redacted-fg-color: #e2e2e2; +$event-redacted-border-color: #cccccc; + +// event timestamp +$event-timestamp-color: #acacac; + +$edit-button-url: "../../img/icon_context_message.svg"; +$copy-button-url: "../../img/icon_copy_message.svg"; + +// e2e +$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color +$e2e-unverified-color: #e8bf37; +$e2e-warning-color: #ba6363; + +/*** ImageView ***/ +$lightbox-bg-color: #454545; +$lightbox-fg-color: #ffffff; +$lightbox-border-color: #ffffff; + +// unused? +$progressbar-color: #000; + +// ***** Mixins! ***** + +@define-mixin mx_DialogButton { + /* align images in buttons (eg spinners) */ + vertical-align: middle; + border: 0px; + border-radius: 36px; + font-family: $font-family; + font-size: 14px; + color: $accent-fg-color; + background-color: $accent-color; + width: auto; + padding: 7px; + padding-left: 1.5em; + padding-right: 1.5em; + cursor: pointer; + display: inline-block; + outline: none; +} + +@define-mixin mx_DialogButton_danger { + background-color: $warning-color; +} + +@define-mixin mx_DialogButton_hover { +} + +@define-mixin mx_DialogButton_small { + @mixin mx_DialogButton; + font-size: 15px; + padding: 0px 1.5em 0px 1.5em; +} diff --git a/res/themes/light/css/light.scss b/res/themes/light/css/light.scss new file mode 100644 index 0000000000..2099f41f60 --- /dev/null +++ b/res/themes/light/css/light.scss @@ -0,0 +1,3 @@ +@import "_base.scss"; +@import "../../../../res/css/_components.scss"; + diff --git a/scripts/emoji-data-strip.js b/scripts/emoji-data-strip.js index 40156471fe..42bf2ac2de 100644 --- a/scripts/emoji-data-strip.js +++ b/scripts/emoji-data-strip.js @@ -12,6 +12,9 @@ const output = Object.keys(EMOJI_DATA).map( category: datum.category, emoji_order: datum.emoji_order, }; + if (datum.aliases.length > 0) { + newDatum.aliases = datum.aliases; + } if (datum.aliases_ascii.length > 0) { newDatum.aliases_ascii = datum.aliases_ascii; } diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh new file mode 100755 index 0000000000..73c622133b --- /dev/null +++ b/scripts/fetchdep.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +set -e + +org="$1" +repo="$2" + +rm -r "$repo" || true + +curbranch="$TRAVIS_PULL_REQUEST_BRANCH" +[ -z "$curbranch" ] && curbranch="$TRAVIS_BRANCH" +[ -z "$curbranch" ] && curbranch=`"echo $GIT_BRANCH" | sed -e 's/^origin\///'` # jenkins + +if [ -n "$curbranch" ] +then + echo "Determined branch to be $curbranch" + + git clone https://github.com/$org/$repo.git $repo --branch "$curbranch" && exit 0 +fi + +echo "Checking out develop branch" +git clone https://github.com/$org/$repo.git $repo --branch develop diff --git a/scripts/fixup-imports.pl b/scripts/fixup-imports.pl new file mode 100755 index 0000000000..3929ab88c9 --- /dev/null +++ b/scripts/fixup-imports.pl @@ -0,0 +1,27 @@ +#!/usr/bin/perl -pi + +# pass in a list of filenames whose imports should be fixed up to be relative +# to matrix-react-sdk rather than vector-web. +# filenames must be relative to src/ - e.g. ./components/moo/Moo.js + +# run with something like: +# sierra:src matthew$ grep -ril 'require(.matrix-react-sdk' . | xargs ../scripts/fixup-imports.pl +# sierra:src matthew$ grep -ril 'import.*matrix-react-sdk' . | xargs ../scripts/fixup-imports.pl + +# e.g. turning: +# var rate_limited_func = require('matrix-react-sdk/lib/ratelimitedfunc'); +# +# into: +# const rate_limited_func = require('../../ratelimitedfunc'); +# +# ...if the current file is two levels deep inside lib. + +$depth = () = $ARGV =~ m#/#g; +$depth--; +$prefix = $depth > 0 ? ('../' x $depth) : './'; + +s/= require\(['"]matrix-react-sdk\/lib\/(.*?)['"]\)/= require('$prefix$1')/; +s/= require\(['"]matrix-react-sdk['"]\)/= require('${prefix}index')/; + +s/^(import .* from )['"]matrix-react-sdk\/lib\/(.*?)['"]/$1'$prefix$2'/; +s/^(import .* from )['"]matrix-react-sdk['"]/$1'${prefix}index'/; diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js new file mode 100755 index 0000000000..c35cd3466a --- /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..48410ea904 100755 --- a/scripts/travis.sh +++ b/scripts/travis.sh @@ -2,10 +2,16 @@ set -ex +scripts/fetchdep.sh matrix-org matrix-js-sdk +rm -r node_modules/matrix-js-sdk || true +ln -s ../matrix-js-sdk node_modules/matrix-js-sdk + +cd matrix-js-sdk +npm install +cd .. + 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/Analytics.js b/src/Analytics.js index a82f57a144..d85d635b28 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -14,25 +14,86 @@ limitations under the License. */ -import { getCurrentLanguage } from './languageHandler'; +import { getCurrentLanguage, _t, _td } from './languageHandler'; import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; +import Modal from './Modal'; +import sdk from './index'; +const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password|home|directory)/; +const hashVarRegex = /#\/(group|room|user)\/.*$/; + +// Remove all but the first item in the hash path. Redact unexpected hashes. +function getRedactedHash(hash) { + // Don't leak URLs we aren't expecting - they could contain tokens/PII + const match = hashRegex.exec(hash); + if (!match) { + console.warn(`Unexpected hash location "${hash}"`); + return '#/'; + } + + if (hashVarRegex.test(hash)) { + return hash.replace(hashVarRegex, "#/$1/"); + } + + return hash.replace(hashRegex, "#/$1"); +} + +// Return the current origin, path and hash separated with a `/`. This does +// not include query parameters. function getRedactedUrl() { - const redactedHash = window.location.hash.replace(/#\/(room|user)\/(.+)/, "#/$1/"); - // hardcoded url to make piwik happy - return 'https://riot.im/app/' + redactedHash; + const { origin, hash } = window.location; + let { pathname } = window.location; + + // Redact paths which could contain unexpected PII + if (origin.startsWith('file://')) { + pathname = "//"; + } + + return origin + pathname + getRedactedHash(hash); } const customVariables = { - 'App Platform': 1, - 'App Version': 2, - 'User Type': 3, - 'Chosen Language': 4, - 'Instance': 5, - 'RTE: Uses Richtext Mode': 6, - 'Homeserver URL': 7, - 'Identity Server URL': 8, + 'App Platform': { + id: 1, + expl: _td('The platform you\'re on'), + example: 'Electron Platform', + }, + 'App Version': { + id: 2, + expl: _td('The version of Riot.im'), + example: '15.0.0', + }, + 'User Type': { + id: 3, + expl: _td('Whether or not you\'re logged in (we don\'t record your user name)'), + example: 'Logged In', + }, + 'Chosen Language': { + id: 4, + expl: _td('Your language of choice'), + example: 'en', + }, + 'Instance': { + id: 5, + expl: _td('Which officially provided instance you are using, if any'), + example: 'app', + }, + 'RTE: Uses Richtext Mode': { + id: 6, + expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'), + example: 'off', + }, + 'Homeserver URL': { + id: 7, + expl: _td('Your homeserver\'s URL'), + example: 'https://matrix.org', + }, + 'Identity Server URL': { + id: 8, + expl: _td('Your identity server\'s URL'), + example: 'https://vector.im', + }, }; function whitelistRedact(whitelist, str) { @@ -40,9 +101,6 @@ function whitelistRedact(whitelist, str) { return ''; } -const whitelistedHSUrls = ["https://matrix.org"]; -const whitelistedISUrls = ["https://vector.im"]; - class Analytics { constructor() { this._paq = null; @@ -66,6 +124,10 @@ class Analytics { */ disable() { this.trackEvent('Analytics', 'opt-out'); + // disableHeartBeatTimer is undocumented but exists in the piwik code + // the _paq.push method will result in an error being printed in the console + // if an unknown method signature is passed + this._paq.push(['disableHeartBeatTimer']); this.disabled = true; } @@ -117,7 +179,7 @@ class Analytics { return true; } - trackPageChange() { + trackPageChange(generationTimeMs) { if (this.disabled) return; if (this.firstPage) { // De-duplicate first page @@ -125,13 +187,21 @@ class Analytics { this.firstPage = false; return; } + + if (typeof generationTimeMs === 'number') { + this._paq.push(['setGenerationTimeMs', generationTimeMs]); + } else { + console.warn('Analytics.trackPageChange: expected generationTimeMs to be a number'); + // But continue anyway because we still want to track the change + } + this._paq.push(['setCustomUrl', getRedactedUrl()]); this._paq.push(['trackPageView']); } - trackEvent(category, action, name) { + trackEvent(category, action, name, value) { if (this.disabled) return; - this._paq.push(['trackEvent', category, action, name]); + this._paq.push(['trackEvent', category, action, name, value]); } logout() { @@ -140,11 +210,19 @@ class Analytics { } _setVisitVariable(key, value) { - this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']); + if (this.disabled) return; + this._paq.push(['setCustomVariable', customVariables[key].id, key, value, 'visit']); } setLoggedIn(isGuest, homeserverUrl, identityServerUrl) { if (this.disabled) return; + + const config = SdkConfig.get(); + if (!config.piwik) return; + + const whitelistedHSUrls = config.piwik.whitelistedHSUrls || []; + const whitelistedISUrls = config.piwik.whitelistedISUrls || []; + this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In'); this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl)); this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl)); @@ -154,6 +232,64 @@ class Analytics { if (this.disabled) return; this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off'); } + + showDetailsModal() { + let rows = []; + if (window.Piwik) { + const Tracker = window.Piwik.getAsyncTracker(); + rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean); + } else { + // Piwik may not have been enabled, so show example values + rows = Object.keys(customVariables).map( + (k) => [ + k, + _t('e.g. %(exampleValue)s', { exampleValue: customVariables[k].example }), + ], + ); + } + + const resolution = `${window.screen.width}x${window.screen.height}`; + const otherVariables = [ + { + expl: _td('Every page you use in the app'), + value: _t( + 'e.g. ', + {}, + { + CurrentPageURL: getRedactedUrl(), + }, + ), + }, + { expl: _td('Your User Agent'), value: navigator.userAgent }, + { expl: _td('Your device resolution'), value: resolution }, + ]; + + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, { + title: _t('Analytics'), + description:

+
+ { _t('The information being sent to us to help make Riot.im better includes:') } +
+ + { rows.map((row) => + + { row[1] !== undefined && } + ) } + { otherVariables.map((item, index) => + + + + , + ) } +
{ _t(customVariables[row[0]].expl) }{ row[1] }
{ _t(item.expl) }{ item.value }
+
+ { _t('Where this page includes identifiable information, such as a room, ' + + 'user or group ID, that data is removed before being sent to the server.') } +
+
, + }); + } } if (!global.mxAnalytics) { 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..acdc3e5122 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017, 2018 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,37 @@ 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 SdkConfig from './SdkConfig'; +import { showUnknownDeviceDialogForCalls } from './cryptodevices'; +import WidgetUtils from './utils/WidgetUtils'; +import WidgetEchoStore from './stores/WidgetEchoStore'; +import ScalarAuthClient from './ScalarAuthClient'; 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 +92,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 +163,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 +197,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 +220,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,134 +241,86 @@ 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); } } switch (payload.action) { case 'place_call': - if (module.exports.getAnyActiveCall()) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, { - title: _t('Existing Call'), - description: _t('You are already in a call.'), - }); - return; // don't allow >1 call to be placed. - } + { + if (module.exports.getAnyActiveCall()) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, { + title: _t('Existing Call'), + description: _t('You are already in a call.'), + }); + return; // don't allow >1 call to be placed. + } - // if the runtime env doesn't do VoIP, whine. - 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.'), - }); - return; - } + // if the runtime env doesn't do VoIP, whine. + 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.'), + }); + return; + } - var room = MatrixClientPeg.get().getRoom(payload.room_id); - if (!room) { - console.error("Room %s does not exist.", payload.room_id); - return; - } + const room = MatrixClientPeg.get().getRoom(payload.room_id); + if (!room) { + console.error("Room %s does not exist.", payload.room_id); + return; + } - var members = room.getJoinedMembers(); - if (members.length <= 1) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, { - description: _t('You cannot place a call with yourself.'), - }); - return; - } - else if (members.length === 2) { - console.log("Place %s call in %s", payload.type, payload.room_id); - const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, { - forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false), - }); - placeCall(call); - } - else { // > 2 - dis.dispatch({ - action: "place_conference_call", - room_id: payload.room_id, - type: payload.type, - remote_element: payload.remote_element, - local_element: payload.local_element - }); + const members = room.getJoinedMembers(); + if (members.length <= 1) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, { + description: _t('You cannot place a call with yourself.'), + }); + return; + } 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); + placeCall(call); + } 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, + }); + } } break; case 'place_conference_call': console.log("Place conference call in %s", payload.room_id); - if (!ConferenceHandler) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, { - description: _t('Conference calls are not supported in this client'), - }); - } - 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)) { - // 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 - // because a central server would be decrypting the audio for each - // participant. - // Therefore we disable conference calling in E2E rooms. - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - 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"); - 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=>{ - if (confirm) { - ConferenceHandler.createNewMatrixCall( - MatrixClientPeg.get(), payload.room_id - ).done(function(call) { - placeCall(call); - }, function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Conference call failed: " + err); - Modal.createTrackedDialog('Call Handler', 'Failed to set up conference call', ErrorDialog, { - title: _t('Failed to set up conference call'), - description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''), - }); - }); - } - }, - }); - } + _startCallApp(payload.room_id, payload.type); break; case 'incoming_call': - if (module.exports.getAnyActiveCall()) { - // ignore multiple incoming calls. in future, we may want a line-1/line-2 setup. - // we avoid rejecting with "busy" in case the user wants to answer it on a different device. - // in future we could signal a "local busy" as a warning to the caller. - // see https://github.com/vector-im/vector-web/issues/1964 - return; - } + { + if (module.exports.getAnyActiveCall()) { + // ignore multiple incoming calls. in future, we may want a line-1/line-2 setup. + // we avoid rejecting with "busy" in case the user wants to answer it on a different device. + // in future we could signal a "local busy" as a warning to the caller. + // see https://github.com/vector-im/vector-web/issues/1964 + return; + } - // if the runtime env doesn't do VoIP, stop here. - if (!MatrixClientPeg.get().supportsVoip()) { - return; - } + // if the runtime env doesn't do VoIP, stop here. + if (!MatrixClientPeg.get().supportsVoip()) { + return; + } - var call = payload.call; - _setCallListeners(call); - _setCallState(call, call.roomId, "ringing"); + const call = payload.call; + _setCallListeners(call); + _setCallState(call, call.roomId, "ringing"); + } break; case 'hangup': if (!calls[payload.room_id]) { @@ -357,20 +337,126 @@ 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; } } + +async function _startCallApp(roomId, type) { + // check for a working intgrations manager. Technically we could put + // the state event in anyway, but the resulting widget would then not + // work for us. Better that the user knows before everyone else in the + // room sees it. + const scalarClient = new ScalarAuthClient(); + let haveScalar = false; + try { + await scalarClient.connect(); + haveScalar = scalarClient.hasCredentials(); + } catch (e) { + // fall through + } + if (!haveScalar) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + + Modal.createTrackedDialog('Could not connect to the integration server', '', ErrorDialog, { + title: _t('Could not connect to the integration server'), + description: _t('A conference call could not be started because the intgrations server is not available'), + }); + return; + } + + dis.dispatch({ + action: 'appsDrawer', + show: true, + }); + + const room = MatrixClientPeg.get().getRoom(roomId); + const currentRoomWidgets = WidgetUtils.getRoomWidgets(room); + + if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentRoomWidgets, 'jitsi')) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + + Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, { + title: _t('Call in Progress'), + description: _t('A call is currently being placed!'), + }); + return; + } + + const currentJitsiWidgets = currentRoomWidgets.filter((ev) => { + return ev.getContent().type === 'jitsi'; + }); + if (currentJitsiWidgets.length > 0) { + console.warn( + "Refusing to start conference call widget in " + roomId + + " a conference call widget is already present", + ); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + + Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, { + title: _t('Call in Progress'), + description: _t('A call is already in progress!'), + }); + return; + } + + // This inherits its poor naming from the field of the same name that goes into + // the event. It's just a random string to make the Jitsi URLs unique. + const widgetSessionId = Math.random().toString(36).substring(2); + const confId = room.roomId.replace(/[^A-Za-z0-9]/g, '') + widgetSessionId; + // NB. we can't just encodeURICompoent all of these because the $ signs need to be there + // (but currently the only thing that needs encoding is the confId) + const queryString = [ + 'confId='+encodeURIComponent(confId), + 'isAudioConf='+(type === 'voice' ? 'true' : 'false'), + 'displayName=$matrix_display_name', + 'avatarUrl=$matrix_avatar_url', + 'email=$matrix_user_id', + ].join('&'); + + let widgetUrl; + if (SdkConfig.get().integrations_jitsi_widget_url) { + // Try this config key. This probably isn't ideal as a way of discovering this + // URL, but this will at least allow the integration manager to not be hardcoded. + widgetUrl = SdkConfig.get().integrations_jitsi_widget_url + '?' + queryString; + } else { + widgetUrl = SdkConfig.get().integrations_rest_url + '/widgets/jitsi.html?' + queryString; + } + + const widgetData = { widgetSessionId }; + + const widgetId = ( + 'jitsi_' + + MatrixClientPeg.get().credentials.userId + + '_' + + Date.now() + ); + + WidgetUtils.setRoomWidget(roomId, widgetId, 'jitsi', widgetUrl, 'Jitsi', widgetData).then(() => { + console.log('Jitsi widget added'); + }).catch((e) => { + if (e.errcode === 'M_FORBIDDEN') { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + + Modal.createTrackedDialog('Call Failed', '', ErrorDialog, { + title: _t('Permission Required'), + description: _t("You do not have permission to start a conference call in this room"), + }); + } + console.error(e); + }); +} + // FIXME: Nasty way of making sure we only register // with the dispatcher once 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 +472,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]]; @@ -396,13 +482,31 @@ var callHandler = { return null; }, + /** + * The conference handler is a module that deals with implementation-specific + * multi-party calling implementations. Riot passes in its own which creates + * a one-to-one call with a freeswitch conference bridge. As of July 2018, + * the de-facto way of conference calling is a Jitsi widget, so this is + * deprecated. It reamins here for two reasons: + * 1. So Riot still supports joining existing freeswitch conference calls + * (but doesn't support creating them). After a transition period, we can + * remove support for joining them too. + * 2. To hide the one-to-one rooms that old-style conferencing creates. This + * is much harder to remove: probably either we make Riot leave & forget these + * rooms after we remove support for joining freeswitch conferences, or we + * accept that random rooms with cryptic users will suddently appear for + * anyone who's ever used conference calling, or we are stuck with this + * code forever. + * + * @param {object} confHandler The conference handler object + */ setConferenceHandler: function(confHandler) { ConferenceHandler = confHandler; }, 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..2330f86b99 100644 --- a/src/CallMediaHandler.js +++ b/src/CallMediaHandler.js @@ -14,51 +14,59 @@ 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() { // Only needed for Electron atm, though should work in modern browsers // once permission has been granted to the webapp return navigator.mediaDevices.enumerateDevices().then(function(devices) { - const audioIn = []; - const videoIn = []; + const audiooutput = []; + const audioinput = []; + const videoinput = []; if (devices.some((device) => !device.label)) return false; devices.forEach((device) => { switch (device.kind) { - case 'audioinput': audioIn.push(device); break; - case 'videoinput': videoIn.push(device); break; + case 'audiooutput': audiooutput.push(device); break; + case 'audioinput': audioinput.push(device); break; + case 'videoinput': videoinput.push(device); break; } }); // console.log("Loaded WebRTC Devices", mediaDevices); return { - audioinput: audioIn, - videoinput: videoIn, + audiooutput, + audioinput, + videoinput, }; }, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); }); }, 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 audioOutDeviceId = SettingsStore.getValue("webrtc_audiooutput"); + const audioDeviceId = SettingsStore.getValue("webrtc_audioinput"); + const videoDeviceId = SettingsStore.getValue("webrtc_videoinput"); + + Matrix.setMatrixCallAudioOutput(audioOutDeviceId); + Matrix.setMatrixCallAudioInput(audioDeviceId); + Matrix.setMatrixCallVideoInput(videoDeviceId); + }, + + setAudioOutput: function(deviceId) { + SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId); + Matrix.setMatrixCallAudioOutput(deviceId); }, 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..0164e6c4cd 100644 --- a/src/ComposerHistoryManager.js +++ b/src/ComposerHistoryManager.js @@ -15,70 +15,73 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ContentState, convertToRaw, convertFromRaw} from 'draft-js'; -import * as RichText from './RichText'; -import Markdown from './Markdown'; +import { Value } from 'slate'; + import _clamp from 'lodash/clamp'; -type MessageFormat = 'html' | 'markdown'; +type MessageFormat = 'rich' | 'markdown'; class HistoryItem { - // Keeping message for backwards-compatibility - message: string; - rawContentState: RawDraftContentState; - format: MessageFormat = 'html'; + // We store history items in their native format to ensure history is accurate + // and then convert them if our RTE has subsequently changed format. + value: Value; + format: MessageFormat = 'rich'; - constructor(contentState: ?ContentState, format: ?MessageFormat) { - this.rawContentState = contentState ? convertToRaw(contentState) : null; + constructor(value: ?Value, format: ?MessageFormat) { + this.value = value; this.format = format; } - toContentState(outputFormat: MessageFormat): ContentState { - const contentState = convertFromRaw(this.rawContentState); - if (outputFormat === 'markdown') { - if (this.format === 'html') { - return ContentState.createFromText(RichText.stateToMarkdown(contentState)); - } - } else { - if (this.format === 'markdown') { - return RichText.htmlToContentState(new Markdown(contentState.getPlainText()).toHTML()); - } - } - // history item has format === outputFormat - return contentState; + static fromJSON(obj: Object): HistoryItem { + return new HistoryItem( + Value.fromJSON(obj.value), + obj.format, + ); + } + + toJSON(): Object { + return { + value: this.value.toJSON(), + format: this.format, + }; } } export default class ComposerHistoryManager { history: Array = []; prefix: string; - lastIndex: number = 0; - currentIndex: number = 0; + lastIndex: number = 0; // used for indexing the storage + currentIndex: number = 0; // used for indexing the loaded validated history Array constructor(roomId: string, prefix: string = 'mx_composer_history_') { this.prefix = prefix + roomId; // TODO: Performance issues? let item; - for(; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { - this.history.push( - Object.assign(new HistoryItem(), JSON.parse(item)), - ); + for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { + try { + this.history.push( + HistoryItem.fromJSON(JSON.parse(item)), + ); + } catch (e) { + console.warn("Throwing away unserialisable history", e); + } } this.lastIndex = this.currentIndex; + // reset currentIndex to account for any unserialisable history + this.currentIndex = this.history.length; } - save(contentState: ContentState, format: MessageFormat) { - const item = new HistoryItem(contentState, format); + save(value: Value, format: MessageFormat) { + const item = new HistoryItem(value, format); this.history.push(item); - this.currentIndex = this.lastIndex + 1; - sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item)); + this.currentIndex = this.history.length; + sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON())); } - getItem(offset: number, format: MessageFormat): ?ContentState { - this.currentIndex = _clamp(this.currentIndex + offset, 0, this.lastIndex - 1); - const item = this.history[this.currentIndex]; - return item ? item.toContentState(format) : null; + getItem(offset: number): ?HistoryItem { + this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1); + return this.history[this.currentIndex]; } } diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 93057fafed..fd21977108 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) { @@ -249,6 +243,7 @@ function uploadFile(matrixClient, roomId, file, progressHandler) { const blob = new Blob([encryptResult.data]); return matrixClient.uploadContent(blob, { progressHandler: progressHandler, + includeFilename: false, }).then(function(url) { // If the attachment is encrypted then bundle the URL along // with the information needed to decrypt the attachment and @@ -281,12 +276,19 @@ class ContentMessages { this.nextId = 0; } + sendStickerContentToRoom(url, roomId, info, text, matrixClient) { + return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => { + console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); + throw e; + }); + } + sendContentToRoom(file, roomId, matrixClient) { const content = { 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 +299,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 +312,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 +333,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 +357,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 +369,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 +378,7 @@ class ContentMessages { } if (error) { dis.dispatch({action: 'upload_failed', upload: upload}); - } - else { + } else { dis.dispatch({action: 'upload_finished', upload: upload}); } }); @@ -389,9 +390,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 77f3644f6f..108697238c 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; import { _t } from './languageHandler'; function getDaysArray() { @@ -51,55 +50,89 @@ function pad(n) { return (n < 10 ? '0' : '') + n; } -function twelveHourTime(date) { +function twelveHourTime(date, showSeconds=false) { let hours = date.getHours() % 12; const minutes = pad(date.getMinutes()); const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM'); hours = hours ? hours : 12; // convert 0 -> 12 + if (showSeconds) { + const seconds = pad(date.getSeconds()); + return `${hours}:${minutes}:${seconds}${ampm}`; + } return `${hours}:${minutes}${ampm}`; } -module.exports = { - formatDate: function(date, showTwelveHour=false) { - const now = new Date(); - const days = getDaysArray(); - const months = getMonthsArray(); - if (date.toDateString() === now.toDateString()) { - 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', { - weekDayName: days[date.getDay()], - time: this.formatTime(date, showTwelveHour), - }); - } else if (now.getFullYear() === date.getFullYear()) { - // TODO: use standard date localize function provided in counterpart - return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', { - weekDayName: days[date.getDay()], - monthName: months[date.getMonth()], - day: date.getDate(), - time: this.formatTime(date, showTwelveHour), - }); - } - return this.formatFullDate(date, showTwelveHour); - }, - - formatFullDate: function(date, showTwelveHour=false) { - const days = getDaysArray(); - const months = getMonthsArray(); - return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', { +export function formatDate(date, showTwelveHour=false) { + const now = new Date(); + const days = getDaysArray(); + const months = getMonthsArray(); + if (date.toDateString() === now.toDateString()) { + return 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', { + weekDayName: days[date.getDay()], + time: formatTime(date, showTwelveHour), + }); + } else if (now.getFullYear() === date.getFullYear()) { + // TODO: use standard date localize function provided in counterpart + return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', { weekDayName: days[date.getDay()], monthName: months[date.getMonth()], day: date.getDate(), - fullYear: date.getFullYear(), - time: this.formatTime(date, showTwelveHour), + time: formatTime(date, showTwelveHour), }); - }, + } + return formatFullDate(date, showTwelveHour); +} - formatTime: function(date, showTwelveHour=false) { - if (showTwelveHour) { - return twelveHourTime(date); - } - return pad(date.getHours()) + ':' + pad(date.getMinutes()); - }, -}; +export function formatFullDateNoTime(date) { + const days = getDaysArray(); + const months = getMonthsArray(); + return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', { + weekDayName: days[date.getDay()], + monthName: months[date.getMonth()], + day: date.getDate(), + fullYear: date.getFullYear(), + }); +} + +export function formatFullDate(date, showTwelveHour=false) { + const days = getDaysArray(); + const months = getMonthsArray(); + return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', { + weekDayName: days[date.getDay()], + monthName: months[date.getMonth()], + day: date.getDate(), + fullYear: date.getFullYear(), + time: formatFullTime(date, showTwelveHour), + }); +} + +export function formatFullTime(date, showTwelveHour=false) { + if (showTwelveHour) { + return twelveHourTime(date, true); + } + return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()); +} + +export function formatTime(date, showTwelveHour=false) { + if (showTwelveHour) { + return twelveHourTime(date); + } + return pad(date.getHours()) + ':' + pad(date.getMinutes()); +} + +const MILLIS_IN_DAY = 86400000; +export function wantsDateSeparator(prevEventDate, nextEventDate) { + if (!nextEventDate || !prevEventDate) { + return false; + } + // Return early for events that are > 24h apart + if (Math.abs(prevEventDate.getTime() - nextEventDate.getTime()) > MILLIS_IN_DAY) { + return true; + } + + // Compare weekdays + return prevEventDate.getDay() !== nextEventDate.getDay(); +} diff --git a/src/DecryptionFailureTracker.js b/src/DecryptionFailureTracker.js new file mode 100644 index 0000000000..b02a5e937b --- /dev/null +++ b/src/DecryptionFailureTracker.js @@ -0,0 +1,202 @@ +/* +Copyright 2018 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. +*/ + +export class DecryptionFailure { + constructor(failedEventId, errorCode) { + this.failedEventId = failedEventId; + this.errorCode = errorCode; + this.ts = Date.now(); + } +} + +export class DecryptionFailureTracker { + // Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list + // is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did + // are accumulated in `failureCounts`. + failures = []; + + // A histogram of the number of failures that will be tracked at the next tracking + // interval, split by failure error code. + failureCounts = { + // [errorCode]: 42 + }; + + // Event IDs of failures that were tracked previously + trackedEventHashMap = { + // [eventId]: true + }; + + // Set to an interval ID when `start` is called + checkInterval = null; + trackInterval = null; + + // Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`. + static TRACK_INTERVAL_MS = 60000; + + // Call `checkFailures` every `CHECK_INTERVAL_MS`. + static CHECK_INTERVAL_MS = 5000; + + // Give events a chance to be decrypted by waiting `GRACE_PERIOD_MS` before counting + // the failure in `failureCounts`. + static GRACE_PERIOD_MS = 60000; + + /** + * Create a new DecryptionFailureTracker. + * + * Call `eventDecrypted(event, err)` on this instance when an event is decrypted. + * + * Call `start()` to start the tracker, and `stop()` to stop tracking. + * + * @param {function} fn The tracking function, which will be called when failures + * are tracked. The function should have a signature `(count, trackedErrorCode) => {...}`, + * where `count` is the number of failures and `errorCode` matches the `.code` of + * provided DecryptionError errors (by default, unless `errorCodeMapFn` is specified. + * @param {function?} errorCodeMapFn The function used to map error codes to the + * trackedErrorCode. If not provided, the `.code` of errors will be used. + */ + constructor(fn, errorCodeMapFn) { + if (!fn || typeof fn !== 'function') { + throw new Error('DecryptionFailureTracker requires tracking function'); + } + + if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') { + throw new Error('DecryptionFailureTracker second constructor argument should be a function'); + } + + this._trackDecryptionFailure = fn; + this._mapErrorCode = errorCodeMapFn; + } + + // loadTrackedEventHashMap() { + // this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes')) || {}; + // } + + // saveTrackedEventHashMap() { + // localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap)); + // } + + eventDecrypted(e, err) { + if (err) { + this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code)); + } else { + // Could be an event in the failures, remove it + this.removeDecryptionFailuresForEvent(e); + } + } + + addDecryptionFailure(failure) { + this.failures.push(failure); + } + + removeDecryptionFailuresForEvent(e) { + this.failures = this.failures.filter((f) => f.failedEventId !== e.getId()); + } + + /** + * Start checking for and tracking failures. + */ + start() { + this.checkInterval = setInterval( + () => this.checkFailures(Date.now()), + DecryptionFailureTracker.CHECK_INTERVAL_MS, + ); + + this.trackInterval = setInterval( + () => this.trackFailures(), + DecryptionFailureTracker.TRACK_INTERVAL_MS, + ); + } + + /** + * Clear state and stop checking for and tracking failures. + */ + stop() { + clearInterval(this.checkInterval); + clearInterval(this.trackInterval); + + this.failures = []; + this.failureCounts = {}; + } + + /** + * Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be + * tracked. Only mark one failure per event ID. + * @param {number} nowTs the timestamp that represents the time now. + */ + checkFailures(nowTs) { + const failuresGivenGrace = []; + const failuresNotReady = []; + while (this.failures.length > 0) { + const f = this.failures.shift(); + if (nowTs > f.ts + DecryptionFailureTracker.GRACE_PERIOD_MS) { + failuresGivenGrace.push(f); + } else { + failuresNotReady.push(f); + } + } + this.failures = failuresNotReady; + + // Only track one failure per event + const dedupedFailuresMap = failuresGivenGrace.reduce( + (map, failure) => { + if (!this.trackedEventHashMap[failure.failedEventId]) { + return map.set(failure.failedEventId, failure); + } else { + return map; + } + }, + // Use a map to preseve key ordering + new Map(), + ); + + const trackedEventIds = [...dedupedFailuresMap.keys()]; + + this.trackedEventHashMap = trackedEventIds.reduce( + (result, eventId) => ({...result, [eventId]: true}), + this.trackedEventHashMap, + ); + + // Commented out for now for expediency, we need to consider unbound nature of storing + // this in localStorage + // this.saveTrackedEventHashMap(); + + const dedupedFailures = dedupedFailuresMap.values(); + + this._aggregateFailures(dedupedFailures); + } + + _aggregateFailures(failures) { + for (const failure of failures) { + const errorCode = failure.errorCode; + this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1; + } + } + + /** + * If there are failures that should be tracked, call the given trackDecryptionFailure + * function with the number of failures that should be tracked. + */ + trackFailures() { + for (const errorCode of Object.keys(this.failureCounts)) { + if (this.failureCounts[errorCode] > 0) { + const trackedErrorCode = this._mapErrorCode ? this._mapErrorCode(errorCode) : errorCode; + + this._trackDecryptionFailure(this.failureCounts[errorCode], trackedErrorCode); + this.failureCounts[errorCode] = 0; + } + } + } +} diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js new file mode 100644 index 0000000000..ea7eeba756 --- /dev/null +++ b/src/FromWidgetPostMessageApi.js @@ -0,0 +1,219 @@ +/* +Copyright 2018 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 URL from 'url'; +import dis from './dispatcher'; +import IntegrationManager from './IntegrationManager'; +import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; +import ActiveWidgetStore from './stores/ActiveWidgetStore'; + +const WIDGET_API_VERSION = '0.0.1'; // Current API version +const SUPPORTED_WIDGET_API_VERSIONS = [ + '0.0.1', +]; +const INBOUND_API_NAME = 'fromWidget'; + +// Listen for and handle incomming requests using the 'fromWidget' postMessage +// API and initiate responses +export default class FromWidgetPostMessageApi { + constructor() { + this.widgetMessagingEndpoints = []; + + this.start = this.start.bind(this); + this.stop = this.stop.bind(this); + this.onPostMessage = this.onPostMessage.bind(this); + } + + start() { + window.addEventListener('message', this.onPostMessage); + } + + stop() { + window.removeEventListener('message', this.onPostMessage); + } + + /** + * Register a widget endpoint for trusted postMessage communication + * @param {string} widgetId Unique widget identifier + * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host) + */ + addEndpoint(widgetId, endpointUrl) { + const u = URL.parse(endpointUrl); + if (!u || !u.protocol || !u.host) { + console.warn('Add FromWidgetPostMessageApi endpoint - Invalid origin:', endpointUrl); + return; + } + + const origin = u.protocol + '//' + u.host; + const endpoint = new WidgetMessagingEndpoint(widgetId, origin); + if (this.widgetMessagingEndpoints.some(function(ep) { + return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl); + })) { + // Message endpoint already registered + console.warn('Add FromWidgetPostMessageApi - Endpoint already registered'); + return; + } else { + console.warn(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint); + this.widgetMessagingEndpoints.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 + */ + removeEndpoint(widgetId, endpointUrl) { + const u = URL.parse(endpointUrl); + if (!u || !u.protocol || !u.host) { + console.warn('Remove widget messaging endpoint - Invalid origin'); + return; + } + + const origin = u.protocol + '//' + u.host; + if (this.widgetMessagingEndpoints && this.widgetMessagingEndpoints.length > 0) { + const length = this.widgetMessagingEndpoints.length; + this.widgetMessagingEndpoints = this.widgetMessagingEndpoints. + filter(function(endpoint) { + return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin); + }); + return (length > this.widgetMessagingEndpoints.length); + } + return false; + } + + /** + * Handle widget postMessage events + * Messages are only handled where a valid, registered messaging endpoints + * @param {Event} event Event to handle + * @return {undefined} + */ + onPostMessage(event) { + if (!event.origin) { // Handle chrome + event.origin = event.originalEvent.origin; + } + + // Event origin is empty string if undefined + if ( + event.origin.length === 0 || + !this.trustedEndpoint(event.origin) || + event.data.api !== INBOUND_API_NAME || + !event.data.widgetId + ) { + return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise + } + + // Although the requestId is required, we don't use it. We'll be nice and process the message + // if the property is missing, but with a warning for widget developers. + if (!event.data.requestId) { + console.warn("fromWidget action '" + event.data.action + "' does not have a requestId"); + } + + const action = event.data.action; + const widgetId = event.data.widgetId; + if (action === 'content_loaded') { + console.warn('Widget reported content loaded for', widgetId); + dis.dispatch({ + action: 'widget_content_loaded', + widgetId: widgetId, + }); + this.sendResponse(event, {success: true}); + } else if (action === 'supported_api_versions') { + this.sendResponse(event, { + api: INBOUND_API_NAME, + supported_versions: SUPPORTED_WIDGET_API_VERSIONS, + }); + } else if (action === 'api_version') { + this.sendResponse(event, { + api: INBOUND_API_NAME, + version: WIDGET_API_VERSION, + }); + } else if (action === 'm.sticker') { + // console.warn('Got sticker message from widget', widgetId); + // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually + const data = event.data.data || event.data.widgetData; + dis.dispatch({action: 'm.sticker', data: data, widgetId: event.data.widgetId}); + } else if (action === 'integration_manager_open') { + // Close the stickerpicker + dis.dispatch({action: 'stickerpicker_close'}); + // Open the integration manager + // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually + const data = event.data.data || event.data.widgetData; + const integType = (data && data.integType) ? data.integType : null; + const integId = (data && data.integId) ? data.integId : null; + IntegrationManager.open(integType, integId); + } else if (action === 'set_always_on_screen') { + // This is a new message: there is no reason to support the deprecated widgetData here + const data = event.data.data; + const val = data.value; + + if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) { + ActiveWidgetStore.setWidgetPersistence(widgetId, val); + } + } else { + console.warn('Widget postMessage event unhandled'); + this.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 + */ + trustedEndpoint(origin) { + if (!origin) { + return false; + } + + return this.widgetMessagingEndpoints.some((endpoint) => { + // TODO / FIXME -- Should this also check the widgetId? + 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 + */ + 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) + */ + 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); + } +} diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js new file mode 100644 index 0000000000..532ee23c25 --- /dev/null +++ b/src/GroupAddressPicker.js @@ -0,0 +1,158 @@ +/* +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 Modal from './Modal'; +import sdk from './'; +import MultiInviter from './utils/MultiInviter'; +import { _t } from './languageHandler'; +import MatrixClientPeg from './MatrixClientPeg'; +import GroupStore from './stores/GroupStore'; + +export function showGroupInviteDialog(groupId) { + return new Promise((resolve, reject) => { + 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).then(resolve, reject); + }, + }); + }); +} + +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); + + return 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 errorList = []; + return Promise.all(addrs.map((addr) => { + return GroupStore + .addRoomToGroup(groupId, 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 671f9b4955..b6a2bd0acb 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd +Copyright 2017, 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,14 +17,17 @@ 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'); +import ReplyThread from "./components/views/elements/ReplyThread"; + +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'; import MatrixClientPeg from './MatrixClientPeg'; +import url from 'url'; emojione.imagePathSVG = 'emojione/svg/'; // Store PNG path for displaying many flags at once (for increased performance over SVG) @@ -32,10 +35,20 @@ emojione.imagePathPNG = 'emojione/png/'; // Use SVGs for emojis emojione.imageType = 'svg'; -const SIMPLE_EMOJI_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/; +// 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}$/; +const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; + /* * Return true if the given string contains emoji * Uses a much, much simpler regex than emojione's so will give false @@ -44,16 +57,13 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; * unicodeToImage uses this function. */ export function containsEmoji(str) { - return SIMPLE_EMOJI_PATTERN.test(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) { - // fast path - if (!containsEmoji(str)) return str; - +function unicodeToImage(str) { let replaceWith, unicode, alt, short, fname; const mappedUnicode = emojione.mapUnicodeToShort(); @@ -61,8 +71,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]; @@ -103,7 +112,6 @@ export function charactersToImageNode(alt, useSvg, ...unicode) { />; } - export function processHtmlForSending(html: string): string { const contentDiv = document.createElement('div'); contentDiv.innerHTML = html; @@ -121,13 +129,6 @@ export function processHtmlForSending(html: string): string { if (i !== contentDiv.children.length - 1) { contentHTML += '
'; } - } else if (element.tagName.toLowerCase() === 'pre') { - // Replace "
\n" with "\n" within `
` tags because the 
is - // redundant. This is a workaround for a bug in draft-js-export-html: - // https://github.com/sstur/draft-js-export-html/issues/62 - contentHTML += '
' +
-                element.innerHTML.replace(/
\n/g, '\n').trim() + - '
'; } else { const temp = document.createElement('div'); temp.appendChild(element.cloneNode(true)); @@ -148,6 +149,118 @@ export function sanitizedHtmlNode(insaneHtml) { return
; } +/** + * Tests if a URL from an untrusted source may be safely put into the DOM + * The biggest threat here is javascript: URIs. + * Note that the HTML sanitiser library has its own internal logic for + * doing this, to which we pass the same list of schemes. This is used in + * other places we need to sanitise URLs. + * @return true if permitted, otherwise false + */ +export function isUrlPermitted(inputUrl) { + try { + const parsed = url.parse(inputUrl); + if (!parsed.protocol) return false; + // URL parser protocol includes the trailing colon + return PERMITTED_URL_SCHEMES.includes(parsed.protocol.slice(0, -1)); + } catch (e) { + return false; + } +} + +const transformTags = { // custom to matrix + // add blank targets to all hyperlinks except vector URLs + 'a': function(tagName, attribs) { + if (attribs.href) { + attribs.target = '_blank'; // by default + + 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 { + m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN); + if (m) { + const entity = m[1]; + switch (entity[0]) { + case '@': + attribs.href = '#/user/' + entity; + break; + case '+': + attribs.href = '#/group/' + entity; + break; + case '#': + case '!': + attribs.href = '#/room/' + entity; + break; + } + delete attribs.target; + } + } + } + attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ + return { tagName, 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 || !attribs.src.startsWith('mxc://')) { + return { tagName, attribs: {}}; + } + attribs.src = MatrixClientPeg.get().mxcUrlToHttp( + attribs.src, + attribs.width || 800, + attribs.height || 600, + ); + return { tagName, attribs }; + }, + 'code': function(tagName, attribs) { + if (typeof attribs.class !== 'undefined') { + // Filter out all classes other than ones starting with language- for syntax highlighting. + const classes = attribs.class.split(/\s+/).filter(function(cl) { + return cl.startsWith('language-'); + }); + attribs.class = classes.join(' '); + } + return { tagName, attribs }; + }, + '*': function(tagName, attribs) { + // Delete any style previously assigned, style is an allowedTag for font and span + // because attributes are stripped after transforming + delete attribs.style; + + // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS + // equivalents + const customCSSMapper = { + 'data-mx-color': 'color', + 'data-mx-bg-color': 'background-color', + // $customAttributeKey: $cssAttributeKey + }; + + let style = ""; + Object.keys(customCSSMapper).forEach((customAttributeKey) => { + const cssAttributeKey = customCSSMapper[customAttributeKey]; + const customAttributeValue = attribs[customAttributeKey]; + if (customAttributeValue && + typeof customAttributeValue === 'string' && + COLOR_REGEX.test(customAttributeValue) + ) { + style += cssAttributeKey + ":" + customAttributeValue + ";"; + delete attribs[customAttributeKey]; + } + }); + + if (style) { + attribs.style = style; + } + + return { tagName, attribs }; + }, +}; + const sanitizeHtmlParams = { allowedTags: [ 'font', // custom to matrix for IRC-style font coloring @@ -168,100 +281,17 @@ 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: PERMITTED_URL_SCHEMES, allowProtocolRelative: false, + transformTags, +}; - transformTags: { // custom to matrix - // add blank targets to all hyperlinks except vector URLs - 'a': function(tagName, attribs) { - if (attribs.href) { - attribs.target = '_blank'; // by default - - var 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 { - m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN); - if (m) { - var entity = m[1]; - if (entity[0] === '@') { - attribs.href = '#/user/' + entity; - } - else if (entity[0] === '#' || entity[0] === '!') { - attribs.href = '#/room/' + entity; - } - delete attribs.target; - } - } - } - attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ - 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://')) { - return { tagName, attribs: {}}; - } - attribs.src = MatrixClientPeg.get().mxcUrlToHttp( - attribs.src, - attribs.width || 800, - attribs.height || 600, - ); - return { tagName: tagName, attribs: attribs }; - }, - '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) { - return cl.startsWith('language-'); - }); - attribs.class = classes.join(' '); - } - return { - tagName: tagName, - attribs: attribs, - }; - }, - '*': function(tagName, attribs) { - // Delete any style previously assigned, style is an allowedTag for font and span - // because attributes are stripped after transforming - delete attribs.style; - - // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS - // equivalents - const customCSSMapper = { - 'data-mx-color': 'color', - 'data-mx-bg-color': 'background-color', - // $customAttributeKey: $cssAttributeKey - }; - - let style = ""; - Object.keys(customCSSMapper).forEach((customAttributeKey) => { - const cssAttributeKey = customCSSMapper[customAttributeKey]; - const customAttributeValue = attribs[customAttributeKey]; - if (customAttributeValue && - typeof customAttributeValue === 'string' && - COLOR_REGEX.test(customAttributeValue) - ) { - style += cssAttributeKey + ":" + customAttributeValue + ";"; - delete attribs[customAttributeKey]; - } - }); - - if (style) { - attribs.style = style; - } - - return { tagName: tagName, attribs: attribs }; - }, - }, +// this is the same as the above except with less rewriting +const composerSanitizeHtmlParams = Object.assign({}, sanitizeHtmlParams); +composerSanitizeHtmlParams.transformTags = { + 'code': transformTags['code'], + '*': transformTags['*'], }; class BaseHighlighter { @@ -282,11 +312,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) { @@ -296,7 +326,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; @@ -314,8 +344,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)]; } @@ -336,7 +365,7 @@ class HtmlHighlighter extends BaseHighlighter { return snippet; } - var span = "" + let span = "" + snippet + ""; if (this.highlightLink) { @@ -361,15 +390,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; @@ -377,54 +406,101 @@ class TextHighlighter extends BaseHighlighter { } - /* turn a matrix event body into html - * - * content: 'content' of the MatrixEvent - * - * highlights: optional list of words to highlight, ordered by longest word first - * - * opts.highlightLink: optional href to add to highlighted words - */ -export function bodyToHtml(content, highlights, opts) { - opts = opts || {}; +/* turn a matrix event body into html + * + * content: 'content' of the MatrixEvent + * + * 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. + * opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing + * opts.returnString: return an HTML string rather than JSX elements + * opts.emojiOne: optional param to do emojiOne (default true) + * opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer + */ +export function bodyToHtml(content, highlights, opts={}) { + const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body; - var isHtml = (content.format === "org.matrix.custom.html"); - let body = isHtml ? content.formatted_body : escape(content.body); + const doEmojiOne = opts.emojiOne === undefined ? true : opts.emojiOne; + let bodyHasEmoji = false; - var safeBody; + let sanitizeParams = sanitizeHtmlParams; + if (opts.forComposerQuote) { + sanitizeParams = composerSanitizeHtmlParams; + } + + let strippedBody; + let safeBody; + let isDisplayedWithHtml; // 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) { - return sanitizeHtml(highlight, sanitizeHtmlParams); + const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink); + const safeHighlights = highlights.map(function(highlight) { + return sanitizeHtml(highlight, sanitizeParams); }); - // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure. - sanitizeHtmlParams.textFilter = function(safeText) { + // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. + sanitizeParams.textFilter = function(safeText) { return highlighter.applyHighlights(safeText, safeHighlights).join(''); }; } - safeBody = sanitizeHtml(body, sanitizeHtmlParams); - safeBody = unicodeToImage(safeBody); - } - finally { - delete sanitizeHtmlParams.textFilter; + + let formattedBody = content.formatted_body; + if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody); + strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body; + + if (doEmojiOne) { + bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body); + } + + // Only generate safeBody if the message was sent as org.matrix.custom.html + if (isHtmlMessage) { + isDisplayedWithHtml = true; + safeBody = sanitizeHtml(formattedBody, sanitizeParams); + } else { + // ... or if there are emoji, which we insert as HTML alongside the + // escaped plaintext body. + if (bodyHasEmoji) { + isDisplayedWithHtml = true; + safeBody = sanitizeHtml(escape(strippedBody), sanitizeParams); + } + } + + // An HTML message with emoji + // or a plaintext message with emoji that was escaped and sanitized into + // HTML. + if (bodyHasEmoji) { + safeBody = unicodeToImage(safeBody); + } + } finally { + delete sanitizeParams.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; + if (opts.returnString) { + return isDisplayedWithHtml ? safeBody : strippedBody; + } + + let emojiBody = false; + if (!opts.disableBigEmoji && bodyHasEmoji) { + EMOJI_REGEX.lastIndex = 0; + const contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : ''; + const match = EMOJI_REGEX.exec(contentBodyTrimmed); + emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length; + } const className = classNames({ 'mx_EventTile_body': true, 'mx_EventTile_bigEmoji': emojiBody, - 'markdown-body': isHtml, + 'markdown-body': isHtmlMessage, }); - return ; + + return isDisplayedWithHtml ? + : + { strippedBody }; } export function emojifyText(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/IntegrationManager.js b/src/IntegrationManager.js new file mode 100644 index 0000000000..eb45a1f425 --- /dev/null +++ b/src/IntegrationManager.js @@ -0,0 +1,73 @@ +/* +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 './index'; +import SdkConfig from './SdkConfig'; +import ScalarMessaging from './ScalarMessaging'; +import ScalarAuthClient from './ScalarAuthClient'; +import RoomViewStore from './stores/RoomViewStore'; + +if (!global.mxIntegrationManager) { + global.mxIntegrationManager = {}; +} + +export default class IntegrationManager { + static _init() { + if (!global.mxIntegrationManager.client || !global.mxIntegrationManager.connected) { + if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { + ScalarMessaging.startListening(); + global.mxIntegrationManager.client = new ScalarAuthClient(); + + return global.mxIntegrationManager.client.connect().then(() => { + global.mxIntegrationManager.connected = true; + }).catch((e) => { + console.error("Failed to connect to integrations server", e); + global.mxIntegrationManager.error = e; + }); + } else { + console.error('Invalid integration manager config', SdkConfig.get()); + } + } + } + + /** + * Launch the integrations manager on the stickers integration page + * @param {string} integName integration / widget type + * @param {string} integId integration / widget ID + * @param {function} onFinished Callback to invoke on integration manager close + */ + static async open(integName, integId, onFinished) { + await IntegrationManager._init(); + const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + if (global.mxIntegrationManager.error || + !(global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials())) { + console.error("Scalar error", global.mxIntegrationManager); + return; + } + const integType = 'type_' + integName; + const src = (global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials()) ? + global.mxIntegrationManager.client.getScalarInterfaceUrlForRoom( + {roomId: RoomViewStore.getRoomId()}, + integType, + integId, + ) : + null; + Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { + src: src, + onFinished: onFinished, + }, "mx_IntegrationsManager"); + } +} diff --git a/src/KeyCode.js b/src/Keyboard.js similarity index 66% rename from src/KeyCode.js rename to src/Keyboard.js index ec5595b71b..bf83a1a05f 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,21 @@ 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; + } +} + +export function isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) { + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + if (isMac) { + return ev.metaKey && !ev.altKey && !ev.ctrlKey; + } else { + return ev.ctrlKey && !ev.altKey && !ev.metaKey; + } +} diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 4d8911f7a6..795489c1fa 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd +Copyright 2018 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. @@ -29,6 +30,7 @@ import DMRoomMap from './utils/DMRoomMap'; import RtsClient from './RtsClient'; import Modal from './Modal'; import sdk from './index'; +import ActiveWidgetStore from './stores/ActiveWidgetStore'; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -64,33 +66,33 @@ import sdk from './index'; * Resolves to `true` if we ended up starting a session, or `false` if we * failed. */ -export function loadSession(opts) { - const fragmentQueryParams = opts.fragmentQueryParams || {}; - let enableGuest = opts.enableGuest || false; - const guestHsUrl = opts.guestHsUrl; - const guestIsUrl = opts.guestIsUrl; - const defaultDeviceDisplayName = opts.defaultDeviceDisplayName; +export async function loadSession(opts) { + try { + let enableGuest = opts.enableGuest || false; + const guestHsUrl = opts.guestHsUrl; + const guestIsUrl = opts.guestIsUrl; + const fragmentQueryParams = opts.fragmentQueryParams || {}; + const defaultDeviceDisplayName = opts.defaultDeviceDisplayName; - if (!guestHsUrl) { - console.warn("Cannot enable guest access: can't determine HS URL to use"); - enableGuest = false; - } + if (!guestHsUrl) { + console.warn("Cannot enable guest access: can't determine HS URL to use"); + enableGuest = false; + } - if (enableGuest && - fragmentQueryParams.guest_user_id && - fragmentQueryParams.guest_access_token - ) { - console.log("Using guest access credentials"); - return _doSetLoggedIn({ - userId: fragmentQueryParams.guest_user_id, - accessToken: fragmentQueryParams.guest_access_token, - homeserverUrl: guestHsUrl, - identityServerUrl: guestIsUrl, - guest: true, - }, true).then(() => true); - } - - return _restoreFromLocalStorage().then((success) => { + if (enableGuest && + fragmentQueryParams.guest_user_id && + fragmentQueryParams.guest_access_token + ) { + console.log("Using guest access credentials"); + return _doSetLoggedIn({ + userId: fragmentQueryParams.guest_user_id, + accessToken: fragmentQueryParams.guest_access_token, + homeserverUrl: guestHsUrl, + identityServerUrl: guestIsUrl, + guest: true, + }, true).then(() => true); + } + const success = await _restoreFromLocalStorage(); if (success) { return true; } @@ -101,7 +103,9 @@ export function loadSession(opts) { // fall back to login screen return false; - }); + } catch (e) { + return _handleLoadSessionFailure(e); + } } /** @@ -195,9 +199,9 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { // The plan is to gradually move the localStorage access done here into // SessionStore to avoid bugs where the view becomes out-of-sync with // localStorage (e.g. teamToken, isGuest etc.) -function _restoreFromLocalStorage() { +async function _restoreFromLocalStorage() { if (!localStorage) { - return Promise.resolve(false); + return false; } const hsUrl = localStorage.getItem("mx_hs_url"); const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org'; @@ -215,26 +219,23 @@ function _restoreFromLocalStorage() { if (accessToken && userId && hsUrl) { console.log(`Restoring session for ${userId}`); - try { - return _doSetLoggedIn({ - userId: userId, - deviceId: deviceId, - accessToken: accessToken, - homeserverUrl: hsUrl, - identityServerUrl: isUrl, - guest: isGuest, - }, false).then(() => true); - } catch (e) { - return _handleRestoreFailure(e); - } + await _doSetLoggedIn({ + userId: userId, + deviceId: deviceId, + accessToken: accessToken, + homeserverUrl: hsUrl, + identityServerUrl: isUrl, + guest: isGuest, + }, false); + return true; } else { console.log("No previous session found."); - return Promise.resolve(false); + return false; } } -function _handleRestoreFailure(e) { - console.log("Unable to restore session", e); +function _handleLoadSessionFailure(e) { + console.log("Unable to load session", e); const def = Promise.defer(); const SessionRestoreErrorDialog = @@ -255,7 +256,7 @@ function _handleRestoreFailure(e) { } // try, try again - return _restoreFromLocalStorage(); + return loadSession(); }); } @@ -362,7 +363,7 @@ async function _doSetLoggedIn(credentials, clearStorage) { dis.dispatch({action: 'on_logged_in', teamToken: teamToken}); }); - startMatrixClient(); + await startMatrixClient(); return MatrixClientPeg.get(); } @@ -385,10 +386,14 @@ function _persistCredentialsToLocalStorage(credentials) { console.log(`Session persisted for ${credentials.userId}`); } +let _isLoggingOut = false; + /** * 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 @@ -402,6 +407,7 @@ export function logout() { return; } + _isLoggingOut = true; MatrixClientPeg.get().logout().then(onLoggedOut, (err) => { // Just throwing an error here is going to be very unhelpful @@ -417,11 +423,15 @@ export function logout() { ).done(); } +export function isLoggingOut() { + return _isLoggingOut; +} + /** * Starts the matrix client and all other react-sdk services that * listen for events while a session is logged in. */ -function startMatrixClient() { +async function startMatrixClient() { console.log(`Lifecycle: Starting MatrixClient`); // dispatch this before starting the matrix client: it's used @@ -434,8 +444,13 @@ function startMatrixClient() { UserActivity.start(); Presence.start(); DMRoomMap.makeShared().start(); + ActiveWidgetStore.start(); - MatrixClientPeg.start(); + await 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'}); } /* @@ -443,6 +458,7 @@ function startMatrixClient() { * storage. Used after a session has been logged out. */ export function onLoggedOut() { + _isLoggingOut = false; stopMatrixClient(); _clearStorage().done(); dis.dispatch({action: 'on_logged_out'}); @@ -482,6 +498,7 @@ export function stopMatrixClient() { Notifier.stop(); UserActivity.stop(); Presence.stop(); + ActiveWidgetStore.stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); const cli = MatrixClientPeg.get(); if (cli) { 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 455d5e95bd..acfea52100 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -48,32 +48,13 @@ 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; } return par.firstChild != par.lastChild; } -import linkifyMatrix from './linkify-matrix'; -import * as linkify from 'linkifyjs'; -linkifyMatrix(linkify); - -// Thieved from draft-js-export-markdown -function escapeMarkdown(s) { - return s.replace(/[*_`]/g, '\\$&'); -} - -// Replace URLs, room aliases and user IDs with md-escaped URLs -function linkifyMarkdown(s) { - const links = linkify.find(s); - links.forEach((l) => { - // This may replace several instances of `l.value` at once, but that's OK - s = s.replace(l.value, escapeMarkdown(l.value)); - }); - return s; -} - /** * Class that wraps commonmark, adding the ability to see whether * a given message actually uses any markdown syntax or whether @@ -81,7 +62,7 @@ function linkifyMarkdown(s) { */ export default class Markdown { constructor(input) { - this.input = linkifyMarkdown(input); + this.input = input; const parser = new commonmark.Parser(); this.parsed = parser.parse(this.input); @@ -121,6 +102,16 @@ export default class Markdown { // (https://github.com/vector-im/riot-web/issues/3154) softbreak: '
', }); + + // Trying to strip out the wrapping

causes a lot more complication + // than it's worth, i think. For instance, this code will go and strip + // out any

tag (no matter where it is in the tree) which doesn't + // contain \n's. + // On the flip side,

s are quite opionated and restricted on where + // you can nest them. + // + // Let's try sending with

s anyway for now, though. + const real_paragraph = renderer.paragraph; renderer.paragraph = function(node, entering) { @@ -134,16 +125,21 @@ export default class Markdown { } }; + renderer.html_inline = html_if_tag_allowed; + renderer.html_block = function(node) { +/* // as with `paragraph`, we only insert line breaks // if there are multiple lines in the markdown. const isMultiLine = is_multi_line(node); - if (isMultiLine) this.cr(); +*/ html_if_tag_allowed.call(this, node); +/* if (isMultiLine) this.cr(); - } +*/ + }; return renderer.render(this.parsed); } @@ -152,7 +148,10 @@ export default class Markdown { * Render the markdown message to plain text. That is, essentially * just remove any backslashes escaping what would otherwise be * markdown syntax - * (to fix https://github.com/vector-im/riot-web/issues/2870) + * (to fix https://github.com/vector-im/riot-web/issues/2870). + * + * N.B. this does **NOT** render arbitrary MD to plain text - only MD + * which has no formatting. Otherwise it emits HTML(!). */ toPlaintext() { const renderer = new commonmark.HtmlRenderer({safe: false}); @@ -175,10 +174,11 @@ 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..f5872812de 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); @@ -94,11 +99,15 @@ class MatrixClientPeg { // the react sdk doesn't work without this, so don't allow opts.pendingEventOrdering = "detached"; + if (SettingsStore.isFeatureEnabled('feature_lazyloading')) { + opts.lazyLoadMembers = true; + } + 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,8 +115,11 @@ 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); + await this.get().startClient(opts); console.log(`MatrixClientPeg: MatrixClient started`); } @@ -136,13 +148,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 +166,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); @@ -165,4 +178,4 @@ class MatrixClientPeg { if (!global.mxMatrixClientPeg) { global.mxMatrixClientPeg = new MatrixClientPeg(); } -module.exports = global.mxMatrixClientPeg; +export default global.mxMatrixClientPeg; diff --git a/src/Modal.js b/src/Modal.js index 056b6d8bf2..06a96824a7 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -17,10 +17,12 @@ 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 PropTypes from 'prop-types'; import Analytics from './Analytics'; import sdk from './index'; +import dis from './dispatcher'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; @@ -33,7 +35,7 @@ const AsyncWrapper = React.createClass({ /** A function which takes a 'callback' argument which it will call * with the real component once it loads. */ - loader: React.PropTypes.func.isRequired, + loader: PropTypes.func.isRequired, }, getInitialState: function() { @@ -79,7 +81,11 @@ class ModalManager { constructor() { this._counter = 0; - /** list of the modals we have stacked up, with the most recent at [0] */ + // The modal to prioritise over all others. If this is set, only show + // this modal. Remove all other modals from the stack when this modal + // is closed. + this._priorityModal = null; + // A list of the modals we have stacked up, with the most recent at [0] this._modals = [ /* { elem: React component for this dialog @@ -103,18 +109,18 @@ class ModalManager { return container; } - createTrackedDialog(analyticsAction, analyticsInfo, Element, props, className) { + createTrackedDialog(analyticsAction, analyticsInfo, ...rest) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); - return this.createDialog(Element, props, className); + return this.createDialog(...rest); } - createDialog(Element, props, className) { - return this.createDialogAsync((cb) => {cb(Element);}, props, className); + createDialog(Element, ...rest) { + return this.createDialogAsync((cb) => {cb(Element);}, ...rest); } - createTrackedDialogAsync(analyticsAction, analyticsInfo, loader, props, className) { + createTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); - return this.createDialogAsync(loader, props, className); + return this.createDialogAsync(...rest); } /** @@ -135,20 +141,33 @@ class ModalManager { * component. (We will also pass an 'onFinished' property.) * * @param {String} className CSS class to apply to the modal wrapper + * + * @param {boolean} isPriorityModal if true, this modal will be displayed regardless + * of other modals that are currently in the stack. + * Also, when closed, all modals will be removed + * from the stack. */ - createDialogAsync(loader, props, className) { - var self = this; + createDialogAsync(loader, props, className, isPriorityModal) { + 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); } + + if (self._priorityModal === modal) { + self._priorityModal = null; + + // XXX: This is destructive + self._modals = []; + } + self._reRender(); }; @@ -160,12 +179,17 @@ 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; - this._modals.unshift(modal); + if (isPriorityModal) { + // XXX: This is destructive + this._priorityModal = modal; + } else { + this._modals.unshift(modal); + } this._reRender(); return {close: closeDialog}; @@ -186,18 +210,30 @@ class ModalManager { } _reRender() { - if (this._modals.length == 0) { + if (this._modals.length == 0 && !this._priorityModal) { + // If there is no modal to render, make all of Riot available + // to screen reader users again + dis.dispatch({ + action: 'aria_unhide_main_app', + }); ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); return; } - var modal = this._modals[0]; - var dialog = ( -

+ // Hide the content outside the modal to screen reader users + // so they won't be able to navigate into it and act on it using + // screen reader specific features + dis.dispatch({ + action: 'aria_hide_main_app', + }); + + const modal = this._priorityModal ? this._priorityModal : this._modals[0]; + const dialog = ( +
- {modal.elem} + { modal.elem }
-
+
); diff --git a/src/Notifier.js b/src/Notifier.js index 155564dcdf..80e8be1084 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -25,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: @@ -80,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 @@ -133,14 +135,16 @@ const Notifier = { const plaf = PlatformPeg.get(); if (!plaf) return; + // Dev note: We don't set the "notificationsEnabled" setting to true here because it is a + // calculated value. It is determined based upon whether or not the master rule is enabled + // and other flags. Setting it here would cause a circular reference. + Analytics.trackEvent('Notifier', 'Set Enabled', enable); // 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 +152,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,55 +164,42 @@ const Notifier = { return; } - if (global.localStorage) { - global.localStorage.setItem('notifications_enabled', 'true'); - } - if (callback) callback(); dis.dispatch({ action: "notifier_enabled", value: true, }); }); - // clear the notifications_hidden flag, so that if notifications are - // 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, }); } + // set the notifications_hidden flag, as the user has knowingly interacted + // with the setting we shouldn't nag them any further + this.setToolbarHidden(true); }, 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 +216,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; @@ -266,6 +256,10 @@ const Notifier = { }, onEventDecrypted: function(ev) { + // 'decrypted' means the decryption process has finished: it may have failed, + // in which case it might decrypt soon if the keys arrive + if (ev.isDecryptionFailure()) return; + const idx = this.pendingEncryptedEventIds.indexOf(ev.getId()); if (idx === -1) return; @@ -303,7 +297,7 @@ const Notifier = { this._playAudioNotification(ev, room); } } - } + }, }; if (!global.mxNotifier) { diff --git a/src/Presence.js b/src/Presence.js index c45d571217..9367fe35cd 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 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,12 +15,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 { @@ -31,7 +32,7 @@ class Presence { this.running = true; if (undefined === this.state) { this._resetTimer(); - this.dispatcherRef = dis.register(this._onUserActivity.bind(this)); + this.dispatcherRef = dis.register(this._onAction.bind(this)); } } @@ -71,14 +72,14 @@ class Presence { if (!this.running) { return; } - var old_state = this.state; + const old_state = this.state; this.state = newState; if (MatrixClientPeg.get().isGuest()) { return; // don't try to set presence when a guest; it won't work. } - var self = this; + const self = this; MatrixClientPeg.get().setPresence(this.state).done(function() { console.log("Presence: %s", newState); }, function(err) { @@ -95,8 +96,10 @@ class Presence { this.setState("unavailable"); } - _onUserActivity() { - this._resetTimer(); + _onAction(payload) { + if (payload.action === "user_activity") { + this._resetTimer(); + } } /** @@ -104,7 +107,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/Registration.js b/src/Registration.js new file mode 100644 index 0000000000..070178fecb --- /dev/null +++ b/src/Registration.js @@ -0,0 +1,92 @@ +/* +Copyright 2018 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. +*/ + +/** + * Utility code for registering with a homeserver + * Note that this is currently *not* used by the actual + * registration code. + */ + +import dis from './dispatcher'; +import sdk from './index'; +import MatrixClientPeg from './MatrixClientPeg'; +import Modal from './Modal'; +import { _t } from './languageHandler'; + +/** + * Starts either the ILAG or full registration flow, depending + * on what the HS supports + * + * @param {object} options + * @param {bool} options.go_home_on_cancel If true, goes to + * the hame page if the user cancels the action + */ +export async function startAnyRegistrationFlow(options) { + if (options === undefined) options = {}; + const flows = await _getRegistrationFlows(); + // look for an ILAG compatible flow. We define this as one + // which has only dummy or recaptcha flows. In practice it + // would support any stage InteractiveAuth supports, just not + // ones like email & msisdn which require the user to supply + // the relevant details in advance. We err on the side of + // caution though. + const hasIlagFlow = flows.some((flow) => { + return flow.stages.every((stage) => { + return ['m.login.dummy', 'm.login.recaptcha'].includes(stage); + }); + }); + + if (hasIlagFlow) { + dis.dispatch({ + action: 'view_set_mxid', + go_home_on_cancel: options.go_home_on_cancel, + }); + } else { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createTrackedDialog('Registration required', '', QuestionDialog, { + title: _t("Registration Required"), + description: _t("You need to register to do this. Would you like to register now?"), + button: _t("Register"), + onFinished: (proceed) => { + if (proceed) { + dis.dispatch({action: 'start_registration'}); + } else if (options.go_home_on_cancel) { + dis.dispatch({action: 'view_home_page'}); + } + }, + }); + } +} + +async function _getRegistrationFlows() { + try { + await MatrixClientPeg.get().register( + null, + null, + undefined, + {}, + {}, + ); + console.log("Register request succeeded when it should have returned 401!"); + } catch (e) { + if (e.httpStatus === 401) { + return e.data.flows; + } + throw e; + } + throw new Error("Register request succeeded when it should have returned 401!"); +} + 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..3e8f834da6 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -1,301 +1,40 @@ -import React from 'react'; -import { - Editor, - EditorState, - Modifier, - ContentState, - ContentBlock, - convertFromHTML, - DefaultDraftBlockRenderMap, - DefaultDraftInlineStyle, - CompositeDecorator, - SelectionState, - Entity, -} from 'draft-js'; -import * as sdk from './index'; +/* +Copyright 2015 - 2017 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018 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 * as emojione from 'emojione'; -import {stateToHTML} from 'draft-js-export-html'; -import {SelectionRange} from "./autocomplete/Autocompleter"; -import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown'; -const MARKDOWN_REGEX = { - LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g, - ITALIC: /([\*_])([\w\s]+?)\1/g, - BOLD: /([\*_])\1([\w\s]+?)\1\1/g, - HR: /(\n|^)((-|\*|_) *){3,}(\n|$)/g, - CODE: /`[^`]*`/g, - STRIKETHROUGH: /~{2}[^~]*~{2}/g, -}; -const USERNAME_REGEX = /@\S+:\S+/g; -const ROOM_REGEX = /#\S+:\S+/g; -const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g'); +export function unicodeToEmojiUri(str) { + const mappedUnicode = emojione.mapUnicodeToShort(); -const ZWS_CODE = 8203; -const ZWS = String.fromCharCode(ZWS_CODE); // zero width space -export function stateToMarkdown(state) { - return __stateToMarkdown(state) - .replace( - ZWS, // draft-js-export-markdown adds these - ''); // this is *not* a zero width space, trust me :) -} - -export const contentStateToHTML = (contentState: ContentState) => { - return stateToHTML(contentState, { - inlineStyles: { - UNDERLINE: { - element: 'u' - } - } - }); -}; - -export function htmlToContentState(html: string): ContentState { - const blockArray = convertFromHTML(html).contentBlocks; - return ContentState.createFromBlockArray(blockArray); -} - -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(); - } - - str = str.replace(emojione.regUnicode, function(unicodeChar) { - if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) { - // if the unicodeChar doesnt exist just return the entire match + // remove any zero width joiners/spaces used in conjugate emojis as the emojione URIs don't contain them + return str.replace(emojione.regUnicode, function(unicodeChar) { + if ((typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap))) { + // if the unicodeChar doesn't exist just return the entire match return unicodeChar; } else { // get the unicode codepoint from the actual char - unicode = emojione.jsEscapeMap[unicodeChar]; - return emojione.imagePathSVG+unicode+'.svg'+emojione.cacheBustParam; + const unicode = emojione.jsEscapeMap[unicodeChar]; + + const short = mappedUnicode[unicode]; + const fname = emojione.emojioneList[short].fname; + + return emojione.imagePathSVG+fname+'.svg'+emojione.cacheBustParam; } }); - - return str; -} - -/** - * Utility function that looks for regex matches within a ContentBlock and invokes {callback} with (start, end) - * From https://facebook.github.io/draft-js/docs/advanced-topics-decorators.html - */ -function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: number, end: number) => any) { - const text = contentBlock.getText(); - let matchArr, start; - while ((matchArr = regex.exec(text)) !== null) { - start = matchArr.index; - callback(start, start + matchArr[0].length); - } -} - -// Workaround for https://github.com/facebook/draft-js/issues/414 -let 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 = { - display: 'inline-block', - width: '1em', - maxHeight: '1em', - background: `url(${uri})`, - backgroundSize: 'contain', - backgroundPosition: 'center center', - overflow: 'hidden', - }; - return ({props.children}); - }, -}; - -/** - * Returns a composite decorator which has access to provided scope. - */ -export function getScopedRTDecorators(scope: any): CompositeDecorator { - return [emojiDecorator]; -} - -export function getScopedMDDecorators(scope: any): CompositeDecorator { - let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map( - (style) => ({ - strategy: (contentState, contentBlock, callback) => { - return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback); - }, - component: (props) => ( - - {props.children} - - ) - })); - - markdownDecorators.push({ - strategy: (contentState, contentBlock, callback) => { - return findWithRegex(MARKDOWN_REGEX.LINK, contentBlock, callback); - }, - component: (props) => ( - - {props.children} - - ) - }); - // markdownDecorators.push(emojiDecorator); - // TODO Consider renabling "syntax highlighting" when we can do it properly - return [emojiDecorator]; -} - -/** - * Passes rangeToReplace to modifyFn and replaces it in contentState with the result. - */ -export function modifyText(contentState: ContentState, rangeToReplace: SelectionState, - modifyFn: (text: string) => string, inlineStyle, entityKey): ContentState { - let getText = (key) => contentState.getBlockForKey(key).getText(), - startKey = rangeToReplace.getStartKey(), - startOffset = rangeToReplace.getStartOffset(), - endKey = rangeToReplace.getEndKey(), - endOffset = rangeToReplace.getEndOffset(), - text = ""; - - - for (let currentKey = startKey; - currentKey && currentKey !== endKey; - currentKey = contentState.getKeyAfter(currentKey)) { - let blockText = getText(currentKey); - text += blockText.substring(startOffset, blockText.length); - - // from now on, we'll take whole blocks - startOffset = 0; - } - - // add remaining part of last block - text += getText(endKey).substring(startOffset, endOffset); - - return Modifier.replaceText(contentState, rangeToReplace, modifyFn(text), inlineStyle, entityKey); -} - -/** - * Computes the plaintext offsets of the given SelectionState. - * Note that this inherently means we make assumptions about what that means (no separator between ContentBlocks, etc) - * Used by autocomplete to show completions when the current selection lies within, or at the edges of a command. - */ -export function selectionStateToTextOffsets(selectionState: SelectionState, - contentBlocks: Array): {start: number, end: number} { - let offset = 0, start = 0, end = 0; - for (let block of contentBlocks) { - if (selectionState.getStartKey() === block.getKey()) { - start = offset + selectionState.getStartOffset(); - } - if (selectionState.getEndKey() === block.getKey()) { - end = offset + selectionState.getEndOffset(); - break; - } - offset += block.getLength(); - } - - return { - start, - end, - }; -} - -export function textOffsetsToSelectionState({start, end}: SelectionRange, - contentBlocks: Array): SelectionState { - let selectionState = SelectionState.createEmpty(); - // Subtract block lengths from `start` and `end` until they are less than the current - // block length (accounting for the NL at the end of each block). Set them to -1 to - // indicate that the corresponding selection state has been determined. - for (const block of contentBlocks) { - const blockLength = block.getLength(); - // -1 indicating that the position start position has been found - if (start !== -1) { - if (start < blockLength + 1) { - selectionState = selectionState.merge({ - anchorKey: block.getKey(), - anchorOffset: start, - }); - start = -1; // selection state for the start calculated - } else { - start -= blockLength + 1; // +1 to account for newline between blocks - } - } - // -1 indicating that the position end position has been found - if (end !== -1) { - if (end < blockLength + 1) { - selectionState = selectionState.merge({ - focusKey: block.getKey(), - focusOffset: end, - }); - end = -1; // selection state for the end calculated - } else { - end -= blockLength + 1; // +1 to account for newline between blocks - } - } - } - return selectionState; -} - -// modified version of https://github.com/draft-js-plugins/draft-js-plugins/blob/master/draft-js-emoji-plugin/src/modifiers/attachImmutableEntitiesToEmojis.js -export function attachImmutableEntitiesToEmoji(editorState: EditorState): EditorState { - const contentState = editorState.getCurrentContent(); - const blocks = contentState.getBlockMap(); - let newContentState = contentState; - - blocks.forEach((block) => { - const plainText = block.getText(); - - const addEntityToEmoji = (start, end) => { - const existingEntityKey = block.getEntityAt(start); - if (existingEntityKey) { - // avoid manipulation in case the emoji already has an entity - const entity = newContentState.getEntity(existingEntityKey); - if (entity && entity.get('type') === 'emoji') { - return; - } - } - - const selection = SelectionState.createEmpty(block.getKey()) - .set('anchorOffset', start) - .set('focusOffset', end); - const emojiText = plainText.substring(start, end); - newContentState = newContentState.createEntity( - 'emoji', 'IMMUTABLE', { emojiUnicode: emojiText } - ); - const entityKey = newContentState.getLastCreatedEntityKey(); - newContentState = Modifier.replaceText( - newContentState, - selection, - emojiText, - null, - entityKey, - ); - }; - - findWithRegex(EMOJI_REGEX, block, addEntityToEmoji); - }); - - if (!newContentState.equals(contentState)) { - const oldSelection = editorState.getSelection(); - editorState = EditorState.push( - editorState, - newContentState, - 'convert-to-immutable-emojis', - ); - // this is somewhat of a hack, we're undoing selection changes caused above - // it would be better not to make those changes in the first place - editorState = EditorState.forceSelection(editorState, oldSelection); - } - - return editorState; -} - -export function hasMultiLineSelection(editorState: EditorState): boolean { - const selectionState = editorState.getSelection(); - const anchorKey = selectionState.getAnchorKey(); - const currentContent = editorState.getCurrentContent(); - const currentContentBlock = currentContent.getBlockForKey(anchorKey); - const start = selectionState.getStartOffset(); - const end = selectionState.getEndOffset(); - const selectedText = currentContentBlock.getText().slice(start, end); - return selectedText.includes('\n'); } 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 68% rename from src/Invite.js rename to src/RoomInvite.js index 9be3da53e4..a96d1b2f6b 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,19 +52,20 @@ 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"), + validAddressTypes: ['mx-user-id', 'email'], button: _t("Start Chat"), onFinished: _onStartChatFinished, }); } 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,9 +82,41 @@ function _onStartChatFinished(shouldInvite, addrs) { const addrTexts = addrs.map((addr) => addr.address); if (_isDmChat(addrTexts)) { + 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 if (addrTexts.length === 1) { // 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"), @@ -127,7 +162,7 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) { } function _isDmChat(addrTexts) { - if (addrTexts.length === 1 && getAddressType(addrTexts[0]) === 'mx') { + if (addrTexts.length === 1 && getAddressType(addrTexts[0]) === 'mx-user-id') { return true; } else { return false; @@ -153,3 +188,15 @@ function _showAnyInviteErrors(addrs, room) { return addrs; } +function _getDirectMessageRooms(addr) { + const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); + const dmRooms = dmRoomMap.getDMRoomsForUserId(addr); + const rooms = dmRooms.filter((dmRoom) => { + const room = MatrixClientPeg.get().getRoom(dmRoom); + if (room) { + return room.getMyMembership() === 'join'; + } + }); + return rooms; +} + diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index 5cc078dc59..91e49fe09b 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -34,7 +34,14 @@ export function getRoomNotifsState(roomId) { } // for everything else, look at the room rule. - const roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId); + let roomRule = null; + try { + roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId); + } catch (err) { + // Possible that the client doesn't have pushRules yet. If so, it + // hasn't started eiher, so indicate that this room is not notifying. + return null; + } // XXX: We have to assume the default is to notify for all messages // (in particular this will be 'wrong' for one to one rooms because @@ -130,6 +137,11 @@ function setRoomNotifsStateUnmuted(roomId, newState) { } function findOverrideMuteRule(roomId) { + if (!MatrixClientPeg.get().pushRules || + !MatrixClientPeg.get().pushRules['global'] || + !MatrixClientPeg.get().pushRules['global'].override) { + return null; + } for (const rule of MatrixClientPeg.get().pushRules['global'].override) { if (isRuleForRoom(roomId, rule)) { if (isMuteRule(rule) && rule.enabled) { diff --git a/src/Rooms.js b/src/Rooms.js index 2e3f4457f0..e24b8316b3 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -31,47 +31,68 @@ export function getDisplayAliasForRoom(room) { * If the room contains only two members including the logged-in user, * return the other one. Otherwise, return null. */ -export function getOnlyOtherMember(room, me) { - const joinedMembers = room.getJoinedMembers(); +export function getOnlyOtherMember(room, myUserId) { - if (joinedMembers.length === 2) { - return joinedMembers.filter(function(m) { - return m.userId !== me.userId; + if (room.currentState.getJoinedMemberCount() === 2) { + return room.getJoinedMembers().filter(function(m) { + return m.userId !== myUserId; })[0]; } return null; } -export function isConfCallRoom(room, me, conferenceHandler) { +function _isConfCallRoom(room, myUserId, conferenceHandler) { if (!conferenceHandler) return false; - if (me.membership != "join") { + const myMembership = room.getMyMembership(); + if (myMembership != "join") { return false; } - const otherMember = getOnlyOtherMember(room, me); - if (otherMember === null) { + const otherMember = getOnlyOtherMember(room, myUserId); + if (!otherMember) { return false; } if (conferenceHandler.isConferenceUser(otherMember.userId)) { return true; } + + return false; } -export function looksLikeDirectMessageRoom(room, me) { - if (me.membership == "join" || me.membership === "ban" || - (me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) - { +// Cache whether a room is a conference call. Assumes that rooms will always +// either will or will not be a conference call room. +const isConfCallRoomCache = { + // $roomId: bool +}; + +export function isConfCallRoom(room, myUserId, conferenceHandler) { + if (isConfCallRoomCache[room.roomId] !== undefined) { + return isConfCallRoomCache[room.roomId]; + } + + const result = _isConfCallRoom(room, myUserId, conferenceHandler); + + isConfCallRoomCache[room.roomId] = result; + + return result; +} + +export function looksLikeDirectMessageRoom(room, myUserId) { + const myMembership = room.getMyMembership(); + const me = room.getMember(myUserId); + + if (myMembership == "join" || myMembership === "ban" || (me && me.isKicked())) { // Used to split rooms via tags const tagNames = Object.keys(room.tags); // Used for 1:1 direct chats - const members = room.currentState.getMembers(); - // Show 1:1 chats in seperate "Direct Messages" section as long as they haven't // been moved to a different tag section - if (members.length === 2 && !tagNames.length) { + const totalMemberCount = room.currentState.getJoinedMemberCount() + + room.currentState.getInvitedMemberCount(); + if (totalMemberCount === 2 && !tagNames.length) { return true; } } @@ -81,10 +102,10 @@ export function looksLikeDirectMessageRoom(room, me) { export function guessAndSetDMRoom(room, isDirect) { let newTarget; if (isDirect) { - const guessedTarget = guessDMRoomTarget( - room, room.getMember(MatrixClientPeg.get().credentials.userId), + const guessedUserId = guessDMRoomTargetId( + room, MatrixClientPeg.get().getUserId() ); - newTarget = guessedTarget.userId; + newTarget = guessedUserId; } else { newTarget = null; } @@ -140,15 +161,15 @@ export function setDMRoom(roomId, userId) { * Given a room, estimate which of its members is likely to * be the target if the room were a DM room and return that user. */ -export function guessDMRoomTarget(room, me) { +function guessDMRoomTargetId(room, myUserId) { let oldestTs; let oldestUser; // Pick the joined user who's been here longest (and isn't us), for (const user of room.getJoinedMembers()) { - if (user.userId == me.userId) continue; + if (user.userId == myUserId) continue; - if (oldestTs === undefined || user.events.member.getTs() < oldestTs) { + if (oldestTs === undefined || (user.events.member && user.events.member.getTs() < oldestTs)) { oldestUser = user; oldestTs = user.events.member.getTs(); } @@ -157,14 +178,14 @@ export function guessDMRoomTarget(room, me) { // if there are no joined members other than us, use the oldest member for (const user of room.currentState.getMembers()) { - if (user.userId == me.userId) continue; + if (user.userId == myUserId) continue; - if (oldestTs === undefined || user.events.member.getTs() < oldestTs) { + if (oldestTs === undefined || (user.events.member && user.events.member.getTs() < oldestTs)) { oldestUser = user; oldestTs = user.events.member.getTs(); } } - if (oldestUser === undefined) return me; + if (oldestUser === undefined) return myUserId; return oldestUser; } diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index 0b753cf3ab..c7e439bf2e 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,11 +39,53 @@ class ScalarAuthClient { // Returns a scalar_token string getScalarToken() { - var tok = window.localStorage.getItem("mx_scalar_token"); - if (tok) return Promise.resolve(tok); + const token = window.localStorage.getItem("mx_scalar_token"); - // No saved token, so do the dance to get one. First, we - // need an openid bearer token from the HS. + if (!token) { + return this.registerForToken(); + } else { + return this.validateToken(token).then(userId => { + const me = MatrixClientPeg.get().getUserId(); + if (userId !== me) { + throw new Error("Scalar token is owned by someone else: " + me); + } + return token; + }).catch(err => { + console.error(err); + + // Something went wrong - try to get a new token. + console.warn("Registering for new scalar token"); + return this.registerForToken(); + }) + } + } + + validateToken(token) { + let url = SdkConfig.get().integrations_rest_url + "/account"; + + const defer = Promise.defer(); + request({ + method: "GET", + uri: url, + qs: {scalar_token: token}, + json: true, + }, (err, response, body) => { + if (err) { + defer.reject(err); + } else if (response.statusCode / 100 !== 2) { + defer.reject({statusCode: response.statusCode}); + } else if (!body || !body.user_id) { + defer.reject(new Error("Missing user_id in response")); + } else { + defer.resolve(body.user_id); + } + }); + + return defer.promise; + } + + registerForToken() { + // Get openid bearer token from the HS as the first part of our dance return MatrixClientPeg.get().getOpenIdToken().then((token_object) => { // Now we can send that to scalar and exchange it for a scalar token return this.exchangeForScalarToken(token_object); @@ -53,9 +96,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 +119,78 @@ class ScalarAuthClient { return defer.promise; } - getScalarInterfaceUrlForRoom(roomId, screen, id) { - var url = SdkConfig.get().integrations_ui_url; + 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; + } + + /** + * Mark all assets associated with the specified widget as "disabled" in the + * integration manager database. + * This can be useful to temporarily prevent purchased assets from being displayed. + * @param {string} widgetType [description] + * @param {string} widgetId [description] + * @return {Promise} Resolves on completion + */ + disableWidgetAssets(widgetType, widgetId) { + let url = SdkConfig.get().integrations_rest_url + '/widgets/set_assets_state'; + url = this.getStarterLink(url); + return new Promise((resolve, reject) => { + request({ + method: 'GET', + uri: url, + json: true, + qs: { + 'widget_type': widgetType, + 'widget_id': widgetId, + 'state': 'disable', + }, + }, (err, response, body) => { + if (err) { + reject(err); + } else if (response.statusCode / 100 !== 2) { + reject({statusCode: response.statusCode}); + } else if (!body) { + reject(new Error("Failed to set widget assets state")); + } else { + resolve(); + } + }); + }); + } + + getScalarInterfaceUrlForRoom(room, screen, id) { + const roomId = room.roomId; + const roomName = room.name; + let url = SdkConfig.get().integrations_ui_url; url += "?scalar_token=" + encodeURIComponent(this.scalarToken); url += "&room_id=" + encodeURIComponent(roomId); + url += "&room_name=" + encodeURIComponent(roomName); + 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..fa7b8c5b76 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -1,6 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd +Copyright 2018 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. @@ -231,10 +232,12 @@ Example: } */ -const SdkConfig = require('./SdkConfig'); -const MatrixClientPeg = require("./MatrixClientPeg"); -const MatrixEvent = require("matrix-js-sdk").MatrixEvent; -const dis = require("./dispatcher"); +import SdkConfig from './SdkConfig'; +import MatrixClientPeg from './MatrixClientPeg'; +import { MatrixEvent } from 'matrix-js-sdk'; +import dis from './dispatcher'; +import WidgetUtils from './utils/WidgetUtils'; +import RoomViewStore from './stores/RoomViewStore'; import { _t } from './languageHandler'; function sendResponse(event, res) { @@ -291,12 +294,7 @@ function setWidget(event, roomId) { const widgetUrl = event.data.url; const widgetName = event.data.name; // optional const widgetData = event.data.data; // optional - - const client = MatrixClientPeg.get(); - if (!client) { - sendError(event, _t('You need to be logged in.')); - return; - } + const userWidget = event.data.userWidget; // both adding/removing widgets need these checks if (!widgetId || widgetUrl === undefined) { @@ -324,26 +322,57 @@ function setWidget(event, roomId) { } } - let content = { - type: widgetType, - url: widgetUrl, - name: widgetName, - data: widgetData, - }; - if (widgetUrl === null) { // widget is being deleted - content = {}; - } + if (userWidget) { + WidgetUtils.setUserWidget(widgetId, widgetType, widgetUrl, widgetName, widgetData).then(() => { + sendResponse(event, { + success: true, + }); - client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => { - sendResponse(event, { - success: true, + dis.dispatch({ action: "user_widget_updated" }); + }).catch((e) => { + sendError(event, _t('Unable to create widget.'), e); }); - }, (err) => { - sendError(event, _t('Failed to send request.'), err); - }); + } else { // Room widget + if (!roomId) { + sendError(event, _t('Missing roomId.'), null); + } + WidgetUtils.setRoomWidget(roomId, widgetId, widgetType, widgetUrl, widgetName, widgetData).then(() => { + sendResponse(event, { + success: true, + }); + }, (err) => { + sendError(event, _t('Failed to send request.'), err); + }); + } } function getWidgets(event, roomId) { + const client = MatrixClientPeg.get(); + if (!client) { + sendError(event, _t('You need to be logged in.')); + return; + } + let widgetStateEvents = []; + + if (roomId) { + const room = client.getRoom(roomId); + if (!room) { + sendError(event, _t('This room is not recognised.')); + return; + } + // XXX: This gets the raw event object (I think because we can't + // send the MatrixEvent over postMessage?) + widgetStateEvents = WidgetUtils.getRoomWidgets(room).map((ev) => ev.event); + } + + // Add user widgets (not linked to a specific room) + const userWidgets = WidgetUtils.getUserWidgetsArray(); + widgetStateEvents = widgetStateEvents.concat(userWidgets); + + sendResponse(event, widgetStateEvents); +} + +function getRoomEncState(event, roomId) { const client = MatrixClientPeg.get(); if (!client) { sendError(event, _t('You need to be logged in.')); @@ -354,16 +383,9 @@ function getWidgets(event, roomId) { sendError(event, _t('This room is not recognised.')); return; } - const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets"); - // Only return widgets which have required fields - let widgetStateEvents = []; - stateEvents.forEach((ev) => { - if (ev.getContent().type && ev.getContent().url) { - widgetStateEvents.push(ev.event); // return the raw event - } - }) + const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId); - sendResponse(event, widgetStateEvents); + sendResponse(event, roomIsEncrypted); } function setPlumbingState(event, roomId, status) { @@ -376,7 +398,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 +437,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(() => { @@ -458,7 +480,7 @@ function getMembershipCount(event, roomId) { sendError(event, _t('This room is not recognised.')); return; } - const count = room.getJoinedMembers().length; + const count = room.getJoinedMemberCount(); sendResponse(event, count); } @@ -475,18 +497,16 @@ function canSendEvent(event, roomId) { sendError(event, _t('This room is not recognised.')); return; } - const me = client.credentials.userId; - const member = room.getMember(me); - if (!member || member.membership !== "join") { + if (room.getMyMembership() !== "join") { sendError(event, _t('You are not in this room.')); return; } + const me = client.credentials.userId; let canSend = false; if (isState) { canSend = room.currentState.maySendStateEvent(evType, me); - } - else { + } else { canSend = room.currentState.maySendEvent(evType, me); } @@ -517,19 +537,6 @@ function returnStateEvent(event, roomId, eventType, stateKey) { sendResponse(event, stateEvent.getContent()); } -var currentRoomId = null; -var currentRoomAlias = null; - -// Listen for when a room is viewed -dis.register(onAction); -function onAction(payload) { - if (payload.action !== "view_room") { - return; - } - currentRoomId = payload.room_id; - currentRoomAlias = payload.room_alias; -} - const onMessage = function(event) { if (!event.origin) { // stupid chrome event.origin = event.originalEvent.origin; @@ -542,8 +549,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 } @@ -555,78 +570,80 @@ const onMessage = function(event) { const roomId = event.data.room_id; const userId = event.data.user_id; + if (!roomId) { - sendError(event, _t('Missing room_id in request')); - return; - } - let promise = Promise.resolve(currentRoomId); - if (!currentRoomId) { - if (!currentRoomAlias) { - sendError(event, _t('Must be viewing a room')); - return; - } - // no room ID but there is an alias, look it up. - console.log("Looking up alias " + currentRoomAlias); - promise = MatrixClientPeg.get().getRoomIdForAlias(currentRoomAlias).then((res) => { - return res.room_id; - }); - } - - promise.then((viewingRoomId) => { - if (roomId !== viewingRoomId) { - sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId})); - return; - } - - // These APIs don't require userId - if (event.data.action === "join_rules_state") { - getJoinRules(event, roomId); - return; - } else if (event.data.action === "set_plumbing_state") { - setPlumbingState(event, roomId, event.data.status); - return; - } else if (event.data.action === "get_membership_count") { - getMembershipCount(event, roomId); + // These APIs don't require roomId + // Get and set user widgets (not associated with a specific room) + // If roomId is specified, it must be validated, so room-based widgets agreed + // handled further down. + if (event.data.action === "get_widgets") { + getWidgets(event, null); return; } else if (event.data.action === "set_widget") { - setWidget(event, roomId); + setWidget(event, null); return; - } else if (event.data.action === "get_widgets") { - getWidgets(event, roomId); - return; - } else if (event.data.action === "can_send_event") { - canSendEvent(event, roomId); + } else { + sendError(event, _t('Missing room_id in request')); return; } + } - if (!userId) { - sendError(event, _t('Missing user_id in request')); - return; - } - switch (event.data.action) { - case "membership_state": - getMembershipState(event, roomId, userId); - break; - case "invite": - inviteUser(event, roomId, userId); - break; - case "bot_options": - botOptions(event, roomId, userId); - break; - case "set_bot_options": - setBotOptions(event, roomId, userId); - break; - case "set_bot_power": - setBotPower(event, roomId, userId, event.data.level); - break; - default: - console.warn("Unhandled postMessage event with action '" + event.data.action +"'"); - break; - } - }, (err) => { - console.error(err); - sendError(event, _t('Failed to lookup current room') + '.'); - }); + if (roomId !== RoomViewStore.getRoomId()) { + sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId})); + return; + } + + // Get and set room-based widgets + if (event.data.action === "get_widgets") { + getWidgets(event, roomId); + return; + } else if (event.data.action === "set_widget") { + setWidget(event, roomId); + return; + } + + // These APIs don't require userId + if (event.data.action === "join_rules_state") { + getJoinRules(event, roomId); + return; + } else if (event.data.action === "set_plumbing_state") { + setPlumbingState(event, roomId, event.data.status); + return; + } else if (event.data.action === "get_membership_count") { + getMembershipCount(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; + } + + if (!userId) { + sendError(event, _t('Missing user_id in request')); + return; + } + switch (event.data.action) { + case "membership_state": + getMembershipState(event, roomId, userId); + break; + case "invite": + inviteUser(event, roomId, userId); + break; + case "bot_options": + botOptions(event, roomId, userId); + break; + case "set_bot_options": + setBotOptions(event, roomId, userId); + break; + case "set_bot_power": + setBotPower(event, roomId, userId, event.data.level); + break; + default: + console.warn("Unhandled postMessage event with action '" + event.data.action +"'"); + break; + } }; let listenerCount = 0; @@ -647,7 +664,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/SlashCommands.js b/src/SlashCommands.js index e5378d4347..3a8e77293b 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 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,27 +15,32 @@ See the License for the specific language governing permissions and limitations under the License. */ -import MatrixClientPeg from "./MatrixClientPeg"; -import dis from "./dispatcher"; -import Tinter from "./Tinter"; + +import React from 'react'; +import MatrixClientPeg from './MatrixClientPeg'; +import dis from './dispatcher'; +import Tinter from './Tinter'; import sdk from './index'; -import { _t } from './languageHandler'; +import {_t, _td} from './languageHandler'; import Modal from './Modal'; +import SettingsStore, {SettingLevel} from './settings/SettingsStore'; class Command { - constructor(name, paramArgs, runFn) { - this.name = name; - this.paramArgs = paramArgs; + constructor({name, args='', description, runFn, hideCompletionAfterSpace=false}) { + this.command = '/' + name; + this.args = args; + this.description = description; this.runFn = runFn; + this.hideCompletionAfterSpace = hideCompletionAfterSpace; } getCommand() { - return "/" + this.name; + return this.command; } getCommandWithArgs() { - return this.getCommand() + " " + this.paramArgs; + return this.getCommand() + " " + this.args; } run(roomId, args) { @@ -46,16 +52,12 @@ class Command { } } -function reject(msg) { - return { - error: msg, - }; +function reject(error) { + return {error}; } function success(promise) { - return { - promise: promise, - }; + return {promise}; } /* Disable the "unexpected this" error for these commands - all of the run @@ -64,292 +66,423 @@ function success(promise) { /* eslint-disable babel/no-invalid-this */ -const commands = { - ddg: new Command("ddg", "", function(roomId, args) { - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - // TODO Don't explain this away, actually show a search UI here. - Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, { - title: _t('/ddg is not a command'), - description: _t('To use it, just wait for autocomplete results to load and tab through them.'), - }); - return success(); +export const CommandMap = { + ddg: new Command({ + name: 'ddg', + args: '', + description: _td('Searches DuckDuckGo for results'), + runFn: function(roomId, args) { + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + // TODO Don't explain this away, actually show a search UI here. + Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, { + title: _t('/ddg is not a command'), + description: _t('To use it, just wait for autocomplete results to load and tab through them.'), + }); + return success(); + }, + hideCompletionAfterSpace: true, }), - // Change your nickname - nick: new Command("nick", "", function(roomId, args) { - if (args) { - return success( - MatrixClientPeg.get().setDisplayName(args), - ); - } - return reject(this.getUsage()); - }), - - // Changes the colorscheme of your current room - tint: new Command("tint", " []", function(roomId, args) { - if (args) { - const matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); - if (matches) { - Tinter.tint(matches[1], matches[4]); - const colorScheme = {}; - colorScheme.primary_color = matches[1]; - if (matches[4]) { - colorScheme.secondary_color = matches[4]; - } - return success( - MatrixClientPeg.get().setRoomAccountData( - roomId, "org.matrix.room.color_scheme", colorScheme, - ), - ); + nick: new Command({ + name: 'nick', + args: '', + description: _td('Changes your display nickname'), + runFn: function(roomId, args) { + if (args) { + return success(MatrixClientPeg.get().setDisplayName(args)); } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, }), - // Change the room topic - topic: new Command("topic", "", function(roomId, args) { - if (args) { - return success( - MatrixClientPeg.get().setRoomTopic(roomId, args), - ); - } - return reject(this.getUsage()); - }), - - // Invite a user - invite: new Command("invite", "", function(roomId, args) { - if (args) { - const matches = args.match(/^(\S+)$/); - if (matches) { - return success( - MatrixClientPeg.get().invite(roomId, matches[1]), - ); - } - } - return reject(this.getUsage()); - }), - - // Join a room - join: new Command("join", "#alias:domain", function(roomId, args) { - if (args) { - const matches = args.match(/^(\S+)$/); - if (matches) { - let roomAlias = matches[1]; - if (roomAlias[0] !== '#') { - return reject(this.getUsage()); - } - if (!roomAlias.match(/:/)) { - roomAlias += ':' + MatrixClientPeg.get().getDomain(); - } - - dis.dispatch({ - action: 'view_room', - room_alias: roomAlias, - auto_join: true, - }); - - return success(); - } - } - return reject(this.getUsage()); - }), - - part: new Command("part", "[#alias:domain]", function(roomId, args) { - let targetRoomId; - if (args) { - const matches = args.match(/^(\S+)$/); - if (matches) { - let roomAlias = matches[1]; - if (roomAlias[0] !== '#') { - return reject(this.getUsage()); - } - if (!roomAlias.match(/:/)) { - roomAlias += ':' + MatrixClientPeg.get().getDomain(); - } - - // Try to find a room with this alias - const rooms = MatrixClientPeg.get().getRooms(); - for (let i = 0; i < rooms.length; i++) { - const aliasEvents = rooms[i].currentState.getStateEvents( - "m.room.aliases", - ); - for (let j = 0; j < aliasEvents.length; j++) { - const aliases = aliasEvents[j].getContent().aliases || []; - for (let k = 0; k < aliases.length; k++) { - if (aliases[k] === roomAlias) { - targetRoomId = rooms[i].roomId; - break; - } - } - if (targetRoomId) { break; } + tint: new Command({ + name: 'tint', + args: ' []', + description: _td('Changes colour scheme of current room'), + runFn: function(roomId, args) { + if (args) { + const matches = args.match(/^(#([\da-fA-F]{3}|[\da-fA-F]{6}))( +(#([\da-fA-F]{3}|[\da-fA-F]{6})))?$/); + if (matches) { + Tinter.tint(matches[1], matches[4]); + const colorScheme = {}; + colorScheme.primary_color = matches[1]; + if (matches[4]) { + colorScheme.secondary_color = matches[4]; + } else { + colorScheme.secondary_color = colorScheme.primary_color; } - if (targetRoomId) { break; } - } - if (!targetRoomId) { - return reject(_t("Unrecognised room alias:") + ' ' + roomAlias); + return success( + SettingsStore.setValue('roomColor', roomId, SettingLevel.ROOM_ACCOUNT, colorScheme), + ); } } - } - if (!targetRoomId) targetRoomId = roomId; - return success( - MatrixClientPeg.get().leave(targetRoomId).then( - function() { - dis.dispatch({action: 'view_next_room'}); - }, - ), - ); + return reject(this.getUsage()); + }, }), - // Kick a user from the room with an optional reason - kick: new Command("kick", " []", function(roomId, args) { - if (args) { - const matches = args.match(/^(\S+?)( +(.*))?$/); - if (matches) { - return success( - MatrixClientPeg.get().kick(roomId, matches[1], matches[3]), - ); + topic: new Command({ + name: 'topic', + args: '', + description: _td('Sets the room topic'), + runFn: function(roomId, args) { + if (args) { + return success(MatrixClientPeg.get().setRoomTopic(roomId, args)); } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, + }), + + invite: new Command({ + name: 'invite', + args: '', + description: _td('Invites user with given id to current room'), + runFn: function(roomId, args) { + if (args) { + const matches = args.match(/^(\S+)$/); + if (matches) { + return success(MatrixClientPeg.get().invite(roomId, matches[1])); + } + } + return reject(this.getUsage()); + }, + }), + + join: new Command({ + name: 'join', + args: '', + description: _td('Joins room with given alias'), + runFn: function(roomId, args) { + if (args) { + const matches = args.match(/^(\S+)$/); + if (matches) { + let roomAlias = matches[1]; + if (roomAlias[0] !== '#') return reject(this.getUsage()); + + if (!roomAlias.includes(':')) { + roomAlias += ':' + MatrixClientPeg.get().getDomain(); + } + + dis.dispatch({ + action: 'view_room', + room_alias: roomAlias, + auto_join: true, + }); + + return success(); + } + } + return reject(this.getUsage()); + }, + }), + + part: new Command({ + name: 'part', + args: '[]', + description: _td('Leave room'), + runFn: function(roomId, args) { + const cli = MatrixClientPeg.get(); + + let targetRoomId; + if (args) { + const matches = args.match(/^(\S+)$/); + if (matches) { + let roomAlias = matches[1]; + if (roomAlias[0] !== '#') return reject(this.getUsage()); + + if (!roomAlias.includes(':')) { + roomAlias += ':' + cli.getDomain(); + } + + // Try to find a room with this alias + const rooms = cli.getRooms(); + for (let i = 0; i < rooms.length; i++) { + const aliasEvents = rooms[i].currentState.getStateEvents('m.room.aliases'); + for (let j = 0; j < aliasEvents.length; j++) { + const aliases = aliasEvents[j].getContent().aliases || []; + for (let k = 0; k < aliases.length; k++) { + if (aliases[k] === roomAlias) { + targetRoomId = rooms[i].roomId; + break; + } + } + if (targetRoomId) break; + } + if (targetRoomId) break; + } + if (!targetRoomId) return reject(_t('Unrecognised room alias:') + ' ' + roomAlias); + } + } + + if (!targetRoomId) targetRoomId = roomId; + return success( + cli.leave(targetRoomId).then(function() { + dis.dispatch({action: 'view_next_room'}); + }), + ); + }, + }), + + kick: new Command({ + name: 'kick', + args: ' [reason]', + description: _td('Kicks user with given id'), + runFn: function(roomId, args) { + if (args) { + const matches = args.match(/^(\S+?)( +(.*))?$/); + if (matches) { + return success(MatrixClientPeg.get().kick(roomId, matches[1], matches[3])); + } + } + return reject(this.getUsage()); + }, }), // Ban a user from the room with an optional reason - ban: new Command("ban", " []", function(roomId, args) { - if (args) { - const matches = args.match(/^(\S+?)( +(.*))?$/); - if (matches) { - return success( - MatrixClientPeg.get().ban(roomId, matches[1], matches[3]), - ); + ban: new Command({ + name: 'ban', + args: ' [reason]', + description: _td('Bans user with given id'), + runFn: function(roomId, args) { + if (args) { + const matches = args.match(/^(\S+?)( +(.*))?$/); + if (matches) { + return success(MatrixClientPeg.get().ban(roomId, matches[1], matches[3])); + } } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, }), - // Unban a user from the room - unban: new Command("unban", "", function(roomId, args) { - if (args) { - const matches = args.match(/^(\S+)$/); - if (matches) { - // Reset the user membership to "leave" to unban him - return success( - MatrixClientPeg.get().unban(roomId, matches[1]), - ); + // Unban a user from ythe room + unban: new Command({ + name: 'unban', + args: '', + description: _td('Unbans user with given id'), + runFn: function(roomId, args) { + if (args) { + const matches = args.match(/^(\S+)$/); + if (matches) { + // Reset the user membership to "leave" to unban him + return success(MatrixClientPeg.get().unban(roomId, matches[1])); + } } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, + }), + + ignore: new Command({ + name: 'ignore', + args: '', + description: _td('Ignores a user, hiding their messages from you'), + runFn: function(roomId, args) { + if (args) { + const cli = MatrixClientPeg.get(); + + const matches = args.match(/^(\S+)$/); + if (matches) { + const userId = matches[1]; + const ignoredUsers = cli.getIgnoredUsers(); + ignoredUsers.push(userId); // de-duped internally in the js-sdk + return success( + cli.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}) }

+
, + hasCancelButton: false, + }); + }), + ); + } + } + return reject(this.getUsage()); + }, + }), + + unignore: new Command({ + name: 'unignore', + args: '', + description: _td('Stops ignoring a user, showing their messages going forward'), + runFn: function(roomId, args) { + if (args) { + const cli = MatrixClientPeg.get(); + + const matches = args.match(/^(\S+)$/); + if (matches) { + const userId = matches[1]; + const ignoredUsers = cli.getIgnoredUsers(); + const index = ignoredUsers.indexOf(userId); + if (index !== -1) ignoredUsers.splice(index, 1); + return success( + cli.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}) }

+
, + hasCancelButton: false, + }); + }), + ); + } + } + return reject(this.getUsage()); + }, }), // Define the power level of a user - op: new Command("op", " []", function(roomId, args) { - if (args) { - const matches = args.match(/^(\S+?)( +(\d+))?$/); - let powerLevel = 50; // default power level for op - if (matches) { - const userId = matches[1]; - if (matches.length === 4 && undefined !== matches[3]) { - powerLevel = parseInt(matches[3]); - } - if (!isNaN(powerLevel)) { - const room = MatrixClientPeg.get().getRoom(roomId); - if (!room) { - return reject("Bad room ID: " + roomId); + op: new Command({ + name: 'op', + args: ' []', + description: _td('Define the power level of a user'), + runFn: function(roomId, args) { + if (args) { + const matches = args.match(/^(\S+?)( +(-?\d+))?$/); + let powerLevel = 50; // default power level for op + if (matches) { + const userId = matches[1]; + if (matches.length === 4 && undefined !== matches[3]) { + powerLevel = parseInt(matches[3]); + } + if (!isNaN(powerLevel)) { + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(roomId); + if (!room) return reject('Bad room ID: ' + roomId); + + const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', ''); + return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent)); } - const powerLevelEvent = room.currentState.getStateEvents( - "m.room.power_levels", "", - ); - return success( - MatrixClientPeg.get().setPowerLevel( - roomId, userId, powerLevel, powerLevelEvent, - ), - ); } } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, }), // Reset the power level of a user - deop: new Command("deop", "", function(roomId, args) { - if (args) { - const matches = args.match(/^(\S+)$/); - if (matches) { - const room = MatrixClientPeg.get().getRoom(roomId); - if (!room) { - return reject("Bad room ID: " + roomId); - } + deop: new Command({ + name: 'deop', + args: '', + description: _td('Deops user with given id'), + runFn: function(roomId, args) { + if (args) { + const matches = args.match(/^(\S+)$/); + if (matches) { + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(roomId); + if (!room) return reject('Bad room ID: ' + roomId); - const powerLevelEvent = room.currentState.getStateEvents( - "m.room.power_levels", "", - ); - return success( - MatrixClientPeg.get().setPowerLevel( - roomId, args, undefined, powerLevelEvent, - ), - ); + const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', ''); + return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent)); + } } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, + }), + + devtools: new Command({ + name: 'devtools', + description: _td('Opens the Developer Tools dialog'), + runFn: 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) { - const matches = args.match(/^(\S+) +(\S+) +(\S+)$/); - if (matches) { - const userId = matches[1]; - const deviceId = matches[2]; - const fingerprint = matches[3]; + verify: new Command({ + name: 'verify', + args: ' ', + description: _td('Verifies a user, device, and pubkey tuple'), + runFn: function(roomId, args) { + if (args) { + const matches = args.match(/^(\S+) +(\S+) +(\S+)$/); + if (matches) { + const cli = MatrixClientPeg.get(); - return success( - // Promise.resolve to handle transition from static result to promise; can be removed - // in future - Promise.resolve(MatrixClientPeg.get().getStoredDevice(userId, deviceId)).then((device) => { - if (!device) { - throw new Error(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`); - } + const userId = matches[1]; + const deviceId = matches[2]; + const fingerprint = matches[3]; - if (device.isVerified()) { - if (device.getFingerprint() === fingerprint) { - throw new Error(_t(`Device already verified!`)); - } else { - throw new Error(_t(`WARNING: Device already verified, but keys do NOT MATCH!`)); + return success( + // Promise.resolve to handle transition from static result to promise; can be removed + // in future + Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => { + if (!device) { + throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`); } - } - if (device.getFingerprint() !== fingerprint) { - const fprint = device.getFingerprint(); - throw new Error( - _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + - ' %(deviceId)s is "%(fprint)s" which does not match the provided key' + - ' "%(fingerprint)s". This could mean your communications are being intercepted!', - {deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint})); - } + if (device.isVerified()) { + if (device.getFingerprint() === fingerprint) { + throw new Error(_t('Device already verified!')); + } else { + throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!')); + } + } - return MatrixClientPeg.get().setDeviceVerified(userId, deviceId, true); - }).then(() => { - // Tell the user we verified everything - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, { - title: _t("Verified key"), - description: ( -
-

- { - _t("The signing key you provided matches the signing key you received " + - "from %(userId)s's device %(deviceId)s. Device marked as verified.", - {userId: userId, deviceId: deviceId}) - } -

-
- ), - hasCancelButton: false, - }); - }), - ); + if (device.getFingerprint() !== fingerprint) { + const fprint = device.getFingerprint(); + throw new Error( + _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + + ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + + '"%(fingerprint)s". This could mean your communications are being intercepted!', + { + fprint, + userId, + deviceId, + fingerprint, + })); + } + + return cli.setDeviceVerified(userId, deviceId, true); + }).then(() => { + // Tell the user we verified everything + const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); + Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, { + title: _t('Verified key'), + description:
+

+ { + _t('The signing key you provided matches the signing key you received ' + + 'from %(userId)s\'s device %(deviceId)s. Device marked as verified.', + {userId, deviceId}) + } +

+
, + hasCancelButton: false, + }); + }), + ); + } } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, + }), + + // Command definitions for autocompletion ONLY: + + // /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes + me: new Command({ + name: 'me', + args: '', + description: _td('Displays action'), + hideCompletionAfterSpace: true, + }), + + discardsession: new Command({ + name: 'discardsession', + description: _td('Forces the current outbound group session in an encrypted room to be discarded'), + runFn: function(roomId) { + try { + MatrixClientPeg.get().forceDiscardSession(roomId); + } catch (e) { + return reject(e.message); + } + return success(); + }, }), }; /* eslint-enable babel/no-invalid-this */ @@ -358,52 +491,43 @@ const commands = { // helpful aliases const aliases = { j: "join", + newballsplease: "discardsession", }; -module.exports = { - /** - * Process the given text for /commands and perform them. - * @param {string} roomId The room in which the command was performed. - * @param {string} input The raw text input by the user. - * @return {Object|null} An object with the property 'error' if there was an error - * processing the command, or 'promise' if a request was sent out. - * Returns null if the input didn't match a command. - */ - processInput: function(roomId, input) { - // trim any trailing whitespace, as it can confuse the parser for - // IRC-style commands - input = input.replace(/\s+$/, ""); - if (input[0] === "/" && input[1] !== "/") { - const bits = input.match(/^(\S+?)( +((.|\n)*))?$/); - let cmd; - let args; - if (bits) { - cmd = bits[1].substring(1).toLowerCase(); - args = bits[3]; - } else { - cmd = input; - } - if (cmd === "me") return null; - if (aliases[cmd]) { - cmd = aliases[cmd]; - } - if (commands[cmd]) { - return commands[cmd].run(roomId, args); - } else { - return reject(_t("Unrecognised command:") + ' ' + input); - } - } - return null; // not a command - }, - getCommandList: function() { - // Return all the commands plus /me and /markdown which aren't handled like normal commands - const cmds = Object.keys(commands).sort().map(function(cmdKey) { - return commands[cmdKey]; - }); - cmds.push(new Command("me", "", function() {})); - cmds.push(new Command("markdown", "", function() {})); +/** + * Process the given text for /commands and perform them. + * @param {string} roomId The room in which the command was performed. + * @param {string} input The raw text input by the user. + * @return {Object|null} An object with the property 'error' if there was an error + * processing the command, or 'promise' if a request was sent out. + * Returns null if the input didn't match a command. + */ +export function processCommandInput(roomId, input) { + // trim any trailing whitespace, as it can confuse the parser for + // IRC-style commands + input = input.replace(/\s+$/, ''); + if (input[0] !== '/') return null; // not a command - return cmds; - }, -}; + const bits = input.match(/^(\S+?)( +((.|\n)*))?$/); + let cmd; + let args; + if (bits) { + cmd = bits[1].substring(1).toLowerCase(); + args = bits[3]; + } else { + cmd = input; + } + + if (aliases[cmd]) { + cmd = aliases[cmd]; + } + if (CommandMap[cmd]) { + // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` + if (!CommandMap[cmd].runFn) return null; + + return CommandMap[cmd].run(roomId, args); + } else { + return reject(_t('Unrecognised command:') + ' ' + input); + } +} diff --git a/src/TextForEvent.js b/src/TextForEvent.js index d39ebd6c0a..91ae5d0d70 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -13,56 +13,66 @@ 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('%(oldDisplayName)s changed their display name to %(displayName)s.', { + oldDisplayName: prevContent.displayname, + displayName: content.displayname, + }); + } else if (!prevContent.displayname && content.displayname) { + return _t('%(senderName)s set their display name to %(displayName)s.', { + senderName: ev.getSender(), + 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,65 +81,119 @@ 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 textForServerACLEvent(ev) { + const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); + const prevContent = ev.getPrevContent(); + const changes = []; + const current = ev.getContent(); + const prev = { + deny: Array.isArray(prevContent.deny) ? prevContent.deny : [], + allow: Array.isArray(prevContent.allow) ? prevContent.allow : [], + allow_ip_literals: !(prevContent.allow_ip_literals === false), + }; + let text = ""; + if (prev.deny.length === 0 && prev.allow.length === 0) { + text = `${senderDisplayName} set server ACLs for this room: `; + } else { + text = `${senderDisplayName} changed the server ACLs for this room: `; + } + + if (!Array.isArray(current.allow)) { + current.allow = []; + } + /* If we know for sure everyone is banned, don't bother showing the diff view */ + if (current.allow.length === 0) { + return text + "🎉 All servers are banned from participating! This room can no longer be used."; + } + + if (!Array.isArray(current.deny)) { + current.deny = []; + } + + const bannedServers = current.deny.filter((srv) => typeof(srv) === 'string' && !prev.deny.includes(srv)); + const unbannedServers = prev.deny.filter((srv) => typeof(srv) === 'string' && !current.deny.includes(srv)); + const allowedServers = current.allow.filter((srv) => typeof(srv) === 'string' && !prev.allow.includes(srv)); + const unallowedServers = prev.allow.filter((srv) => typeof(srv) === 'string' && !current.allow.includes(srv)); + + if (bannedServers.length > 0) { + changes.push(`Servers matching ${bannedServers.join(", ")} are now banned.`); + } + + if (unbannedServers.length > 0) { + changes.push(`Servers matching ${unbannedServers.join(", ")} were removed from the ban list.`); + } + + if (allowedServers.length > 0) { + changes.push(`Servers matching ${allowedServers.join(", ")} are now allowed.`); + } + + if (unallowedServers.length > 0) { + changes.push(`Servers matching ${unallowedServers.join(", ")} were removed from the allowed list.`); + } + + if (prev.allow_ip_literals !== current.allow_ip_literals) { + const allowban = current.allow_ip_literals ? "allowed" : "banned"; + changes.push(`Participating from a server using an IP literal hostname is now ${allowban}.`); + } + + return text + changes.join(" "); } 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; } @@ -179,18 +243,18 @@ function textForRoomAliasesEvent(ev) { } 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") { @@ -203,48 +267,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 @@ -255,18 +323,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 @@ -275,11 +343,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), + }), ); } }); @@ -287,11 +355,16 @@ function textForPowerEvent(event) { return ''; } return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', { - senderName: senderName, + 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.getSender(); const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent(); @@ -322,27 +395,32 @@ function textForWidgetEvent(event) { } } -var handlers = { +const handlers = { 'm.room.message': textForMessageEvent, + 'm.call.invite': textForCallInviteEvent, + 'm.call.answer': textForCallAnswerEvent, + 'm.call.hangup': textForCallHangupEvent, +}; + +const stateHandlers = { 'm.room.aliases': textForRoomAliasesEvent, - '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.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, + 'm.room.server_acl': textForServerACLEvent, '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..d24a4c3e74 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,282 @@ 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) { + 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]; + try { + 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 + + // stylesheets we don't have permission to access (eg. ones from extensions) have a null + // href and will throw exceptions if we try to access their rules. + 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, + }); + } + } + } + } + } catch (e) { + // Catch any random exceptions that happen here: all sorts of things can go + // wrong with this (nulls, SecurityErrors) and mostly it's for other + // stylesheets that we don't want to proces anyway. We should not propagate an + // exception out since this will cause the app to fail to start. + console.log("Failed to calculate CSS fixups for a stylesheet: " + ss.href, e); + } + } + 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 +434,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/ToWidgetPostMessageApi.js b/src/ToWidgetPostMessageApi.js new file mode 100644 index 0000000000..def4af56ae --- /dev/null +++ b/src/ToWidgetPostMessageApi.js @@ -0,0 +1,86 @@ +/* +Copyright 2018 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 Promise from "bluebird"; + +// const OUTBOUND_API_NAME = 'toWidget'; + +// Initiate requests using the "toWidget" postMessage API and handle responses +// NOTE: ToWidgetPostMessageApi only handles message events with a data payload with a +// response field +export default class ToWidgetPostMessageApi { + constructor(timeoutMs) { + this._timeoutMs = timeoutMs || 5000; // default to 5s timer + this._counter = 0; + this._requestMap = { + // $ID: {resolve, reject} + }; + this.start = this.start.bind(this); + this.stop = this.stop.bind(this); + this.onPostMessage = this.onPostMessage.bind(this); + } + + start() { + window.addEventListener('message', this.onPostMessage); + } + + stop() { + window.removeEventListener('message', this.onPostMessage); + } + + onPostMessage(ev) { + // THIS IS ALL UNSAFE EXECUTION. + // We do not verify who the sender of `ev` is! + const payload = ev.data; + // NOTE: Workaround for running in a mobile WebView where a + // postMessage immediately triggers this callback even though it is + // not the response. + if (payload.response === undefined) { + return; + } + const promise = this._requestMap[payload.requestId]; + if (!promise) { + return; + } + delete this._requestMap[payload.requestId]; + promise.resolve(payload); + } + + // Initiate outbound requests (toWidget) + exec(action, targetWindow, targetOrigin) { + targetWindow = targetWindow || window.parent; // default to parent window + targetOrigin = targetOrigin || "*"; + this._counter += 1; + action.requestId = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter; + + return new Promise((resolve, reject) => { + this._requestMap[action.requestId] = {resolve, reject}; + targetWindow.postMessage(action, targetOrigin); + + if (this._timeoutMs > 0) { + setTimeout(() => { + if (!this._requestMap[action.requestId]) { + return; + } + console.error("postMessage request timed out. Sent object: " + JSON.stringify(action), + this._requestMap); + this._requestMap[action.requestId].reject(new Error("Timed out")); + delete this._requestMap[action.requestId]; + }, this._timeoutMs); + } + }); + } +} 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..55e60f2a9a 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 = { /** @@ -29,22 +28,24 @@ module.exports = { return false; } else if (ev.getType() == 'm.room.member') { return false; + } else if (ev.getType() == 'm.room.third_party_invite') { + return false; } else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') { return false; } 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 +55,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 +65,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 +85,5 @@ module.exports = { // is unread on the theory that false positives are better than // false negatives here. return true; - } + }, }; diff --git a/src/UserAddress.js b/src/UserAddress.js index 9eee48629d..e7501a0d91 100644 --- a/src/UserAddress.js +++ b/src/UserAddress.js @@ -16,11 +16,12 @@ limitations under the License. const emailRegex = /^\S+@\S+\.\S+$/; -const mxidRegex = /^@\S+:\S+$/; +const mxUserIdRegex = /^@\S+:\S+$/; +const mxRoomIdRegex = /^!\S+:\S+$/; import PropTypes from 'prop-types'; export const addressTypes = [ - 'mx', 'email', + 'mx-user-id', 'mx-room-id', 'email', ]; // PropType definition for an object describing @@ -41,13 +42,16 @@ export const UserAddressType = PropTypes.shape({ export function getAddressType(inputText) { const isEmailAddress = emailRegex.test(inputText); - const isMatrixId = mxidRegex.test(inputText); + const isUserId = mxUserIdRegex.test(inputText); + const isRoomId = mxRoomIdRegex.test(inputText); // sanity check the input for user IDs if (isEmailAddress) { return 'email'; - } else if (isMatrixId) { - return 'mx'; + } else if (isUserId) { + return 'mx-user-id'; + } else if (isRoomId) { + return 'mx-room-id'; } else { return null; } diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 68a1ba229f..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/VectorConferenceHandler.js b/src/VectorConferenceHandler.js new file mode 100644 index 0000000000..c53a01d464 --- /dev/null +++ b/src/VectorConferenceHandler.js @@ -0,0 +1,138 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +"use strict"; + +import Promise from 'bluebird'; +var Matrix = require("matrix-js-sdk"); +var Room = Matrix.Room; +var CallHandler = require('./CallHandler'); + +// FIXME: this is Riot (Vector) specific code, but will be removed shortly when +// we switch over to jitsi entirely for video conferencing. + +// FIXME: This currently forces Vector to try to hit the matrix.org AS for conferencing. +// This is bad because it prevents people running their own ASes from being used. +// This isn't permanent and will be customisable in the future: see the proposal +// at docs/conferencing.md for more info. +var USER_PREFIX = "fs_"; +var DOMAIN = "matrix.org"; + +function ConferenceCall(matrixClient, groupChatRoomId) { + this.client = matrixClient; + this.groupRoomId = groupChatRoomId; + this.confUserId = module.exports.getConferenceUserIdForRoom(this.groupRoomId); +} + +ConferenceCall.prototype.setup = function() { + var self = this; + return this._joinConferenceUser().then(function() { + return self._getConferenceUserRoom(); + }).then(function(room) { + // return a call for *this* room to be placed. We also tack on + // confUserId to speed up lookups (else we'd need to loop every room + // looking for a 1:1 room with this conf user ID!) + var call = Matrix.createNewMatrixCall(self.client, room.roomId); + call.confUserId = self.confUserId; + call.groupRoomId = self.groupRoomId; + return call; + }); +}; + +ConferenceCall.prototype._joinConferenceUser = function() { + // Make sure the conference user is in the group chat room + var groupRoom = this.client.getRoom(this.groupRoomId); + if (!groupRoom) { + return Promise.reject("Bad group room ID"); + } + var member = groupRoom.getMember(this.confUserId); + if (member && member.membership === "join") { + return Promise.resolve(); + } + return this.client.invite(this.groupRoomId, this.confUserId); +}; + +ConferenceCall.prototype._getConferenceUserRoom = function() { + // Use an existing 1:1 with the conference user; else make one + var rooms = this.client.getRooms(); + var confRoom = null; + for (var i = 0; i < rooms.length; i++) { + var confUser = rooms[i].getMember(this.confUserId); + if (confUser && confUser.membership === "join" && + rooms[i].getJoinedMemberCount() === 2) { + confRoom = rooms[i]; + break; + } + } + if (confRoom) { + return Promise.resolve(confRoom); + } + return this.client.createRoom({ + preset: "private_chat", + invite: [this.confUserId] + }).then(function(res) { + return new Room(res.room_id, null, client.getUserId()); + }); +}; + +/** + * Check if this user ID is in fact a conference bot. + * @param {string} userId The user ID to check. + * @return {boolean} True if it is a conference bot. + */ +module.exports.isConferenceUser = function(userId) { + if (userId.indexOf("@" + USER_PREFIX) !== 0) { + return false; + } + var base64part = userId.split(":")[0].substring(1 + USER_PREFIX.length); + if (base64part) { + var decoded = new Buffer(base64part, "base64").toString(); + // ! $STUFF : $STUFF + return /^!.+:.+/.test(decoded); + } + return false; +}; + +module.exports.getConferenceUserIdForRoom = function(roomId) { + // abuse browserify's core node Buffer support (strip padding ='s) + var base64RoomId = new Buffer(roomId).toString("base64").replace(/=/g, ""); + return "@" + USER_PREFIX + base64RoomId + ":" + DOMAIN; +}; + +module.exports.createNewMatrixCall = function(client, roomId) { + var confCall = new ConferenceCall( + client, roomId + ); + return confCall.setup(); +}; + +module.exports.getConferenceCallForRoom = function(roomId) { + // search for a conference 1:1 call for this group chat room ID + var activeCall = CallHandler.getAnyActiveCall(); + if (activeCall && activeCall.confUserId) { + var thisRoomConfUserId = module.exports.getConferenceUserIdForRoom( + roomId + ); + if (thisRoomConfUserId === activeCall.confUserId) { + return activeCall; + } + } + return null; +}; + +module.exports.ConferenceCall = ConferenceCall; + +module.exports.slot = 'conference'; diff --git a/src/Velociraptor.js b/src/Velociraptor.js index 9c85bafca0..6a4666305c 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -1,6 +1,7 @@ -var React = require('react'); -var ReactDom = require('react-dom'); -var Velocity = require('velocity-vector'); +const React = require('react'); +const ReactDom = require('react-dom'); +import PropTypes from 'prop-types'; +const Velocity = require('velocity-vector'); /** * The Velociraptor contains components and animates transitions with velocity. @@ -14,16 +15,16 @@ module.exports = React.createClass({ propTypes: { // either a list of child nodes, or a single child. - children: React.PropTypes.any, + children: PropTypes.any, // optional transition information for changing existing children - transition: React.PropTypes.object, + transition: PropTypes.object, // a list of state objects to apply to each child node in turn - startStyles: React.PropTypes.array, + startStyles: PropTypes.array, // a list of transition options from the corresponding startStyle - enterTransitionOpts: React.PropTypes.array, + enterTransitionOpts: PropTypes.array, }, getDefaultProps: function() { @@ -46,13 +47,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 +72,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 +104,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. @@ -146,7 +147,7 @@ module.exports = React.createClass({ // creating/destroying large numbers of elements" // (https://github.com/julianshapiro/velocity/issues/47) const domNode = ReactDom.findDOMNode(this.nodes[k]); - Velocity.Utilities.removeData(domNode); + if (domNode) Velocity.Utilities.removeData(domNode); } this.nodes[k] = node; }, @@ -154,7 +155,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..5b722df65f --- /dev/null +++ b/src/WidgetMessaging.js @@ -0,0 +1,117 @@ +/* +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. +*/ + +/* +* See - https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing for +* spec. details / documentation. +*/ + +import FromWidgetPostMessageApi from './FromWidgetPostMessageApi'; +import ToWidgetPostMessageApi from './ToWidgetPostMessageApi'; + +if (!global.mxFromWidgetMessaging) { + global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); + global.mxFromWidgetMessaging.start(); +} +if (!global.mxToWidgetMessaging) { + global.mxToWidgetMessaging = new ToWidgetPostMessageApi(); + global.mxToWidgetMessaging.start(); +} + +const OUTBOUND_API_NAME = 'toWidget'; + +export default class WidgetMessaging { + constructor(widgetId, widgetUrl, target) { + this.widgetId = widgetId; + this.widgetUrl = widgetUrl; + this.target = target; + this.fromWidget = global.mxFromWidgetMessaging; + this.toWidget = global.mxToWidgetMessaging; + this.start(); + } + + messageToWidget(action) { + action.widgetId = this.widgetId; // Required to be sent for all outbound requests + + return this.toWidget.exec(action, this.target).then((data) => { + // Check for errors and reject if found + if (data.response === undefined) { // null is valid + throw new Error("Missing 'response' field"); + } + if (data.response && data.response.error) { + const err = data.response.error; + const msg = String(err.message ? err.message : "An error was returned"); + if (err._error) { + console.error(err._error); + } + // Potential XSS attack if 'msg' is not appropriately sanitized, + // as it is untrusted input by our parent window (which we assume is Riot). + // We can't aggressively sanitize [A-z0-9] since it might be a translation. + throw new Error(msg); + } + // Return the response field for the request + return data.response; + }); + } + + /** + * Request a screenshot from a widget + * @return {Promise} To be resolved with screenshot data when it has been generated + */ + getScreenshot() { + console.warn('Requesting screenshot for', this.widgetId); + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: "screenshot", + }) + .catch((error) => new Error("Failed to get screenshot: " + error.message)) + .then((response) => response.screenshot); + } + + /** + * Request capabilities required by the widget + * @return {Promise} To be resolved with an array of requested widget capabilities + */ + getCapabilities() { + console.warn('Requesting capabilities for', this.widgetId); + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: "capabilities", + }).then((response) => { + console.warn('Got capabilities for', this.widgetId, response.capabilities); + return response.capabilities; + }); + } + + sendVisibility(visible) { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: "visibility", + visible, + }) + .catch((error) => { + console.error("Failed to send visibility: ", error); + }); + } + + start() { + this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl); + } + + stop() { + this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl); + } +} diff --git a/src/WidgetMessagingEndpoint.js b/src/WidgetMessagingEndpoint.js new file mode 100644 index 0000000000..9114e12137 --- /dev/null +++ b/src/WidgetMessagingEndpoint.js @@ -0,0 +1,37 @@ +/* +Copyright 2018 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. +*/ + + +/** + * Represents mapping of widget instance to URLs for trusted postMessage communication. + */ +export default 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; + } +} diff --git a/src/WidgetUtils.js b/src/WidgetUtils.js deleted file mode 100644 index 34c998978d..0000000000 --- a/src/WidgetUtils.js +++ /dev/null @@ -1,58 +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 MatrixClientPeg from './MatrixClientPeg'; - -export default class WidgetUtils { - - /* Returns true if user is able to send state events to modify widgets in this room - * @param roomId -- The ID of the room to check - * @return Boolean -- true if the user can modify widgets in this room - * @throws Error -- specifies the error reason - */ - static canUserModifyWidgets(roomId) { - if (!roomId) { - console.warn('No room ID specified'); - return false; - } - - const client = MatrixClientPeg.get(); - if (!client) { - console.warn('User must be be logged in'); - return false; - } - - const room = client.getRoom(roomId); - if (!room) { - console.warn(`Room ID ${roomId} is not recognised`); - return false; - } - - const me = client.credentials.userId; - if (!me) { - console.warn('Failed to get user ID'); - return false; - } - - const member = room.getMember(me); - if (!member || member.membership !== "join") { - console.warn(`User ${me} is not in room ${roomId}`); - return false; - } - - return room.currentState.maySendStateEvent('im.vector.modular.widgets', me); - } -} 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..17be9a5e0f --- /dev/null +++ b/src/actions/MatrixActionCreators.js @@ -0,0 +1,240 @@ +/* +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(), + }; +} + +/** + * @typedef RoomAction + * @type {Object} + * @property {string} action 'MatrixActions.Room'. + * @property {Room} room the Room that was stored. + */ + +/** + * Create a MatrixActions.Room action that represents a MatrixClient `Room` + * matrix event, emitted when a Room is stored in the client. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {Room} room the Room that was stored. + * @returns {RoomAction} an action of type `MatrixActions.Room`. + */ +function createRoomAction(matrixClient, room) { + return { action: 'MatrixActions.Room', room }; +} + +/** + * @typedef RoomTagsAction + * @type {Object} + * @property {string} action 'MatrixActions.Room.tags'. + * @property {Room} room the Room whose tags changed. + */ + +/** + * Create a MatrixActions.Room.tags action that represents a MatrixClient + * `Room.tags` matrix event, emitted when the m.tag room account data + * event is updated. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {MatrixEvent} roomTagsEvent the m.tag event. + * @param {Room} room the Room whose tags were changed. + * @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`. + */ +function createRoomTagsAction(matrixClient, roomTagsEvent, room) { + return { action: 'MatrixActions.Room.tags', room }; +} + +/** + * @typedef RoomTimelineAction + * @type {Object} + * @property {string} action 'MatrixActions.Room.timeline'. + * @property {boolean} isLiveEvent whether the event was attached to a + * live timeline. + * @property {boolean} isLiveUnfilteredRoomTimelineEvent whether the + * event was attached to a timeline in the set of unfiltered timelines. + * @property {Room} room the Room whose tags changed. + */ + +/** + * Create a MatrixActions.Room.timeline action that represents a + * MatrixClient `Room.timeline` matrix event, emitted when an event + * is added to or removed from a timeline of a room. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {MatrixEvent} timelineEvent the event that was added/removed. + * @param {Room} room the Room that was stored. + * @param {boolean} toStartOfTimeline whether the event is being added + * to the start (and not the end) of the timeline. + * @param {boolean} removed whether the event was removed from the + * timeline. + * @param {Object} data + * @param {boolean} data.liveEvent whether the event is a live event, + * belonging to a live timeline. + * @param {EventTimeline} data.timeline the timeline being altered. + * @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`. + */ +function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTimeline, removed, data) { + return { + action: 'MatrixActions.Room.timeline', + event: timelineEvent, + isLiveEvent: data.liveEvent, + isLiveUnfilteredRoomTimelineEvent: + room && data.timeline.getTimelineSet() === room.getUnfilteredTimelineSet(), + }; +} + +/** + * @typedef RoomMembershipAction + * @type {Object} + * @property {string} action 'MatrixActions.RoomMember.membership'. + * @property {RoomMember} member the member whose membership was updated. + */ + +/** + * Create a MatrixActions.Room.selfMembership action that represents + * a MatrixClient `RoomMember.membership` matrix event for the syncing user, + * emitted when the member's membership is updated. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {MatrixEvent} membershipEvent the m.room.member event. + * @param {RoomMember} member the member whose membership was updated. + * @param {string} oldMembership the member's previous membership. + * @returns {RoomMembershipAction} an action of type `MatrixActions.RoomMember.membership`. + */ +function createSelfRoomMembershipAction(matrixClient, membershipEvent, member, oldMembership) { + if (member.userId === matrixClient.getUserId()) { + return { action: 'MatrixActions.Room.selfMembership', member }; + } + return null; +} + +/** + * @typedef EventDecryptedAction + * @type {Object} + * @property {string} action 'MatrixActions.Event.decrypted'. + * @property {MatrixEvent} event the matrix event that was decrypted. + */ + +/** + * Create a MatrixActions.Event.decrypted action that represents + * a MatrixClient `Event.decrypted` matrix event, emitted when a + * matrix event is decrypted. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {MatrixEvent} event the matrix event that was decrypted. + * @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`. + */ +function createEventDecryptedAction(matrixClient, event) { + return { action: 'MatrixActions.Event.decrypted', event }; +} + +/** + * 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); + this._addMatrixClientListener(matrixClient, 'Room', createRoomAction); + this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction); + this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction); + this._addMatrixClientListener(matrixClient, 'RoomMember.membership', createSelfRoomMembershipAction); + this._addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction); + }, + + /** + * 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) => { + const payload = actionCreator(matrixClient, ...args); + if (payload) { + dis.dispatch(payload, true); + } + }; + 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/RoomListActions.js b/src/actions/RoomListActions.js new file mode 100644 index 0000000000..e5911c4e32 --- /dev/null +++ b/src/actions/RoomListActions.js @@ -0,0 +1,146 @@ +/* +Copyright 2018 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'; +import RoomListStore from '../stores/RoomListStore'; + +import Modal from '../Modal'; +import * as Rooms from '../Rooms'; +import { _t } from '../languageHandler'; +import sdk from '../index'; + +const RoomListActions = {}; + +/** + * Creates an action thunk that will do an asynchronous request to + * tag room. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {Room} room the room to tag. + * @param {string} oldTag the tag to remove (unless oldTag ==== newTag) + * @param {string} newTag the tag with which to tag the room. + * @param {?number} oldIndex the previous position of the room in the + * list of rooms. + * @param {?number} newIndex the new position of the room in the list + * of rooms. + * @returns {function} an action thunk. + * @see asyncAction + */ +RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, newIndex) { + let metaData = null; + + // Is the tag ordered manually? + if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { + const lists = RoomListStore.getRoomLists(); + const newList = [...lists[newTag]]; + + newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); + + // If the room was moved "down" (increasing index) in the same list we + // need to use the orders of the tiles with indices shifted by +1 + const offset = ( + newTag === oldTag && oldIndex < newIndex + ) ? 1 : 0; + + const indexBefore = offset + newIndex - 1; + const indexAfter = offset + newIndex; + + const prevOrder = indexBefore <= 0 ? + 0 : newList[indexBefore].tags[newTag].order; + const nextOrder = indexAfter >= newList.length ? + 1 : newList[indexAfter].tags[newTag].order; + + metaData = { + order: (prevOrder + nextOrder) / 2.0, + }; + } + + return asyncAction('RoomListActions.tagRoom', () => { + const promises = []; + const roomId = room.roomId; + + // Evil hack to get DMs behaving + if ((oldTag === undefined && newTag === 'im.vector.fake.direct') || + (oldTag === 'im.vector.fake.direct' && newTag === undefined) + ) { + return Rooms.guessAndSetDMRoom( + room, newTag === 'im.vector.fake.direct', + ).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to set direct chat tag " + err); + Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { + title: _t('Failed to set direct chat tag'), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + } + + const hasChangedSubLists = oldTag !== newTag; + + // More evilness: We will still be dealing with moving to favourites/low prio, + // but we avoid ever doing a request with 'im.vector.fake.direct`. + // + // if we moved lists, remove the old tag + if (oldTag && oldTag !== 'im.vector.fake.direct' && + hasChangedSubLists + ) { + const promiseToDelete = matrixClient.deleteRoomTag( + roomId, oldTag, + ).catch(function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to remove tag " + oldTag + " from room: " + err); + Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { + title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + + promises.push(promiseToDelete); + } + + // if we moved lists or the ordering changed, add the new tag + if (newTag && newTag !== 'im.vector.fake.direct' && + (hasChangedSubLists || metaData) + ) { + // metaData is the body of the PUT to set the tag, so it must + // at least be an empty object. + metaData = metaData || {}; + + const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to add tag " + newTag + " to room: " + err); + Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { + title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + + throw err; + }); + + promises.push(promiseToAdd); + } + + return Promise.all(promises); + }, () => { + // For an optimistic update + return { + room, oldTag, newTag, metaData, + }; + }); +}; + +export default RoomListActions; diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js new file mode 100644 index 0000000000..a257ff16d8 --- /dev/null +++ b/src/actions/TagOrderActions.js @@ -0,0 +1,109 @@ +/* +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 + * move a tag in TagOrderStore to destinationIx. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {string} tag the tag to move. + * @param {number} destinationIx the new position of the tag. + * @returns {function} an action thunk that will dispatch actions + * indicating the status of the request. + * @see asyncAction + */ +TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) { + // Only commit tags if the state is ready, i.e. not null + let tags = TagOrderStore.getOrderedTags(); + let removedTags = TagOrderStore.getRemovedTagsAccountData() || []; + if (!tags) { + return; + } + + tags = tags.filter((t) => t !== tag); + tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)]; + + removedTags = removedTags.filter((t) => t !== tag); + + const storeId = TagOrderStore.getStoreId(); + + return asyncAction('TagOrderActions.moveTag', () => { + Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); + return matrixClient.setAccountData( + 'im.vector.web.tag_ordering', + {tags, removedTags, _storeId: storeId}, + ); + }, () => { + // For an optimistic update + return {tags, removedTags}; + }); +}; + +/** + * Creates an action thunk that will do an asynchronous request to + * label a tag as removed in im.vector.web.tag_ordering account data. + * + * The reason this is implemented with new state `removedTags` is that + * we incrementally and initially populate `tags` with groups that + * have been joined. If we remove a group from `tags`, it will just + * get added (as it looks like a group we've recently joined). + * + * NB: If we ever support adding of tags (which is planned), we should + * take special care to remove the tag from `removedTags` when we add + * it. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {string} tag the tag to remove. + * @returns {function} an action thunk that will dispatch actions + * indicating the status of the request. + * @see asyncAction + */ +TagOrderActions.removeTag = function(matrixClient, tag) { + // Don't change tags, just removedTags + const tags = TagOrderStore.getOrderedTags(); + const removedTags = TagOrderStore.getRemovedTagsAccountData() || []; + + if (removedTags.includes(tag)) { + // Return a thunk that doesn't do anything, we don't even need + // an asynchronous action here, the tag is already removed. + return () => {}; + } + + removedTags.push(tag); + + const storeId = TagOrderStore.getStoreId(); + + return asyncAction('TagOrderActions.removeTag', () => { + Analytics.trackEvent('TagOrderActions', 'removeTag'); + return matrixClient.setAccountData( + 'im.vector.web.tag_ordering', + {tags, removedTags, _storeId: storeId}, + ); + }, () => { + // For an optimistic update + return {removedTags}; + }); +}; + +export default TagOrderActions; diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js new file mode 100644 index 0000000000..967ce609e7 --- /dev/null +++ b/src/actions/actionCreators.js @@ -0,0 +1,57 @@ +/* +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. + * @param {function?} pendingFn a function that returns an object to assign + * to the `request` key of the ${id}.pending + * payload. + * @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`. + * + * The shape of each are: + * { action: '${id}.pending', request } + * { action: '${id}.success', result } + * { action: '${id}.failure', err } + * + * where `request` is returned by `pendingFn` and + * result is the result of the promise returned by + * `fn`. + */ +export function asyncAction(id, fn, pendingFn) { + return (dispatch) => { + dispatch({ + action: id + '.pending', + request: + typeof pendingFn === 'function' ? pendingFn() : undefined, + }); + 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..5db8b2365f 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -14,17 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -var React = require("react"); +const React = require("react"); +import PropTypes from 'prop-types'; 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', propTypes: { - event: React.PropTypes.object.isRequired, - onFinished: React.PropTypes.func.isRequired, + event: PropTypes.object.isRequired, + onFinished: PropTypes.func.isRequired, }, getInitialState: function() { @@ -33,7 +34,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 +61,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 +90,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 +119,7 @@ module.exports = React.createClass({ { _t('Ed25519 fingerprint') } - {device.getFingerprint()} + { device.getFingerprint() } @@ -126,7 +127,7 @@ module.exports = React.createClass({ }, _renderEventInfo: function() { - var event = this.props.event; + const event = this.props.event; return ( @@ -165,36 +166,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..06fb0668d5 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -16,6 +16,7 @@ limitations under the License. import FileSaver from 'file-saver'; import React from 'react'; +import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as Matrix from 'matrix-js-sdk'; @@ -29,8 +30,8 @@ export default React.createClass({ displayName: 'ExportE2eKeysDialog', propTypes: { - matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired, - onFinished: React.PropTypes.func.isRequired, + matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired, + onFinished: PropTypes.func.isRequired, }, getInitialState: function() { @@ -136,13 +137,13 @@ export default React.createClass({ ) }

- {this.state.errStr} + { this.state.errStr }
@@ -155,7 +156,7 @@ export default React.createClass({
@@ -172,7 +173,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..10744a8911 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import * as Matrix from 'matrix-js-sdk'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; @@ -40,8 +41,8 @@ export default React.createClass({ displayName: 'ImportE2eKeysDialog', propTypes: { - matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired, - onFinished: React.PropTypes.func.isRequired, + matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired, + onFinished: PropTypes.func.isRequired, }, getInitialState: function() { @@ -134,13 +135,13 @@ export default React.createClass({ ) }

- {this.state.errStr} + { this.state.errStr }
@@ -153,14 +154,14 @@ export default React.createClass({
+ disabled={disableForm} />
@@ -170,7 +171,7 @@ export default React.createClass({ disabled={!this.state.enableSubmit || disableForm} />
diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 4c7d039da4..f9fb61d3a3 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, 2018 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. @@ -19,23 +20,33 @@ import React from 'react'; import type {Completion, SelectionRange} from './Autocompleter'; export default class AutocompleteProvider { - constructor(commandRegex?: RegExp) { + constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) { if (commandRegex) { if (!commandRegex.global) { throw new Error('commandRegex must have global flag set'); } this.commandRegex = commandRegex; } + if (forcedCommandRegex) { + if (!forcedCommandRegex.global) { + throw new Error('forcedCommandRegex must have global flag set'); + } + this.forcedCommandRegex = forcedCommandRegex; + } + } + + destroy() { + // stub } /** * Of the matched commands in the query, returns the first that contains or is contained by the selection, or null. */ - getCurrentCommand(query: string, selection: {start: number, end: number}, force: boolean = false): ?string { + getCurrentCommand(query: string, selection: SelectionRange, force: boolean = false): ?string { let commandRegex = this.commandRegex; if (force && this.shouldForceComplete()) { - commandRegex = /\S+/g; + commandRegex = this.forcedCommandRegex || /\S+/g; } if (commandRegex == null) { @@ -46,14 +57,14 @@ export default class AutocompleteProvider { let match; while ((match = commandRegex.exec(query)) != null) { - let matchStart = match.index, - matchEnd = matchStart + match[0].length; - if (selection.start <= matchEnd && selection.end >= matchStart) { + const start = match.index; + const end = start + match[0].length; + if (selection.start <= end && selection.end >= start) { return { command: match, range: { - start: matchStart, - end: matchEnd, + start, + end, }, }; } diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 7a64fb154c..7f91676cc3 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -1,5 +1,6 @@ /* Copyright 2016 Aviral Dasgupta +Copyright 2017, 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,16 +18,20 @@ limitations under the License. // @flow import type {Component} from 'react'; +import {Room} from 'matrix-js-sdk'; import CommandProvider from './CommandProvider'; +import CommunityProvider from './CommunityProvider'; 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 = { - start: number, - end: number + beginning: boolean, // whether the selection is in the first block of the editor or not + start: number, // byte offset relative to the start anchor of the current editor selection. + end: number, // byte offset relative to the end anchor of the current editor selection. }; export type Completion = { @@ -43,43 +48,60 @@ const PROVIDERS = [ UserProvider, RoomProvider, EmojiProvider, + NotifProvider, CommandProvider, + CommunityProvider, 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: 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 => + 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..a35a31966a 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -1,6 +1,8 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,90 +18,16 @@ limitations under the License. */ import React from 'react'; -import { _t } from '../languageHandler'; +import {_t} from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import FuzzyMatcher from './FuzzyMatcher'; import {TextualCompletion} from './Components'; +import type {Completion, SelectionRange} from "./Autocompleter"; +import {CommandMap} from '../SlashCommands'; -// TODO merge this with the factory mechanics of SlashCommands? -// Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file -const COMMANDS = [ - { - command: '/me', - args: '', - description: 'Displays action', - }, - { - command: '/ban', - args: ' [reason]', - description: 'Bans user with given id', - }, - { - command: '/unban', - args: '', - description: 'Unbans user with given id', - }, - { - command: '/op', - args: ' []', - description: 'Define the power level of a user', - }, - { - command: '/deop', - args: '', - description: 'Deops user with given id', - }, - { - command: '/invite', - args: '', - description: 'Invites user with given id to current room', - }, - { - command: '/join', - args: '', - description: 'Joins room with given alias', - }, - { - command: '/part', - args: '[]', - description: 'Leave room', - }, - { - command: '/topic', - args: '', - description: 'Sets the room topic', - }, - { - command: '/kick', - args: ' [reason]', - description: 'Kicks user with given id', - }, - { - command: '/nick', - args: '', - description: 'Changes your display nickname', - }, - { - command: '/ddg', - args: '', - description: 'Searches DuckDuckGo for results', - }, - { - command: '/tint', - args: ' []', - description: 'Changes colour scheme of current room', - }, - { - command: '/verify', - args: ' ', - description: 'Verifies a user, device, and pubkey tuple', - }, - // Omitting `/markdown` as it only seems to apply to OldComposer -]; +const COMMANDS = Object.values(CommandMap); -const COMMAND_RE = /(^\/\w*)/g; - -let instance = null; +const COMMAND_RE = /(^\/\w*)(?: .*)?/g; export default class CommandProvider extends AutocompleteProvider { constructor() { @@ -109,38 +37,48 @@ export default class CommandProvider extends AutocompleteProvider { }); } - async getCompletions(query: string, selection: {start: number, end: number}) { - let completions = []; + async getCompletions(query: string, selection: SelectionRange, force?: boolean): Array { const {command, range} = this.getCurrentCommand(query, selection); - if (command) { - completions = this.matcher.match(command[0]).map((result) => { - return { - completion: result.command + ' ', - component: (), - range, - }; - }); + if (!command) return []; + + let matches = []; + // check if the full match differs from the first word (i.e. returns false if the command has args) + if (command[0] !== command[1]) { + // The input looks like a command with arguments, perform exact match + const name = command[1].substr(1); // strip leading `/` + if (CommandMap[name]) { + // some commands, namely `me` and `ddg` don't suit having the usage shown whilst typing their arguments + if (CommandMap[name].hideCompletionAfterSpace) return []; + matches = [CommandMap[name]]; + } + } else { + if (query === '/') { + // If they have just entered `/` show everything + matches = COMMANDS; + } else { + // otherwise fuzzy match against all of the fields + matches = this.matcher.match(command[1]); + } } - return completions; + + return matches.map((result) => ({ + // If the command is the same as the one they entered, we don't want to discard their arguments + completion: result.command === command[1] ? command[0] : (result.command + ' '), + component: , + range, + })); } getName() { 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/CommunityProvider.js b/src/autocomplete/CommunityProvider.js new file mode 100644 index 0000000000..6bcf1a02fd --- /dev/null +++ b/src/autocomplete/CommunityProvider.js @@ -0,0 +1,111 @@ +/* +Copyright 2018 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { _t } from '../languageHandler'; +import AutocompleteProvider from './AutocompleteProvider'; +import MatrixClientPeg from '../MatrixClientPeg'; +import FuzzyMatcher from './FuzzyMatcher'; +import {PillCompletion} from './Components'; +import sdk from '../index'; +import _sortBy from 'lodash/sortBy'; +import {makeGroupPermalink} from "../matrix-to"; +import type {Completion, SelectionRange} from "./Autocompleter"; +import FlairStore from "../stores/FlairStore"; + +const COMMUNITY_REGEX = /\B\+\S*/g; + +function score(query, space) { + const index = space.indexOf(query); + if (index === -1) { + return Infinity; + } else { + return index; + } +} + +export default class CommunityProvider extends AutocompleteProvider { + constructor() { + super(COMMUNITY_REGEX); + this.matcher = new FuzzyMatcher([], { + keys: ['groupId', 'name', 'shortDescription'], + }); + } + + async getCompletions(query: string, selection: SelectionRange, force?: boolean = false): Array { + const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar'); + + // Disable autocompletions when composing commands because of various issues + // (see https://github.com/vector-im/riot-web/issues/4762) + if (/^(\/join|\/leave)/.test(query)) { + return []; + } + + const cli = MatrixClientPeg.get(); + let completions = []; + const {command, range} = this.getCurrentCommand(query, selection, force); + if (command) { + const joinedGroups = cli.getGroups().filter(({myMembership}) => myMembership === 'join'); + + const groups = (await Promise.all(joinedGroups.map(async ({groupId}) => { + try { + return FlairStore.getGroupProfileCached(cli, groupId); + } catch (e) { // if FlairStore failed, fall back to just groupId + return Promise.resolve({ + name: '', + groupId, + avatarUrl: '', + shortDescription: '', + }); + } + }))); + + this.matcher.setObjects(groups); + + const matchedString = command[0]; + completions = this.matcher.match(matchedString); + completions = _sortBy(completions, [ + (c) => score(matchedString, c.groupId), + (c) => c.groupId.length, + ]).map(({avatarUrl, groupId, name}) => ({ + completion: groupId, + suffix: ' ', + href: makeGroupPermalink(groupId), + component: ( + + } title={name} description={groupId} /> + ), + range, + })) + .slice(0, 4); + } + return completions; + } + + getName() { + return '💬 ' + _t('Communities'); + } + + renderCompletions(completions: [React.Component]): ?React.Component { + return
+ { completions } +
; + } +} diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index 0f0399cf7d..b09f4e963e 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import classNames from 'classnames'; /* These were earlier stateless functional components but had to be converted @@ -30,22 +31,22 @@ export class TextualCompletion extends React.Component { subtitle, description, className, - ...restProps, + ...restProps } = this.props; return (
- {title} - {subtitle} - {description} + { title } + { subtitle } + { description }
); } } TextualCompletion.propTypes = { - title: React.PropTypes.string, - subtitle: React.PropTypes.string, - description: React.PropTypes.string, - className: React.PropTypes.string, + title: PropTypes.string, + subtitle: PropTypes.string, + description: PropTypes.string, + className: PropTypes.string, }; export class PillCompletion extends React.Component { @@ -56,22 +57,22 @@ export class PillCompletion extends React.Component { description, initialComponent, className, - ...restProps, + ...restProps } = this.props; return (
- {initialComponent} - {title} - {subtitle} - {description} + { initialComponent } + { title } + { subtitle } + { description }
); } } PillCompletion.propTypes = { - title: React.PropTypes.string, - subtitle: React.PropTypes.string, - description: React.PropTypes.string, - initialComponent: React.PropTypes.element, - className: React.PropTypes.string, + title: PropTypes.string, + subtitle: PropTypes.string, + description: PropTypes.string, + initialComponent: PropTypes.element, + className: PropTypes.string, }; diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index 9c996bb1cc..e25ef16428 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, 2018 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,12 +22,11 @@ import AutocompleteProvider from './AutocompleteProvider'; import 'whatwg-fetch'; import {TextualCompletion} from './Components'; +import type {SelectionRange} from "./Autocompleter"; const DDG_REGEX = /\/ddg\s+(.+)$/g; const REFERRER = 'vector'; -let instance = null; - export default class DuckDuckGoProvider extends AutocompleteProvider { constructor() { super(DDG_REGEX); @@ -37,8 +37,8 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`; } - async getCompletions(query: string, selection: {start: number, end: number}) { - let {command, range} = this.getCurrentCommand(query, selection); + async getCompletions(query: string, selection: SelectionRange, force?: boolean = false) { + const {command, range} = this.getCurrentCommand(query, selection); if (!query || !command) { return []; } @@ -47,7 +47,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { method: 'GET', }); const json = await response.json(); - let results = json.Results.map(result => { + const results = json.Results.map((result) => { return { completion: result.Text, component: ( @@ -96,16 +96,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..719550d59f 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, 2018 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. @@ -18,13 +19,14 @@ limitations under the License. import React from 'react'; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; -import {emojioneList, shortnameToImage, shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione'; +import {shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione'; import FuzzyMatcher from './FuzzyMatcher'; import sdk from '../index'; import {PillCompletion} from './Components'; -import type {SelectionRange, Completion} from './Autocompleter'; +import type {Completion, SelectionRange} from './Autocompleter'; import _uniq from 'lodash/uniq'; import _sortBy from 'lodash/sortBy'; +import SettingsStore from "../settings/SettingsStore"; import EmojiData from '../stripped-emoji.json'; @@ -46,7 +48,7 @@ const CATEGORY_ORDER = [ // (^|\s|(emojiUnicode)) to make sure we're either at the start of the string or there's a // whitespace character or an emoji before the emoji. The reason for unicodeRegexp is // that we need to support inputting multiple emoji with no space between them. -const EMOJI_REGEX = new RegExp('(?:^|\\s|' + unicodeRegexp + ')(' + asciiRegexp + '|:\\w*:?)$', 'g'); +const EMOJI_REGEX = new RegExp('(?:^|\\s|' + unicodeRegexp + ')(' + asciiRegexp + '|:[+-\\w]*:?)$', 'g'); // We also need to match the non-zero-length prefixes to remove them from the final match, // and update the range so that we don't replace the whitespace or the previous emoji. @@ -63,14 +65,13 @@ const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sor return { name: a.name, shortname: a.shortname, + aliases: a.aliases ? a.aliases.join(' ') : '', aliases_ascii: a.aliases_ascii ? a.aliases_ascii.join(' ') : '', // Include the index so that we can preserve the original order _orderBy: index, }; }); -let instance = null; - function score(query, space) { const index = space.indexOf(query); if (index === -1) { @@ -84,7 +85,7 @@ export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); this.matcher = new FuzzyMatcher(EMOJI_SHORTNAMES, { - keys: ['aliases_ascii', 'shortname'], + keys: ['aliases_ascii', 'shortname', 'aliases'], // For matching against ascii equivalents shouldMatchWordsOnly: false, }); @@ -95,7 +96,11 @@ export default class EmojiProvider extends AutocompleteProvider { }); } - async getCompletions(query: string, selection: SelectionRange) { + async getCompletions(query: string, selection: SelectionRange, force?: boolean): Array { + 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 +138,7 @@ export default class EmojiProvider extends AutocompleteProvider { return { completion: unicode, component: ( - {unicode}} /> + { unicode }} /> ), range, }; @@ -146,15 +151,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..432388c255 --- /dev/null +++ b/src/autocomplete/NotifProvider.js @@ -0,0 +1,64 @@ +/* +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'; +import type {Completion, SelectionRange} from "./Autocompleter"; + +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: SelectionRange, force?:boolean = false): Array { + 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', + completionId: '@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/PlainWithPillsSerializer.js b/src/autocomplete/PlainWithPillsSerializer.js new file mode 100644 index 0000000000..59cf1bde3b --- /dev/null +++ b/src/autocomplete/PlainWithPillsSerializer.js @@ -0,0 +1,93 @@ +/* +Copyright 2018 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. +*/ + +// Based originally on slate-plain-serializer + +import { Block } from 'slate'; + +/** + * Plain text serializer, which converts a Slate `value` to a plain text string, + * serializing pills into various different formats as required. + * + * @type {PlainWithPillsSerializer} + */ + +class PlainWithPillsSerializer { + + /* + * @param {String} options.pillFormat - either 'md', 'plain', 'id' + */ + constructor(options = {}) { + const { + pillFormat = 'plain', + } = options; + this.pillFormat = pillFormat; + } + + /** + * Serialize a Slate `value` to a plain text string, + * serializing pills as either MD links, plain text representations or + * ID representations as required. + * + * @param {Value} value + * @return {String} + */ + serialize = value => { + return this._serializeNode(value.document); + } + + /** + * Serialize a `node` to plain text. + * + * @param {Node} node + * @return {String} + */ + _serializeNode = node => { + if ( + node.object == 'document' || + (node.object == 'block' && Block.isBlockList(node.nodes)) + ) { + return node.nodes.map(this._serializeNode).join('\n'); + } else if (node.type == 'emoji') { + return node.data.get('emojiUnicode'); + } else if (node.type == 'pill') { + const completion = node.data.get('completion'); + // over the wire the @room pill is just plaintext + if (completion === '@room') return completion; + + switch (this.pillFormat) { + case 'plain': + return completion; + case 'md': + return `[${ completion }](${ node.data.get('href') })`; + case 'id': + return node.data.get('completionId') || completion; + } + } else if (node.nodes) { + return node.nodes.map(this._serializeNode).join(''); + } else { + return node.text; + } + } +} + +/** + * Export. + * + * @type {PlainWithPillsSerializer} + */ + +export default PlainWithPillsSerializer; diff --git a/src/autocomplete/QueryMatcher.js b/src/autocomplete/QueryMatcher.js index 762b285685..9d4d4d0598 100644 --- a/src/autocomplete/QueryMatcher.js +++ b/src/autocomplete/QueryMatcher.js @@ -1,6 +1,7 @@ //@flow /* Copyright 2017 Aviral Dasgupta +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,6 +28,10 @@ class KeyMap { priorityMap = new Map(); } +function stripDiacritics(str: string): string { + return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); +} + export default class QueryMatcher { /** * @param {object[]} objects the objects to perform a match on @@ -46,10 +51,11 @@ export default class QueryMatcher { objects.forEach((object, i) => { const keyValues = _at(object, keys); for (const keyValue of keyValues) { - if (!map.hasOwnProperty(keyValue)) { - map[keyValue] = []; + const key = stripDiacritics(keyValue).toLowerCase(); + if (!map.hasOwnProperty(key)) { + map[key] = []; } - map[keyValue].push(object); + map[key].push(object); } keyMap.priorityMap.set(object, i); }); @@ -82,7 +88,7 @@ export default class QueryMatcher { } match(query: String): Array { - query = query.toLowerCase(); + query = stripDiacritics(query).toLowerCase(); if (this.options.shouldMatchWordsOnly) { query = query.replace(/[^\w]/g, ''); } @@ -91,7 +97,7 @@ export default class QueryMatcher { } const results = []; this.keyMap.keys.forEach((key) => { - let resultKey = key.toLowerCase(); + let resultKey = key; if (this.options.shouldMatchWordsOnly) { resultKey = resultKey.replace(/[^\w]/g, ''); } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 1770089eb2..38e2ab8373 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -1,6 +1,8 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +Copyright 2017, 2018 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,10 +26,10 @@ import {PillCompletion} from './Components'; import {getDisplayAliasForRoom} from '../Rooms'; import sdk from '../index'; import _sortBy from 'lodash/sortBy'; +import {makeRoomPermalink} from "../matrix-to"; +import type {Completion, SelectionRange} from "./Autocompleter"; -const ROOM_REGEX = /(?=#)(\S*)/g; - -let instance = null; +const ROOM_REGEX = /\B#\S*/g; function score(query, space) { const index = space.indexOf(query); @@ -46,15 +48,9 @@ export default class RoomProvider extends AutocompleteProvider { }); } - async getCompletions(query: string, selection: {start: number, end: number}, force = false) { + async getCompletions(query: string, selection: SelectionRange, force?: boolean = false): Array { const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); - // Disable autocompletions when composing commands because of various issues - // (see https://github.com/vector-im/riot-web/issues/4762) - if (/^(\/join|\/leave)/.test(query)) { - return []; - } - const client = MatrixClientPeg.get(); let completions = []; const {command, range} = this.getCurrentCommand(query, selection, force); @@ -78,8 +74,9 @@ export default class RoomProvider extends AutocompleteProvider { const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId; return { completion: displayAlias, + completionId: displayAlias, suffix: ' ', - href: 'https://matrix.to/#/' + displayAlias, + href: makeRoomPermalink(displayAlias), component: ( } title={room.name} description={displayAlias} /> ), @@ -96,17 +93,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..156aac2eb8 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -2,6 +2,8 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +Copyright 2017, 2018 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,52 +24,98 @@ import AutocompleteProvider from './AutocompleteProvider'; import {PillCompletion} from './Components'; import sdk from '../index'; import FuzzyMatcher from './FuzzyMatcher'; -import _pull from 'lodash/pull'; import _sortBy from 'lodash/sortBy'; import MatrixClientPeg from '../MatrixClientPeg'; -import type {Room, RoomMember} from 'matrix-js-sdk'; +import type {MatrixEvent, Room, RoomMember, RoomState} from 'matrix-js-sdk'; +import {makeUserPermalink} from "../matrix-to"; +import type {Completion, SelectionRange} from "./Autocompleter"; -const USER_REGEX = /@\S*/g; +const USER_REGEX = /\B@\S*/g; -let instance = null; +// used when you hit 'tab' - we allow some separator chars at the beginning +// to allow you to tab-complete /mat into /(matthew) +const FORCED_USER_REGEX = /[^/,:; \t\n]\S*/g; export default class UserProvider extends AutocompleteProvider { - users: Array = []; + users: Array = null; + room: Room = null; - constructor() { - super(USER_REGEX, { - keys: ['name'], - }); + constructor(room) { + super(USER_REGEX, FORCED_USER_REGEX); + this.room = room; this.matcher = new FuzzyMatcher([], { keys: ['name', 'userId'], shouldMatchPrefix: true, + shouldMatchWordsOnly: false, }); + + 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); } - async getCompletions(query: string, selection: {start: number, end: number}, force = false) { - const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); + destroy() { + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound); + MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound); + } + } - // Disable autocompletions when composing commands because of various issues - // (see https://github.com/vector-im/riot-web/issues/4762) - if (/^(\/ban|\/unban|\/op|\/deop|\/invite|\/kick|\/verify)/.test(query)) { - return []; + _onRoomTimeline(ev: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, data: Object) { + 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; + + // TODO: lazyload if we have no ev.sender room member? + this.onUserSpoke(ev.sender); + } + + _onRoomStateMember(ev: MatrixEvent, state: RoomState, member: RoomMember) { + // 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: SelectionRange, force?: boolean = false): Array { + const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); + + // lazy-load user list into matcher + if (this.users === null) this._makeUsers(); + let completions = []; - let {command, range} = this.getCurrentCommand(query, selection, force); - if (command) { - completions = this.matcher.match(command[0]).map((user) => { + const {command, range} = this.getCurrentCommand(query, selection, force); + + if (!command) return completions; + + const fullMatch = command[0]; + // Don't search if the query is a single "@" + if (fullMatch && fullMatch !== '@') { + completions = this.matcher.match(fullMatch).map((user) => { const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done return { // Length of completion should equal length of text in decorator. draft-js // relies on the length of the entity === length of the text in the decoration. completion: user.rawDisplayName.replace(' (IRC)', ''), - suffix: range.start === 0 ? ': ' : ' ', - href: 'https://matrix.to/#/' + user.userId, + completionId: user.userId, + suffix: (selection.beginning && range.start === 0) ? ': ' : ' ', + href: makeUserPermalink(user.userId), component: ( } + initialComponent={} title={displayName} description={user.userId} /> ), @@ -78,32 +126,30 @@ export default class UserProvider extends AutocompleteProvider { return completions; } - getName() { + getName(): string { 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) => { - if (member.userId !== currentUserId) return true; - }); + this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId); - this.users = _sortBy(this.users, (member) => - 1E20 - lastSpoken[member.userId] || 1E20, - ); + this.users = _sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20); this.matcher.setObjects(this.users); } onUserSpoke(user: RoomMember) { - if(user.userId === MatrixClientPeg.get().credentials.userId) return; + if (this.users === null) return; + if (!user) 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 +159,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} + return
+ { completions }
; } diff --git a/src/components/structures/BottomLeftMenu.js b/src/components/structures/BottomLeftMenu.js new file mode 100644 index 0000000000..d289ca5da1 --- /dev/null +++ b/src/components/structures/BottomLeftMenu.js @@ -0,0 +1,197 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +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 React from 'react'; +import ReactDOM from 'react-dom'; +import sdk from '../../index'; +import dis from '../../dispatcher'; +import Velocity from 'velocity-vector'; +import 'velocity-vector/velocity.ui'; +import SettingsStore from '../../settings/SettingsStore'; + +const CALLOUT_ANIM_DURATION = 1000; + +module.exports = React.createClass({ + displayName: 'BottomLeftMenu', + + propTypes: { + collapsed: React.PropTypes.bool.isRequired, + }, + + getInitialState: function() { + return({ + directoryHover : false, + roomsHover : false, + homeHover: false, + peopleHover : false, + settingsHover : false, + }); + }, + + componentWillMount: function() { + this._dispatcherRef = dis.register(this.onAction); + this._peopleButton = null; + this._directoryButton = null; + this._createRoomButton = null; + this._lastCallouts = {}; + }, + + componentWillUnmount: function() { + dis.unregister(this._dispatcherRef); + }, + + // Room events + onDirectoryClick: function() { + dis.dispatch({ action: 'view_room_directory' }); + }, + + onDirectoryMouseEnter: function() { + this.setState({ directoryHover: true }); + }, + + onDirectoryMouseLeave: function() { + this.setState({ directoryHover: false }); + }, + + onRoomsClick: function() { + dis.dispatch({ action: 'view_create_room' }); + }, + + onRoomsMouseEnter: function() { + this.setState({ roomsHover: true }); + }, + + onRoomsMouseLeave: function() { + this.setState({ roomsHover: false }); + }, + + // Home button events + onHomeClick: function() { + dis.dispatch({ action: 'view_home_page' }); + }, + + onHomeMouseEnter: function() { + this.setState({ homeHover: true }); + }, + + onHomeMouseLeave: function() { + this.setState({ homeHover: false }); + }, + + // People events + onPeopleClick: function() { + dis.dispatch({ action: 'view_create_chat' }); + }, + + onPeopleMouseEnter: function() { + this.setState({ peopleHover: true }); + }, + + onPeopleMouseLeave: function() { + this.setState({ peopleHover: false }); + }, + + // Settings events + onSettingsClick: function() { + dis.dispatch({ action: 'view_user_settings' }); + }, + + onSettingsMouseEnter: function() { + this.setState({ settingsHover: true }); + }, + + onSettingsMouseLeave: function() { + this.setState({ settingsHover: false }); + }, + + onAction: function(payload) { + let calloutElement; + switch (payload.action) { + // Incoming instruction: dance! + case 'callout_start_chat': + calloutElement = this._peopleButton; + break; + case 'callout_room_directory': + calloutElement = this._directoryButton; + break; + case 'callout_create_room': + calloutElement = this._createRoomButton; + break; + } + if (calloutElement) { + const lastCallout = this._lastCallouts[payload.action]; + const now = Date.now(); + if (lastCallout == undefined || lastCallout < now - CALLOUT_ANIM_DURATION) { + this._lastCallouts[payload.action] = now; + Velocity(ReactDOM.findDOMNode(calloutElement), "callout.bounce", CALLOUT_ANIM_DURATION); + } + } + }, + + // Get the label/tooltip to show + getLabel: function(label, show) { + if (show) { + var RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); + return ; + } + }, + + _collectPeopleButton: function(e) { + this._peopleButton = e; + }, + + _collectDirectoryButton: function(e) { + this._directoryButton = e; + }, + + _collectCreateRoomButton: function(e) { + this._createRoomButton = e; + }, + + render: function() { + const HomeButton = sdk.getComponent('elements.HomeButton'); + const StartChatButton = sdk.getComponent('elements.StartChatButton'); + const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton'); + const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton'); + const SettingsButton = sdk.getComponent('elements.SettingsButton'); + const GroupsButton = sdk.getComponent('elements.GroupsButton'); + + const groupsButton = SettingsStore.getValue("TagPanel.disableTagPanel") ? + : null; + + return ( +
+
+ +
+ +
+
+ +
+
+ +
+ { groupsButton } + + + +
+
+ ); + }, +}); diff --git a/src/components/structures/CompatibilityPage.js b/src/components/structures/CompatibilityPage.js new file mode 100644 index 0000000000..4cbaab3dfa --- /dev/null +++ b/src/components/structures/CompatibilityPage.js @@ -0,0 +1,73 @@ +/* +Copyright 2015, 2016 OpenMarket 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. +*/ + +'use strict'; + +var React = require('react'); +import { _t } from '../../languageHandler'; + +module.exports = React.createClass({ + displayName: 'CompatibilityPage', + propTypes: { + onAccept: React.PropTypes.func + }, + + getDefaultProps: function() { + return { + onAccept: function() {} // NOP + }; + }, + + onAccept: function() { + this.props.onAccept(); + }, + + render: function() { + + return ( +
+
+

{ _t("Sorry, your browser is not able to run Riot.", {}, { 'b': (sub) => {sub} }) }

+

+ { _t("Riot uses many advanced browser features, some of which are not available or experimental in your current browser.") } +

+

+ { _t('Please install Chrome or Firefox for the best experience.', + {}, + { + 'chromeLink': (sub) => {sub}, + 'firefoxLink': (sub) => {sub}, + }, + )} + { _t('Safari and Opera work too.', + {}, + { + 'safariLink': (sub) => {sub}, + 'operaLink': (sub) => {sub}, + }, + )} +

+

+ { _t("With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!") } +

+ +
+
+ ); + } +}); diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index e5a62b8345..7295fd45d3 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 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,90 +16,176 @@ limitations under the License. */ -'use strict'; - -var classNames = require('classnames'); -var React = require('react'); -var ReactDOM = require('react-dom'); +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and // pass in a custom control as the actual body. -module.exports = { - ContextualMenuContainerId: "mx_ContextualMenu_Container", +const ContextualMenuContainerId = "mx_ContextualMenu_Container"; +function getOrCreateContainer() { + let container = document.getElementById(ContextualMenuContainerId); + + if (!container) { + container = document.createElement("div"); + container.id = ContextualMenuContainerId; + document.body.appendChild(container); + } + + return container; +} + +export default class ContextualMenu extends React.Component { propTypes: { - menuWidth: React.PropTypes.number, - menuHeight: React.PropTypes.number, - chevronOffset: React.PropTypes.number, - menuColour: React.PropTypes.string, - }, + top: PropTypes.number, + bottom: PropTypes.number, + left: PropTypes.number, + right: PropTypes.number, + menuWidth: PropTypes.number, + menuHeight: PropTypes.number, + chevronOffset: PropTypes.number, + menuColour: PropTypes.string, + chevronFace: PropTypes.string, // top, bottom, left, right + // Function to be called on menu close + onFinished: PropTypes.func, + menuPaddingTop: PropTypes.number, + menuPaddingRight: PropTypes.number, + menuPaddingBottom: PropTypes.number, + menuPaddingLeft: PropTypes.number, - getOrCreateContainer: function() { - var container = document.getElementById(this.ContextualMenuContainerId); + // If true, insert an invisible screen-sized element behind the + // menu that when clicked will close it. + hasBackground: PropTypes.bool, - if (!container) { - container = document.createElement("div"); - container.id = this.ContextualMenuContainerId; - document.body.appendChild(container); + // The component to render as the context menu + elementClass: PropTypes.element.isRequired, + // on resize callback + windowResize: PropTypes.func, + // method to close menu + closeMenu: PropTypes.func, + }; + + constructor() { + super(); + this.state = { + contextMenuRect: null, + }; + + this.onContextMenu = this.onContextMenu.bind(this); + this.collectContextMenuRect = this.collectContextMenuRect.bind(this); + } + + collectContextMenuRect(element) { + // We don't need to clean up when unmounting, so ignore + if (!element) return; + + this.setState({ + contextMenuRect: element.getBoundingClientRect(), + }); + } + + onContextMenu(e) { + if (this.props.closeMenu) { + this.props.closeMenu(); + + e.preventDefault(); + const x = e.clientX; + const y = e.clientY; + + // XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst + // a context menu and its click-guard are up without completely rewriting how the context menus work. + setImmediate(() => { + const clickEvent = document.createEvent('MouseEvents'); + clickEvent.initMouseEvent( + 'contextmenu', true, true, window, 0, + 0, 0, x, y, false, false, + false, false, 0, null, + ); + document.elementFromPoint(x, y).dispatchEvent(clickEvent); + }); + } + } + + render() { + const position = {}; + let chevronFace = null; + + const props = this.props; + + if (props.top) { + position.top = props.top; + } else { + position.bottom = props.bottom; } - return container; - }, + if (props.left) { + position.left = props.left; + chevronFace = 'left'; + } else { + position.right = props.right; + chevronFace = 'right'; + } - createMenu: function(Element, props) { - var self = this; + const contextMenuRect = this.state.contextMenuRect || null; + const padding = 10; - var closeMenu = function() { - ReactDOM.unmountComponentAtNode(self.getOrCreateContainer()); + const chevronOffset = {}; + if (props.chevronFace) { + chevronFace = props.chevronFace; + } + if (chevronFace === 'top' || chevronFace === 'bottom') { + chevronOffset.left = props.chevronOffset; + } else { + const target = position.top; - if (props && props.onFinished) { - props.onFinished.apply(null, arguments); + // By default, no adjustment is made + let adjusted = target; + + // If we know the dimensions of the context menu, adjust its position + // such that it does not leave the (padded) window. + if (contextMenuRect) { + adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding); } - }; - var position = { - top: props.top, - }; - - var chevronOffset = {}; - if (props.chevronOffset) { - chevronOffset.top = props.chevronOffset; + position.top = adjusted; + chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted); } // 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; } @@ -111,21 +198,54 @@ module.exports = { menuStyle["backgroundColor"] = props.menuColour; } + if (!isNaN(Number(props.menuPaddingTop))) { + menuStyle["paddingTop"] = props.menuPaddingTop; + } + if (!isNaN(Number(props.menuPaddingLeft))) { + menuStyle["paddingLeft"] = props.menuPaddingLeft; + } + if (!isNaN(Number(props.menuPaddingBottom))) { + menuStyle["paddingBottom"] = props.menuPaddingBottom; + } + if (!isNaN(Number(props.menuPaddingRight))) { + menuStyle["paddingRight"] = props.menuPaddingRight; + } + + const ElementClass = props.elementClass; + // 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 = ( -
-
- {chevron} - -
-
- + return
+
+ { chevron } +
- ); + { props.hasBackground &&
} + +
; + } +} - ReactDOM.render(menu, this.getOrCreateContainer()); +export function createMenu(ElementClass, props, hasBackground=true) { + const closeMenu = function(...args) { + ReactDOM.unmountComponentAtNode(getOrCreateContainer()); - return {close: closeMenu}; - }, -}; + if (props && props.onFinished) { + props.onFinished.apply(null, args); + } + }; + + // We only reference closeMenu once per call to createMenu + const menu = ; + + ReactDOM.render(menu, getOrCreateContainer()); + + return {close: closeMenu}; +} diff --git a/src/components/structures/CreateRoom.js b/src/components/structures/CreateRoom.js index 7ecc315ba7..2bb9adb544 100644 --- a/src/components/structures/CreateRoom.js +++ b/src/components/structures/CreateRoom.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; import React from 'react'; +import PropTypes from 'prop-types'; import { _t } from '../../languageHandler'; import sdk from '../../index'; import MatrixClientPeg from '../../MatrixClientPeg'; @@ -30,8 +31,8 @@ module.exports = React.createClass({ displayName: 'CreateRoom', propTypes: { - onRoomCreated: React.PropTypes.func, - collapsedRhs: React.PropTypes.bool, + onRoomCreated: PropTypes.func, + collapsedRhs: PropTypes.bool, }, phases: { @@ -61,7 +62,7 @@ module.exports = React.createClass({ }, onCreateRoom: function() { - var options = {}; + const options = {}; if (this.state.room_name) { options.name = this.state.room_name; @@ -79,14 +80,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 +95,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 +117,7 @@ module.exports = React.createClass({ phase: this.phases.CREATING, }); - var self = this; + const self = this; deferred.then(function(resp) { self.setState({ @@ -209,7 +210,7 @@ module.exports = React.createClass({ onAliasChanged: function(alias) { this.setState({ - alias: alias + alias: alias, }); }, @@ -220,64 +221,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 (
- +
-
-