diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles
index f501f373cd..42818244b3 100644
--- a/.eslintignore.errorfiles
+++ b/.eslintignore.errorfiles
@@ -6,39 +6,57 @@ 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/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/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/RoomSubList.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/ViewSource.js
src/components/views/avatars/BaseAvatar.js
+src/components/views/avatars/GroupAvatar.js
src/components/views/avatars/MemberAvatar.js
src/components/views/create_room/RoomAlias.js
+src/components/views/dialogs/BugReportDialog.js
+src/components/views/dialogs/ChangelogDialog.js
src/components/views/dialogs/ChatCreateOrReuseDialog.js
src/components/views/dialogs/DeactivateAccountDialog.js
+src/components/views/dialogs/SetPasswordDialog.js
src/components/views/dialogs/UnknownDeviceDialog.js
+src/components/views/directory/NetworkDropdown.js
src/components/views/elements/AddressSelector.js
src/components/views/elements/DeviceVerifyButtons.js
src/components/views/elements/DirectorySearchBox.js
src/components/views/elements/EditableText.js
+src/components/views/elements/ImageView.js
+src/components/views/elements/InlineSpinner.js
src/components/views/elements/MemberEventListSummary.js
+src/components/views/elements/Spinner.js
src/components/views/elements/TintableSvg.js
src/components/views/elements/UserSelector.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/InteractiveAuthEntryComponents.js
src/components/views/login/PasswordLogin.js
src/components/views/login/RegistrationForm.js
src/components/views/login/ServerConfig.js
src/components/views/messages/MFileBody.js
-src/components/views/messages/MImageBody.js
src/components/views/messages/RoomAvatarEvent.js
src/components/views/messages/TextualBody.js
src/components/views/room_settings/AliasSettings.js
@@ -55,12 +73,14 @@ 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/ReadReceiptMarker.js
+src/components/views/rooms/RoomDropTarget.js
src/components/views/rooms/RoomList.js
src/components/views/rooms/RoomPreviewBar.js
src/components/views/rooms/RoomSettings.js
src/components/views/rooms/RoomTile.js
+src/components/views/rooms/RoomTooltip.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
@@ -69,6 +89,8 @@ 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/IntegrationsManager.js
+src/components/views/settings/Notifications.js
src/ContentMessages.js
src/HtmlUtils.js
src/ImageUtils.js
@@ -78,9 +100,16 @@ 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
@@ -89,10 +118,12 @@ src/ScalarAuthClient.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
@@ -102,8 +133,9 @@ test/components/structures/MessagePanel-test.js
test/components/structures/ScrollPanel-test.js
test/components/structures/TimelinePanel-test.js
test/components/views/dialogs/InteractiveAuthDialog-test.js
-test/components/views/elements/MemberEventListSummary-test.js
test/components/views/login/RegistrationForm-test.js
test/components/views/rooms/MessageComposerInput-test.js
test/mock-clock.js
+test/notifications/ContentRules-test.js
+test/notifications/PushRuleVectorState-test.js
test/stores/RoomViewStore-test.js
diff --git a/.eslintrc.js b/.eslintrc.js
index c6aeb0d1be..bf423a1ad8 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -41,7 +41,8 @@ module.exports = {
"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"],
@@ -50,7 +51,12 @@ module.exports = {
//
//
// https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/docs/rules/jsx-curly-spacing.md
- "react/jsx-curly-spacing": ["error", {"when": "never", "children": {"when": "always"}}],
+ //
+ // 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
@@ -88,7 +94,6 @@ module.exports = {
"valid-jsdoc": ["warn"],
"new-cap": ["warn"],
"key-spacing": ["warn"],
- "arrow-parens": ["warn"],
"prefer-const": ["warn"],
// crashes currently: https://github.com/eslint/eslint/issues/6274
diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh
index 87200871a5..eeba4d0b7e 100755
--- a/.travis-test-riot.sh
+++ b/.travis-test-riot.sh
@@ -9,16 +9,9 @@ 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"
-
+scripts/fetchdep.sh vector-im riot-web
cd "$RIOT_WEB_DIR"
-git checkout "$curbranch" || git checkout develop
-
mkdir node_modules
npm install
diff --git a/.travis.yml b/.travis.yml
index 4137d754bf..ec07243a28 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,5 @@ addons:
chrome: stable
install:
- npm install
- - (cd node_modules/matrix-js-sdk && npm install)
script:
./scripts/travis.sh
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 87459882c9..b161a9d908 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,678 @@
+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)
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 3a2d66739e..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
diff --git a/package-lock.json b/package-lock.json
index 6dd02674be..f183f1635d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "0.10.7",
+ "version": "0.12.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -268,7 +268,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
- "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@@ -1203,10 +1203,7 @@
"boom": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
- "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
- "requires": {
- "hoek": "4.2.0"
- }
+ "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE="
},
"brace-expansion": {
"version": "1.1.8",
@@ -1238,6 +1235,12 @@
"resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz",
"integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc="
},
+ "browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true
+ },
"browserify-aes": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-0.4.0.tgz",
@@ -1254,6 +1257,14 @@
"dev": true,
"requires": {
"pako": "0.2.9"
+ },
+ "dependencies": {
+ "pako": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+ "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=",
+ "dev": true
+ }
}
},
"buffer": {
@@ -1449,9 +1460,9 @@
"dev": true
},
"commonmark": {
- "version": "0.27.0",
- "resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.27.0.tgz",
- "integrity": "sha1-2GwmK5YoIelIPGnFR7xYhAwEezQ=",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.28.1.tgz",
+ "integrity": "sha1-Buq41SM4uDn6Gi11rwCF7tGxvq4=",
"requires": {
"entities": "1.1.1",
"mdurl": "1.0.1",
@@ -1580,10 +1591,7 @@
"boom": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
- "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
- "requires": {
- "hoek": "4.2.0"
- }
+ "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw=="
}
}
},
@@ -1705,12 +1713,6 @@
"integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
"dev": true
},
- "diff": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz",
- "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=",
- "dev": true
- },
"doctrine": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz",
@@ -2139,7 +2141,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
- "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@@ -2183,15 +2185,26 @@
}
},
"eslint-plugin-react": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.4.0.tgz",
- "integrity": "sha512-tvjU9u3VqmW2vVuYnE8Qptq+6ji4JltjOjJ9u7VAOxVYkUkyBZWRvNYKbDv5fN+L6wiA+4we9+qQahZ0m63XEA==",
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.7.0.tgz",
+ "integrity": "sha512-KC7Snr4YsWZD5flu6A5c0AcIZidzW3Exbqp7OT67OaD2AppJtlBr/GuPrW/vaQM/yfZotEvKAdrxrO+v8vwYJA==",
"dev": true,
"requires": {
- "doctrine": "2.0.0",
+ "doctrine": "2.1.0",
"has": "1.0.1",
"jsx-ast-utils": "2.0.1",
"prop-types": "15.6.0"
+ },
+ "dependencies": {
+ "doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "requires": {
+ "esutils": "2.0.2"
+ }
+ }
}
},
"espree": {
@@ -2238,7 +2251,7 @@
"estree-walker": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.5.0.tgz",
- "integrity": "sha512-/bEAy+yKAZQrEWUhGmS3H9XpGqSDBtRzX0I2PgMw9kA2n1jN22uV5B5p7MFdZdvWdXCRJztXAfx6ZeRfgkEETg==",
+ "integrity": "sha1-quO1fELeuAEONJyJJGLw5xxd0ao=",
"dev": true
},
"esutils": {
@@ -2554,6 +2567,22 @@
}
}
},
+ "focus-trap": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-2.4.3.tgz",
+ "integrity": "sha512-sT5Ip9nyAIxWq8Apt1Fdv6yTci5GotaOtO5Ro1/+F3PizttNBcCYz8j/Qze54PPFK73KUbOqh++HUCiyNPqvhA==",
+ "requires": {
+ "tabbable": "1.1.2"
+ }
+ },
+ "focus-trap-react": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-3.1.2.tgz",
+ "integrity": "sha512-MoQmONoy9gRPyrC5DGezkcOMGgx7MtIOAQDHe098UtL2sA2vmucJwEmQisb+8LRXNYFHxuw5zJ1oLFeKu4Mteg==",
+ "requires": {
+ "focus-trap": "2.4.3"
+ }
+ },
"for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -2629,7 +2658,7 @@
"fsevents": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz",
- "integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==",
+ "integrity": "sha1-MoK3E/s62A7eDp/PRhG1qm/AM/Q=",
"dev": true,
"optional": true,
"requires": {
@@ -3562,6 +3591,11 @@
"assert-plus": "1.0.0"
}
},
+ "gfm.css": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/gfm.css/-/gfm.css-1.1.2.tgz",
+ "integrity": "sha512-KhK3rqxMj+UTLRxWnfUA5n8XZYMWfHrrcCxtWResYR2B3hWIqBM6v9FPGZSlVuX+ScLewizOvNkjYXuPs95ThQ=="
+ },
"glob": {
"version": "5.0.15",
"resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
@@ -3596,7 +3630,7 @@
"globals": {
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
- "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
+ "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=",
"dev": true
},
"globby": {
@@ -3616,7 +3650,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
- "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@@ -3635,12 +3669,6 @@
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
"dev": true
},
- "growl": {
- "version": "1.9.2",
- "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz",
- "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=",
- "dev": true
- },
"har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@@ -3709,19 +3737,24 @@
"requires": {
"boom": "4.3.1",
"cryptiles": "3.1.2",
- "hoek": "4.2.0",
"sntp": "2.0.2"
}
},
- "highlight.js": {
- "version": "8.9.1",
- "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-8.9.1.tgz",
- "integrity": "sha1-uKnFSTISqTkvAiK2SclhFJfr+4g="
+ "he": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+ "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
+ "dev": true
},
- "hoek": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz",
- "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ=="
+ "highlight.js": {
+ "version": "9.12.0",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz",
+ "integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4="
+ },
+ "hoist-non-react-statics": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz",
+ "integrity": "sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w=="
},
"home-or-tmp": {
"version": "2.0.0",
@@ -3862,7 +3895,6 @@
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz",
"integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=",
- "dev": true,
"requires": {
"loose-envify": "1.3.1"
}
@@ -4137,30 +4169,6 @@
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
- "jade": {
- "version": "0.26.3",
- "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz",
- "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=",
- "dev": true,
- "requires": {
- "commander": "0.6.1",
- "mkdirp": "0.3.0"
- },
- "dependencies": {
- "commander": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz",
- "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=",
- "dev": true
- },
- "mkdirp": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
- "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=",
- "dev": true
- }
- }
- },
"jquery": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz",
@@ -4303,7 +4311,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
- "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@@ -4472,6 +4480,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
},
+ "lodash-es": {
+ "version": "4.17.10",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.10.tgz",
+ "integrity": "sha512-iesFYPmxYYGTcmQK0sL8bX3TGHyM6b2qREaB4kamHfQyfPJP0xgoGxp19nsH16nsfquLdiyKyX3mQkfiSGV8Rg=="
+ },
"lodash.assign": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
@@ -4547,17 +4560,28 @@
"dev": true
},
"matrix-js-sdk": {
- "version": "0.8.5",
- "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-0.8.5.tgz",
- "integrity": "sha1-1ZAVTx53ADVyZw+p28rH5APnbk8=",
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-0.10.1.tgz",
+ "integrity": "sha512-BLo+Okn2o///TyWBKtjFXvhlD32vGfr10eTE51hHx/jwaXO82VyGMzMi+IDPS4SDYUbvXI7PpamECeh9TXnV2w==",
"requires": {
"another-json": "0.2.0",
+ "babel-runtime": "6.26.0",
"bluebird": "3.5.1",
"browser-request": "0.3.3",
"content-type": "1.0.4",
"request": "2.83.0"
}
},
+ "matrix-mock-request": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/matrix-mock-request/-/matrix-mock-request-1.2.1.tgz",
+ "integrity": "sha1-2aWrqNPYJG6I/3YyWYuZwUE/QjI=",
+ "dev": true,
+ "requires": {
+ "bluebird": "3.5.1",
+ "expect": "1.20.2"
+ }
+ },
"matrix-react-test-utils": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/matrix-react-test-utils/-/matrix-react-test-utils-0.1.1.tgz",
@@ -4579,6 +4603,11 @@
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"dev": true
},
+ "memoize-one": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-3.1.1.tgz",
+ "integrity": "sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA=="
+ },
"memory-fs": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@@ -4632,7 +4661,7 @@
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
- "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
"requires": {
"brace-expansion": "1.1.8"
}
@@ -4660,75 +4689,73 @@
}
},
"mocha": {
- "version": "2.5.3",
- "resolved": "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz",
- "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.1.1.tgz",
+ "integrity": "sha512-kKKs/H1KrMMQIEsWNxGmb4/BGsmj0dkeyotEvbrAuQ01FcWRLssUNXCEUZk6SZtyJBi6EE7SL0zDDtItw1rGhw==",
"dev": true,
"requires": {
- "commander": "2.3.0",
- "debug": "2.2.0",
- "diff": "1.4.0",
- "escape-string-regexp": "1.0.2",
- "glob": "3.2.11",
- "growl": "1.9.2",
- "jade": "0.26.3",
+ "browser-stdout": "1.3.1",
+ "commander": "2.11.0",
+ "debug": "3.1.0",
+ "diff": "3.5.0",
+ "escape-string-regexp": "1.0.5",
+ "glob": "7.1.2",
+ "growl": "1.10.3",
+ "he": "1.1.1",
+ "minimatch": "3.0.4",
"mkdirp": "0.5.1",
- "supports-color": "1.2.0",
- "to-iso-string": "0.0.2"
+ "supports-color": "4.4.0"
},
"dependencies": {
- "commander": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz",
- "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=",
- "dev": true
- },
"debug": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
- "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
- "ms": "0.7.1"
+ "ms": "2.0.0"
}
},
- "escape-string-regexp": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz",
- "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=",
+ "diff": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
"dev": true
},
"glob": {
- "version": "3.2.11",
- "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
- "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"requires": {
+ "fs.realpath": "1.0.0",
+ "inflight": "1.0.6",
"inherits": "2.0.3",
- "minimatch": "0.3.0"
+ "minimatch": "3.0.4",
+ "once": "1.4.0",
+ "path-is-absolute": "1.0.1"
}
},
- "minimatch": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz",
- "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=",
- "dev": true,
- "requires": {
- "lru-cache": "2.2.4",
- "sigmund": "1.0.1"
- }
+ "growl": {
+ "version": "1.10.3",
+ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz",
+ "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
+ "dev": true
},
- "ms": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
- "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=",
+ "has-flag": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
+ "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
"dev": true
},
"supports-color": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz",
- "integrity": "sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4=",
- "dev": true
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
+ "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
+ "dev": true,
+ "requires": {
+ "has-flag": "2.0.0"
+ }
}
}
},
@@ -4979,10 +5006,9 @@
}
},
"pako": {
- "version": "0.2.9",
- "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
- "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=",
- "dev": true
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
+ "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg=="
},
"parallelshell": {
"version": "3.0.2",
@@ -5205,10 +5231,23 @@
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
"dev": true
},
+ "raf": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz",
+ "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==",
+ "requires": {
+ "performance-now": "2.1.0"
+ }
+ },
+ "raf-schd": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-2.1.1.tgz",
+ "integrity": "sha512-ngcBQygUeE3kHlOaBSqgWKv7BT9kx5kQ6fAwFJRNRT7TD54M+hx1kpNHb8sONRskcYQedJg2RC2xKlAHRUQBig=="
+ },
"randomatic": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
- "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==",
+ "integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=",
"dev": true,
"requires": {
"is-number": "3.0.0",
@@ -5287,6 +5326,23 @@
"integrity": "sha1-wStu/cIkfBDae4dw0YUICnsEcVY=",
"dev": true
},
+ "react-beautiful-dnd": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz",
+ "integrity": "sha512-d73RMu4QOFCyjUELLWFyY/EuclnfqulI9pECx+2gIuJvV0ycf1uR88o+1x0RSB9ILD70inHMzCBKNkWVbbt+vA==",
+ "requires": {
+ "babel-runtime": "6.26.0",
+ "invariant": "2.2.2",
+ "memoize-one": "3.1.1",
+ "prop-types": "15.6.0",
+ "raf-schd": "2.1.1",
+ "react-motion": "0.5.2",
+ "react-redux": "5.0.7",
+ "redux": "3.7.2",
+ "redux-thunk": "2.2.0",
+ "reselect": "3.0.1"
+ }
+ },
"react-dom": {
"version": "15.6.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz",
@@ -5304,6 +5360,43 @@
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279810d05319ac5ff1bd34910bff32325c7b"
}
},
+ "react-motion": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/react-motion/-/react-motion-0.5.2.tgz",
+ "integrity": "sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==",
+ "requires": {
+ "performance-now": "0.2.0",
+ "prop-types": "15.6.0",
+ "raf": "3.4.0"
+ },
+ "dependencies": {
+ "performance-now": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
+ "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU="
+ }
+ }
+ },
+ "react-redux": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.7.tgz",
+ "integrity": "sha512-5VI8EV5hdgNgyjfmWzBbdrqUkrVRKlyTKk1sGH3jzM2M2Mhj/seQgPXaz6gVAj2lz/nz688AdTqMO18Lr24Zhg==",
+ "requires": {
+ "hoist-non-react-statics": "2.5.0",
+ "invariant": "2.2.2",
+ "lodash": "4.17.10",
+ "lodash-es": "4.17.10",
+ "loose-envify": "1.3.1",
+ "prop-types": "15.6.0"
+ },
+ "dependencies": {
+ "lodash": {
+ "version": "4.17.10",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
+ "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg=="
+ }
+ }
+ },
"readable-stream": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
@@ -5350,6 +5443,22 @@
"resolve": "1.4.0"
}
},
+ "redux": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
+ "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
+ "requires": {
+ "lodash": "4.17.4",
+ "lodash-es": "4.17.10",
+ "loose-envify": "1.3.1",
+ "symbol-observable": "1.2.0"
+ }
+ },
+ "redux-thunk": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.2.0.tgz",
+ "integrity": "sha1-5hWhbha0ehmlFXZhM9Hj6Zt4UuU="
+ },
"regenerate": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz",
@@ -5498,6 +5607,11 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
+ "reselect": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
+ "integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc="
+ },
"resolve": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz",
@@ -5544,7 +5658,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
- "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@@ -5642,7 +5756,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
- "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@@ -5655,12 +5769,6 @@
}
}
},
- "sigmund": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
- "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=",
- "dev": true
- },
"sinon": {
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz",
@@ -5688,10 +5796,7 @@
"sntp": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz",
- "integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=",
- "requires": {
- "hoek": "4.2.0"
- }
+ "integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys="
},
"socket.io": {
"version": "1.7.3",
@@ -5848,24 +5953,30 @@
"dev": true
},
"source-map-loader": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.1.6.tgz",
- "integrity": "sha1-wJkD2m1zueU7ftjuUkVZcFHpjpE=",
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.3.tgz",
+ "integrity": "sha512-MYbFX9DYxmTQFfy2v8FC1XZwpwHKYxg3SK8Wb7VPBKuhDjz8gi9re2819MsG4p49HDyiOSUKlmZ+nQBArW5CGw==",
"dev": true,
"requires": {
- "async": "0.9.2",
+ "async": "2.6.0",
"loader-utils": "0.2.17",
- "source-map": "0.1.43"
+ "source-map": "0.6.1"
},
"dependencies": {
- "source-map": {
- "version": "0.1.43",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
- "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
+ "async": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz",
+ "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==",
"dev": true,
"requires": {
- "amdefine": "1.0.1"
+ "lodash": "4.17.4"
}
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
}
}
},
@@ -5917,7 +6028,7 @@
"stream-http": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz",
- "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==",
+ "integrity": "sha1-QKBQ7I3DtTsz2ZCUFcAsC/Gr+60=",
"dev": true,
"requires": {
"builtin-status-codes": "3.0.0",
@@ -5983,6 +6094,16 @@
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
"dev": true
},
+ "symbol-observable": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
+ },
+ "tabbable": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-1.1.2.tgz",
+ "integrity": "sha512-77oqsKEPrxIwgRcXUwipkj9W5ItO97L6eUT1Ar7vh+El16Zm4M6V+YU1cbipHEa6q0Yjw8O3Hoh8oRgatV5s7A=="
+ },
"table": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz",
@@ -6111,12 +6232,6 @@
"integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=",
"dev": true
},
- "to-iso-string": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz",
- "integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=",
- "dev": true
- },
"tough-cookie": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",
diff --git a/package.json b/package.json
index eb2cabf854..6c34979c43 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "0.11.3",
+ "version": "0.12.5",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@@ -25,7 +25,8 @@
"release.sh",
"scripts",
"src",
- "test"
+ "test",
+ "res"
],
"bin": {
"reskindex": "scripts/reskindex.js",
@@ -43,7 +44,7 @@
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
"lint": "eslint src/",
"lintall": "eslint src/ test/",
- "lintwithexclusions": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
+ "lintwithexclusions": "eslint --max-warnings 20 --ignore-path .eslintignore.errorfiles src test",
"clean": "rimraf lib",
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt",
"test": "karma start --single-run=true --browsers ChromeHeadless",
@@ -65,21 +66,25 @@
"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",
"lodash": "^4.13.1",
- "matrix-js-sdk": "0.9.2",
+ "lolex": "2.3.2",
+ "matrix-js-sdk": "0.10.2",
"optimist": "^0.6.1",
+ "pako": "^1.0.5",
"prop-types": "^15.5.8",
"querystring": "^0.2.0",
- "react": "^15.4.0",
+ "react": "^15.6.0",
"react-addons-css-transition-group": "15.3.2",
- "react-dnd": "^2.1.4",
- "react-dnd-html5-backend": "^2.1.2",
- "react-dom": "^15.4.0",
+ "react-beautiful-dnd": "^4.0.1",
+ "react-dom": "^15.6.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"sanitize-html": "^1.14.1",
"text-encoding-utf-8": "^1.0.1",
@@ -107,7 +112,7 @@
"eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^4.0.1",
"eslint-plugin-flowtype": "^2.30.0",
- "eslint-plugin-react": "^7.4.0",
+ "eslint-plugin-react": "^7.7.0",
"estree-walker": "^0.5.0",
"expect": "^1.16.0",
"flow-parser": "^0.57.3",
@@ -122,14 +127,15 @@
"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",
+ "mocha": "^5.0.5",
"parallelshell": "^3.0.2",
"react-addons-test-utils": "^15.4.0",
"require-json": "0.0.1",
"rimraf": "^2.4.3",
"sinon": "^1.17.3",
- "source-map-loader": "^0.1.5",
+ "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..c4cda6821e
--- /dev/null
+++ b/res/css/_common.scss
@@ -0,0 +1,362 @@
+/*
+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;
+}
+
+::-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..2734939ae3
--- /dev/null
+++ b/res/css/_components.scss
@@ -0,0 +1,110 @@
+// 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/_SetEmailDialog.scss";
+@import "./views/dialogs/_SetMxIdDialog.scss";
+@import "./views/dialogs/_SetPasswordDialog.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/_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/_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..a0191b92cf
--- /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: 2000;
+}
+
+.mx_ContextualMenu_background {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ opacity: 1.0;
+ z-index: 2000;
+}
+
+.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: 2001;
+}
+
+.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..96ed5878ac
--- /dev/null
+++ b/res/css/structures/_LeftPanel.scss
@@ -0,0 +1,129 @@
+/*
+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_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_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..156b1709fe
--- /dev/null
+++ b/res/css/structures/_MatrixChat.scss
@@ -0,0 +1,104 @@
+/*
+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 .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..ca7431eac2
--- /dev/null
+++ b/res/css/structures/_RoomStatusBar.scss
@@ -0,0 +1,181 @@
+/*
+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 {
+ 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;
+}
+
+.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..a2863460ad
--- /dev/null
+++ b/res/css/structures/_RoomSubList.scss
@@ -0,0 +1,244 @@
+/*
+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;
+}
+
+/*
+.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..b8e1190375
--- /dev/null
+++ b/res/css/structures/_RoomView.scss
@@ -0,0 +1,272 @@
+/*
+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;
+
+ -webkit-transition: all .2s ease-out;
+ -moz-transition: all .2s ease-out;
+ -ms-transition: all .2s ease-out;
+ -o-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..ab1d4feac3
--- /dev/null
+++ b/res/css/structures/_TagPanel.scss
@@ -0,0 +1,126 @@
+/*
+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;
+ cursor: pointer;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.mx_TagPanel .mx_TagPanel_clearButton {
+ /* 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..888f147d21
--- /dev/null
+++ b/res/css/views/dialogs/_CreateRoomDialog.scss
@@ -0,0 +1,33 @@
+/*
+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 {
+ font-size: 15px;
+ border-radius: 3px;
+ border: 1px solid $input-border-color;
+ padding: 9px;
+ color: $primary-fg-color;
+ background-color: $primary-bg-color;
+}
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..8918373ecf
--- /dev/null
+++ b/res/css/views/dialogs/_DevtoolsDialog.scss
@@ -0,0 +1,166 @@
+/*
+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_RoomStateExplorer_button, .mx_DevTools_RoomStateExplorer_query {
+ margin-bottom: 10px;
+}
+
+.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
+{
+ padding-bottom: 21px;
+ display: table-cell;
+ font-weight: bold;
+ padding-right: 24px;
+}
+
+.mx_DevTools_inputCell {
+ display: table-cell;
+ padding-bottom: 21px;
+ 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_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/_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/_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..474a123455
--- /dev/null
+++ b/res/css/views/elements/_RichText.scss
@@ -0,0 +1,75 @@
+// 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_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 {
+ 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_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_RoomPill {
+ color: $accent-fg-color;
+ background-color: $rte-room-pill-color;
+ padding-right: 5px;
+}
+
+.mx_UserPill .mx_BaseAvatar,
+.mx_RoomPill .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..be69b15f37
--- /dev/null
+++ b/res/css/views/globals/_MatrixToolbar.scss
@@ -0,0 +1,62 @@
+/*
+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_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;
+}
\ No newline at end of file
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/_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..1c809f0743
--- /dev/null
+++ b/res/css/views/messages/_MImageBody.scss
@@ -0,0 +1,24 @@
+/*
+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 {
+ max-width: 100%;
+}
\ No newline at end of file
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..3e6bbe5aa4
--- /dev/null
+++ b/res/css/views/messages/_MStickerBody.scss
@@ -0,0 +1,46 @@
+/*
+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 {
+ display: block;
+ margin-right: 34px;
+ min-height: 110px;
+ padding: 20px 0;
+}
+
+.mx_MStickerBody_image_container {
+ display: inline-block;
+ position: relative;
+}
+
+.mx_MStickerBody_image {
+ max-width: 100%;
+ opacity: 0;
+}
+
+.mx_MStickerBody_image_visible {
+ opacity: 1;
+}
+
+.mx_MStickerBody_placeholder {
+ position: absolute;
+ opacity: 1;
+}
+
+.mx_MStickerBody_placeholder_invisible {
+ transition: 500ms;
+ opacity: 0;
+}
diff --git a/res/css/views/messages/_MTextBody.scss b/res/css/views/messages/_MTextBody.scss
new file mode 100644
index 0000000000..fcf397fd2d
--- /dev/null
+++ b/res/css/views/messages/_MTextBody.scss
@@ -0,0 +1,24 @@
+/*
+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;
+}
+
+.mx_MTextBody pre{
+ overflow-y: auto;
+ max-height: 30vh;
+}
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..28d432686d
--- /dev/null
+++ b/res/css/views/rooms/_AppsDrawer.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_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_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 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..732ada088b
--- /dev/null
+++ b/res/css/views/rooms/_Autocomplete.scss
@@ -0,0 +1,93 @@
+.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 {
+ 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..ce2bf9c8a4
--- /dev/null
+++ b/res/css/views/rooms/_EventTile.scss
@@ -0,0 +1,538 @@
+/*
+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;
+ z-index: 2;
+}
+
+.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;
+ z-index: 1;
+ 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;
+ z-index: 2;
+ 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;
+}
+
+.mx_EventTile_content .markdown-body code {
+ // deliberate constants as we're behind an invert filter
+ background-color: #f8f8f8;
+ color: #333;
+}
+
+.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 {
+ position: relative;
+ 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 pre: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;
+}
+
+.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..0a708a8edc
--- /dev/null
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -0,0 +1,246 @@
+/*
+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_MessageComposer_wrapper {
+ max-width: 960px;
+ vertical-align: middle;
+ margin: auto;
+ border-top: 1px solid $primary-hairline-color;
+ position: relative;
+}
+
+.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;
+}
+
+.mx_MessageComposer_input {
+ flex: 1;
+ vertical-align: middle;
+ display: flex;
+ flex-direction: column;
+ min-height: 60px;
+ justify-content: center;
+ align-items: flex-start;
+ font-size: 14px;
+ margin-right: 6px;
+}
+
+@keyframes visualbell
+{
+ from { background-color: #faa }
+ to { background-color: $primary-bg-color }
+}
+
+.mx_MessageComposer_input_error {
+ animation: 0.2s visualbell;
+}
+
+.mx_MessageComposer_input_empty .public-DraftEditorPlaceholder-root {
+ display: none;
+}
+
+.mx_MessageComposer_input .DraftEditor-root {
+ width: 100%;
+ flex: 1;
+ word-break: break-word;
+ max-height: 120px;
+ min-height: 21px;
+ overflow: auto;
+}
+
+.mx_MessageComposer_input .DraftEditor-root .DraftEditor-editorContainer {
+ /* Ensure mx_UserPill and mx_RoomPill (see _RichText) are not obscured from the top */
+ padding-top: 2px;
+}
+
+.mx_MessageComposer .public-DraftStyleDefault-block {
+ overflow-x: hidden;
+}
+
+.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.public-DraftStyleDefault-pre 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..ca790ef8f0
--- /dev/null
+++ b/res/css/views/rooms/_PinnedEventTile.scss
@@ -0,0 +1,67 @@
+/*
+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 {
+ color: #868686;
+ font-size: 0.8em;
+ vertical-align: top;
+ display: block;
+ padding-bottom: 3px;
+}
+
+.mx_PinnedEventTile .mx_EventTile_content {
+ margin-left: 50px;
+ position: relative;
+ top: 0;
+ left: 0;
+}
+
+.mx_PinnedEventTile .mx_BaseAvatar {
+ float: left;
+ margin-right: 10px;
+}
+
+.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.8em;
+}
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..4013af4c7c
--- /dev/null
+++ b/res/css/views/rooms/_RoomSettings.scss
@@ -0,0 +1,247 @@
+/*
+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_leaveButton,
+.mx_RoomSettings_unbanButton {
+ @mixin mx_DialogButton;
+ position: relative;
+ margin-right: 8px;
+}
+
+.mx_RoomSettings_leaveButton:hover,
+.mx_RoomSettings_unbanButton:hover {
+ @mixin mx_DialogButton_hover;
+}
+
+.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/_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 @@
+
+
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 @@
+
+
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 @@
+
+
\ 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 @@
+
+
\ 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-text-bold-o-n.svg b/res/img/button-text-bold-o-n.svg
new file mode 100644
index 0000000000..161e740e90
--- /dev/null
+++ b/res/img/button-text-bold-o-n.svg
@@ -0,0 +1,17 @@
+
+
\ 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 @@
+
+
\ No newline at end of file
diff --git a/res/img/button-text-bullet-o-n.svg b/res/img/button-text-bullet-o-n.svg
new file mode 100644
index 0000000000..d4a40e889c
--- /dev/null
+++ b/res/img/button-text-bullet-o-n.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/res/img/button-text-bullet.svg b/res/img/button-text-bullet.svg
new file mode 100644
index 0000000000..ae3e640d8e
--- /dev/null
+++ b/res/img/button-text-bullet.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/res/img/button-text-code-o-n.svg b/res/img/button-text-code-o-n.svg
new file mode 100644
index 0000000000..8d1439c97b
--- /dev/null
+++ b/res/img/button-text-code-o-n.svg
@@ -0,0 +1,25 @@
+
+
\ No newline at end of file
diff --git a/res/img/button-text-code.svg b/res/img/button-text-code.svg
new file mode 100644
index 0000000000..24026cb709
--- /dev/null
+++ b/res/img/button-text-code.svg
@@ -0,0 +1,25 @@
+
+
\ 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-italic-o-n.svg b/res/img/button-text-italic-o-n.svg
new file mode 100644
index 0000000000..15fe588596
--- /dev/null
+++ b/res/img/button-text-italic-o-n.svg
@@ -0,0 +1,17 @@
+
+
\ 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 @@
+
+
\ No newline at end of file
diff --git a/res/img/button-text-numbullet-o-n.svg b/res/img/button-text-numbullet-o-n.svg
new file mode 100644
index 0000000000..869a2c2cc2
--- /dev/null
+++ b/res/img/button-text-numbullet-o-n.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/res/img/button-text-numbullet.svg b/res/img/button-text-numbullet.svg
new file mode 100644
index 0000000000..8e5b8b87b6
--- /dev/null
+++ b/res/img/button-text-numbullet.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/res/img/button-text-quote-o-n.svg b/res/img/button-text-quote-o-n.svg
new file mode 100644
index 0000000000..f8a86125c9
--- /dev/null
+++ b/res/img/button-text-quote-o-n.svg
@@ -0,0 +1,17 @@
+
+
\ No newline at end of file
diff --git a/res/img/button-text-quote.svg b/res/img/button-text-quote.svg
new file mode 100644
index 0000000000..d70c261f5d
--- /dev/null
+++ b/res/img/button-text-quote.svg
@@ -0,0 +1,17 @@
+
+
\ No newline at end of file
diff --git a/res/img/button-text-strike-o-n.svg b/res/img/button-text-strike-o-n.svg
new file mode 100644
index 0000000000..2914fcabe6
--- /dev/null
+++ b/res/img/button-text-strike-o-n.svg
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/res/img/button-text-strike.svg b/res/img/button-text-strike.svg
new file mode 100644
index 0000000000..5f262dc350
--- /dev/null
+++ b/res/img/button-text-strike.svg
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/res/img/button-text-underline-o-n.svg b/res/img/button-text-underline-o-n.svg
new file mode 100644
index 0000000000..870be3ce6a
--- /dev/null
+++ b/res/img/button-text-underline-o-n.svg
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/res/img/button-text-underline.svg b/res/img/button-text-underline.svg
new file mode 100644
index 0000000000..26f448539c
--- /dev/null
+++ b/res/img/button-text-underline.svg
@@ -0,0 +1,18 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
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 @@
+
+
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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ No newline at end of file
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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
+
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 @@
+
+
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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
+
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 @@
+
+
\ 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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
\ 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 @@
+
+
+
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 @@
+
+
+
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 @@
+
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
\ 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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
\ No newline at end of file
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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ No newline at end of file
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 @@
+
+
\ 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 @@
+
+
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 @@
+
+
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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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..31773ebd09
--- /dev/null
+++ b/res/themes/dark/css/_dark.scss
@@ -0,0 +1,204 @@
+
+// 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;
+
+// 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..5d5f5d7c90
--- /dev/null
+++ b/res/themes/light/css/_base.scss
@@ -0,0 +1,177 @@
+/* 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;
+$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;
+
+// ********************
+
+$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_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/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/travis.sh b/scripts/travis.sh
index c4a06c1bd1..48410ea904 100755
--- a/scripts/travis.sh
+++ b/scripts/travis.sh
@@ -2,6 +2,14 @@
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
diff --git a/src/Analytics.js b/src/Analytics.js
index 5c39b48a35..8ffce7077f 100644
--- a/src/Analytics.js
+++ b/src/Analytics.js
@@ -16,51 +16,75 @@
import { getCurrentLanguage, _t, _td } from './languageHandler';
import PlatformPeg from './PlatformPeg';
-import SdkConfig, { DEFAULTS } from './SdkConfig';
+import SdkConfig from './SdkConfig';
import Modal from './Modal';
import sdk from './index';
-function getRedactedHash() {
- return window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/");
+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 and hash separated with a `/`. This does not include query parameters.
function getRedactedUrl() {
- // hardcoded url to make piwik happy
- return 'https://riot.im/app/' + getRedactedHash();
+ const { origin, pathname, hash } = window.location;
+ return origin + pathname + getRedactedHash(hash);
}
const customVariables = {
'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',
},
};
@@ -92,6 +116,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;
}
@@ -143,7 +171,7 @@ class Analytics {
return true;
}
- trackPageChange() {
+ trackPageChange(generationTimeMs) {
if (this.disabled) return;
if (this.firstPage) {
// De-duplicate first page
@@ -151,6 +179,14 @@ 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']);
}
@@ -166,6 +202,7 @@ class Analytics {
}
_setVisitVariable(key, value) {
+ if (this.disabled) return;
this._paq.push(['setCustomVariable', customVariables[key].id, key, value, 'visit']);
}
@@ -173,8 +210,10 @@ class Analytics {
if (this.disabled) return;
const config = SdkConfig.get();
- const whitelistedHSUrls = config.piwik.whitelistedHSUrls || DEFAULTS.piwik.whitelistedHSUrls;
- const whitelistedISUrls = config.piwik.whitelistedISUrls || DEFAULTS.piwik.whitelistedISUrls;
+ 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));
@@ -187,36 +226,56 @@ class Analytics {
}
showDetailsModal() {
- const Tracker = window.Piwik.getAsyncTracker();
- const rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean);
+ 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:
+ description:
{ _t('The information being sent to us to help make Riot.im better includes:') }
{ rows.map((row) =>
{ _t(customVariables[row[0]].expl) }
-
{ row[1] }
+ { row[1] !== undefined &&
{ row[1] }
}
) }
-
-
-
- { _t('We also record each page you use in the app (currently ), your User Agent'
- + ' () and your device resolution ().',
- {},
- {
- CurrentPageHash: { getRedactedHash() },
- CurrentUserAgent: { navigator.userAgent },
- CurrentDeviceResolution: { resolution },
- },
+ { otherVariables.map((item, index) =>
+
+
{ _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.') }
diff --git a/src/ContentMessages.js b/src/ContentMessages.js
index 8d40b65124..7fe625f8b9 100644
--- a/src/ContentMessages.js
+++ b/src/ContentMessages.js
@@ -275,6 +275,13 @@ 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',
diff --git a/src/DateUtils.js b/src/DateUtils.js
index 91df1e46d5..108697238c 100644
--- a/src/DateUtils.js
+++ b/src/DateUtils.js
@@ -50,11 +50,15 @@ 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}`;
}
@@ -82,6 +86,17 @@ export function formatDate(date, showTwelveHour=false) {
return formatFullDate(date, showTwelveHour);
}
+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();
@@ -90,10 +105,17 @@ export function formatFullDate(date, showTwelveHour=false) {
monthName: months[date.getMonth()],
day: date.getDate(),
fullYear: date.getFullYear(),
- time: formatTime(date, showTwelveHour),
+ 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);
diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js
new file mode 100644
index 0000000000..792fd73733
--- /dev/null
+++ b/src/FromWidgetPostMessageApi.js
@@ -0,0 +1,210 @@
+/*
+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';
+
+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 {
+ 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
index ef9010cbf2..91380b6eed 100644
--- a/src/GroupAddressPicker.js
+++ b/src/GroupAddressPicker.js
@@ -19,31 +19,33 @@ import sdk from './';
import MultiInviter from './utils/MultiInviter';
import { _t } from './languageHandler';
import MatrixClientPeg from './MatrixClientPeg';
-import GroupStoreCache from './stores/GroupStoreCache';
+import GroupStore from './stores/GroupStore';
export function showGroupInviteDialog(groupId) {
- const description =
-
{ _t("Who would you like to add to this community?") }
-
- { _t(
- "Warning: any person you add to a community will be publicly "+
- "visible to anyone who knows the community ID",
- ) }
-
{ _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;
+ 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);
- },
+ _onGroupInviteFinished(groupId, addrs).then(resolve, reject);
+ },
+ });
});
}
@@ -87,7 +89,7 @@ function _onGroupInviteFinished(groupId, addrs) {
const addrTexts = addrs.map((addr) => addr.address);
- multiInviter.invite(addrTexts).then((completionStates) => {
+ return multiInviter.invite(addrTexts).then((completionStates) => {
// Show user any errors
const errorList = [];
for (const addr of Object.keys(completionStates)) {
@@ -114,11 +116,10 @@ function _onGroupInviteFinished(groupId, addrs) {
function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
const matrixClient = MatrixClientPeg.get();
- const groupStore = GroupStoreCache.getGroupStore(groupId);
const errorList = [];
return Promise.all(addrs.map((addr) => {
- return groupStore
- .addRoomToGroup(addr.address, addRoomsPublicly)
+ return GroupStore
+ .addRoomToGroup(groupId, addr.address, addRoomsPublicly)
.catch(() => { errorList.push(addr.address); })
.then(() => {
const roomId = addr.address;
diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js
index 0c262fe89a..7ca404be31 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,6 +17,8 @@ limitations under the License.
'use strict';
+import ReplyThread from "./components/views/elements/ReplyThread";
+
const React = require('react');
const sanitizeHtml = require('sanitize-html');
const highlight = require('highlight.js');
@@ -25,6 +27,7 @@ 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)
@@ -44,6 +47,8 @@ const SYMBOL_PATTERN = /([\u2100-\u2bff])/;
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
@@ -152,6 +157,25 @@ 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 sanitizeHtmlParams = {
allowedTags: [
'font', // custom to matrix for IRC-style font coloring
@@ -172,7 +196,7 @@ const sanitizeHtmlParams = {
// Lots of these won't come up by default because we don't allow them
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
// URL schemes we permit
- allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'],
+ allowedSchemes: PERMITTED_URL_SCHEMES,
allowProtocolRelative: false,
@@ -386,14 +410,16 @@ class TextHighlighter extends BaseHighlighter {
*
* 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
*/
export function bodyToHtml(content, highlights, opts={}) {
- const isHtml = (content.format === "org.matrix.custom.html");
- const body = isHtml ? content.formatted_body : escape(content.body);
+ const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
let bodyHasEmoji = false;
+ 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
@@ -409,9 +435,33 @@ export function bodyToHtml(content, highlights, opts={}) {
return highlighter.applyHighlights(safeText, safeHighlights).join('');
};
}
- safeBody = sanitizeHtml(body, sanitizeHtmlParams);
- bodyHasEmoji = containsEmoji(body);
- if (bodyHasEmoji) safeBody = unicodeToImage(safeBody);
+
+ let formattedBody = content.formatted_body;
+ if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody);
+ strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body;
+
+ 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, sanitizeHtmlParams);
+ } else {
+ // ... or if there are emoji, which we insert as HTML alongside the
+ // escaped plaintext body.
+ if (bodyHasEmoji) {
+ isDisplayedWithHtml = true;
+ safeBody = sanitizeHtml(escape(strippedBody), sanitizeHtmlParams);
+ }
+ }
+
+ // 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 sanitizeHtmlParams.textFilter;
}
@@ -419,7 +469,7 @@ export function bodyToHtml(content, highlights, opts={}) {
let emojiBody = false;
if (!opts.disableBigEmoji && bodyHasEmoji) {
EMOJI_REGEX.lastIndex = 0;
- const contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
+ const contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : '';
const match = EMOJI_REGEX.exec(contentBodyTrimmed);
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
}
@@ -427,9 +477,12 @@ export function bodyToHtml(content, highlights, opts={}) {
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/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/Lifecycle.js b/src/Lifecycle.js
index efd5c20d5c..7378e982ef 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.
@@ -64,33 +65,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 +102,9 @@ export function loadSession(opts) {
// fall back to login screen
return false;
- });
+ } catch (e) {
+ return _handleLoadSessionFailure(e);
+ }
}
/**
@@ -195,9 +198,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 +218,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 +255,7 @@ function _handleRestoreFailure(e) {
}
// try, try again
- return _restoreFromLocalStorage();
+ return loadSession();
});
}
@@ -362,7 +362,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
dis.dispatch({action: 'on_logged_in', teamToken: teamToken});
});
- startMatrixClient();
+ await startMatrixClient();
return MatrixClientPeg.get();
}
@@ -423,7 +423,7 @@ export function logout() {
* 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
@@ -437,7 +437,7 @@ function startMatrixClient() {
Presence.start();
DMRoomMap.makeShared().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.
diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js
index 14dfa91fa4..9d86a62de4 100644
--- a/src/MatrixClientPeg.js
+++ b/src/MatrixClientPeg.js
@@ -98,7 +98,6 @@ class MatrixClientPeg {
const opts = utils.deepCopy(this.opts);
// the react sdk doesn't work without this, so don't allow
opts.pendingEventOrdering = "detached";
- opts.disablePresence = true; // we do this manually
try {
const promise = this.matrixClient.store.startup();
@@ -175,4 +174,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 c9f08772e7..06a96824a7 100644
--- a/src/Modal.js
+++ b/src/Modal.js
@@ -22,6 +22,7 @@ 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";
@@ -80,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
@@ -104,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);
}
/**
@@ -136,8 +141,13 @@ 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) {
+ createDialogAsync(loader, props, className, isPriorityModal) {
const self = this;
const modal = {};
@@ -150,6 +160,14 @@ class ModalManager {
if (i >= 0) {
self._modals.splice(i, 1);
}
+
+ if (self._priorityModal === modal) {
+ self._priorityModal = null;
+
+ // XXX: This is destructive
+ self._modals = [];
+ }
+
self._reRender();
};
@@ -166,7 +184,12 @@ class ModalManager {
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};
@@ -187,12 +210,24 @@ 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;
}
- const modal = this._modals[0];
+ // 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 = (
diff --git a/src/Notifier.js b/src/Notifier.js
index e69bdf4461..b823c4df05 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -256,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;
diff --git a/src/Presence.js b/src/Presence.js
index 2652c64c96..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.
@@ -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));
}
}
@@ -56,27 +57,13 @@ class Presence {
return this.state;
}
- /**
- * Get the current status message.
- * @returns {String} the status message, may be null
- */
- getStatusMessage() {
- return this.statusMessage;
- }
-
/**
* Set the presence state.
* If the state has changed, the Home Server will be notified.
* @param {string} newState the new presence state (see PRESENCE enum)
- * @param {String} statusMessage an optional status message for the presence
- * @param {boolean} maintain true to have this status maintained by this tracker
*/
- setState(newState, statusMessage=null, maintain=false) {
- if (this.maintain) {
- // Don't update presence if we're maintaining a particular status
- return;
- }
- if (newState === this.state && statusMessage === this.statusMessage) {
+ setState(newState) {
+ if (newState === this.state) {
return;
}
if (PRESENCE_STATES.indexOf(newState) === -1) {
@@ -86,37 +73,21 @@ class Presence {
return;
}
const old_state = this.state;
- const old_message = this.statusMessage;
this.state = newState;
- this.statusMessage = statusMessage;
- this.maintain = maintain;
if (MatrixClientPeg.get().isGuest()) {
return; // don't try to set presence when a guest; it won't work.
}
- const updateContent = {
- presence: this.state,
- status_msg: this.statusMessage ? this.statusMessage : '',
- };
-
const self = this;
- MatrixClientPeg.get().setPresence(updateContent).done(function() {
+ MatrixClientPeg.get().setPresence(this.state).done(function() {
console.log("Presence: %s", newState);
-
- // We have to dispatch because the js-sdk is unreliable at telling us about our own presence
- dis.dispatch({action: "self_presence_updated", statusInfo: updateContent});
}, function(err) {
console.error("Failed to set presence: %s", err);
self.state = old_state;
- self.statusMessage = old_message;
});
}
- stopMaintainingStatus() {
- this.maintain = false;
- }
-
/**
* Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
* @private
@@ -125,9 +96,10 @@ class Presence {
this.setState("unavailable");
}
- _onUserActivity(payload) {
- if (payload.action === "sync_state" || payload.action === "self_presence_updated") return;
- this._resetTimer();
+ _onAction(payload) {
+ if (payload.action === "user_activity") {
+ this._resetTimer();
+ }
}
/**
diff --git a/src/RoomInvite.js b/src/RoomInvite.js
index 1979c6d111..0bcc08eb06 100644
--- a/src/RoomInvite.js
+++ b/src/RoomInvite.js
@@ -57,6 +57,7 @@ export function showStartChatInviteDialog() {
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,
});
@@ -85,9 +86,7 @@ function _onStartChatFinished(shouldInvite, addrs) {
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 ChatCreateOrReuseDialog = sdk.getComponent("views.dialogs.ChatCreateOrReuseDialog");
const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, {
userId: addrTexts[0],
onNewDMClick: () => {
@@ -115,6 +114,15 @@ function _onStartChatFinished(shouldInvite, addrs) {
});
});
}
+ } else if (addrTexts.length === 1) {
+ // Start a new DM chat
+ createRoom({dmUserId: addrTexts[0]}).catch((err) => {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, {
+ title: _t("Failed to invite user"),
+ description: ((err && err.message) ? err.message : _t("Operation failed")),
+ });
+ });
} else {
// Start multi user chat
let room;
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/ScalarAuthClient.js b/src/ScalarAuthClient.js
index 568dd6d185..c7e439bf2e 100644
--- a/src/ScalarAuthClient.js
+++ b/src/ScalarAuthClient.js
@@ -148,10 +148,48 @@ class ScalarAuthClient {
return defer.promise;
}
- getScalarInterfaceUrlForRoom(roomId, screen, id) {
+ /**
+ * 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 3c164c6551..9457e6ccfb 100644
--- a/src/ScalarMessaging.js
+++ b/src/ScalarMessaging.js
@@ -235,6 +235,7 @@ const SdkConfig = require('./SdkConfig');
const MatrixClientPeg = require("./MatrixClientPeg");
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
const dis = require("./dispatcher");
+const Widgets = require('./utils/widgets');
import { _t } from './languageHandler';
function sendResponse(event, res) {
@@ -285,12 +286,58 @@ function inviteUser(event, roomId, userId) {
});
}
+/**
+ * Returns a promise that resolves when a widget with the given
+ * ID has been added as a user widget (ie. the accountData event
+ * arrives) or rejects after a timeout
+ *
+ * @param {string} widgetId The ID of the widget to wait for
+ * @param {boolean} add True to wait for the widget to be added,
+ * false to wait for it to be deleted.
+ * @returns {Promise} that resolves when the widget is available
+ */
+function waitForUserWidget(widgetId, add) {
+ return new Promise((resolve, reject) => {
+ const currentAccountDataEvent = MatrixClientPeg.get().getAccountData('m.widgets');
+
+ // Tests an account data event, returning true if it's in the state
+ // we're waiting for it to be in
+ function eventInIntendedState(ev) {
+ if (!ev || !currentAccountDataEvent.getContent()) return false;
+ if (add) {
+ return ev.getContent()[widgetId] !== undefined;
+ } else {
+ return ev.getContent()[widgetId] === undefined;
+ }
+ }
+
+ if (eventInIntendedState(currentAccountDataEvent)) {
+ resolve();
+ return;
+ }
+
+ function onAccountData(ev) {
+ if (eventInIntendedState(currentAccountDataEvent)) {
+ MatrixClientPeg.get().removeListener('accountData', onAccountData);
+ clearTimeout(timerId);
+ resolve();
+ }
+ }
+ const timerId = setTimeout(() => {
+ MatrixClientPeg.get().removeListener('accountData', onAccountData);
+ reject(new Error("Timed out waiting for widget ID " + widgetId + " to appear"));
+ }, 10000);
+ MatrixClientPeg.get().on('accountData', onAccountData);
+ });
+}
+
function setWidget(event, roomId) {
const widgetId = event.data.widget_id;
const widgetType = event.data.type;
const widgetUrl = event.data.url;
const widgetName = event.data.name; // optional
const widgetData = event.data.data; // optional
+ const userWidget = event.data.userWidget;
const client = MatrixClientPeg.get();
if (!client) {
@@ -330,17 +377,64 @@ function setWidget(event, roomId) {
name: widgetName,
data: widgetData,
};
- if (widgetUrl === null) { // widget is being deleted
- content = {};
- }
- client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => {
- sendResponse(event, {
- success: true,
+ if (userWidget) {
+ const client = MatrixClientPeg.get();
+ const userWidgets = Widgets.getUserWidgets();
+
+ // Delete existing widget with ID
+ try {
+ delete userWidgets[widgetId];
+ } catch (e) {
+ console.error(`$widgetId is non-configurable`);
+ }
+
+ // Add new widget / update
+ if (widgetUrl !== null) {
+ userWidgets[widgetId] = {
+ content: content,
+ sender: client.getUserId(),
+ state_key: widgetId,
+ type: 'm.widget',
+ id: widgetId,
+ };
+ }
+
+ // This starts listening for when the echo comes back from the server
+ // since the widget won't appear added until this happens. If we don't
+ // wait for this, the action will complete but if the user is fast enough,
+ // the widget still won't actually be there.
+ client.setAccountData('m.widgets', userWidgets).then(() => {
+ return waitForUserWidget(widgetId, widgetUrl !== null);
+ }).then(() => {
+ 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);
+ }
+
+ if (widgetUrl === null) { // widget is being deleted
+ content = {};
+ }
+ // TODO - Room widgets need to be moved to 'm.widget' state events
+ // https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
+ client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => {
+ // XXX: We should probably wait for the echo of the state event to come back from the server,
+ // as we do with user widgets.
+ sendResponse(event, {
+ success: true,
+ });
+ }, (err) => {
+ sendError(event, _t('Failed to send request.'), err);
+ });
+ }
}
function getWidgets(event, roomId) {
@@ -349,19 +443,30 @@ function getWidgets(event, roomId) {
sendError(event, _t('You need to be logged in.'));
return;
}
- const room = client.getRoom(roomId);
- if (!room) {
- sendError(event, _t('This room is not recognised.'));
- return;
- }
- const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
- // Only return widgets which have required fields
- const widgetStateEvents = [];
- stateEvents.forEach((ev) => {
- if (ev.getContent().type && ev.getContent().url) {
- widgetStateEvents.push(ev.event); // return the raw event
+ let widgetStateEvents = [];
+
+ if (roomId) {
+ const room = client.getRoom(roomId);
+ if (!room) {
+ sendError(event, _t('This room is not recognised.'));
+ return;
}
- });
+ // TODO - Room widgets need to be moved to 'm.widget' state events
+ // https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
+ const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
+ // Only return widgets which have required fields
+ if (room) {
+ stateEvents.forEach((ev) => {
+ if (ev.getContent().type && ev.getContent().url) {
+ widgetStateEvents.push(ev.event); // return the raw event
+ }
+ });
+ }
+ }
+
+ // Add user widgets (not linked to a specific room)
+ const userWidgets = Widgets.getUserWidgetsArray();
+ widgetStateEvents = widgetStateEvents.concat(userWidgets);
sendResponse(event, widgetStateEvents);
}
@@ -563,7 +668,7 @@ const onMessage = function(event) {
const url = SdkConfig.get().integrations_ui_url;
if (
event.origin.length === 0 ||
- !url.startsWith(event.origin) ||
+ !url.startsWith(event.origin + '/') ||
!event.data.action ||
event.data.api // Ignore messages with specific API set
) {
@@ -578,9 +683,22 @@ 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;
+ // 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, null);
+ return;
+ } else {
+ sendError(event, _t('Missing room_id in request'));
+ return;
+ }
}
let promise = Promise.resolve(currentRoomId);
if (!currentRoomId) {
@@ -601,6 +719,15 @@ const onMessage = function(event) {
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);
@@ -611,12 +738,6 @@ const onMessage = function(event) {
} else if (event.data.action === "get_membership_count") {
getMembershipCount(event, roomId);
return;
- } else if (event.data.action === "set_widget") {
- setWidget(event, roomId);
- return;
- } else if (event.data.action === "get_widgets") {
- getWidgets(event, roomId);
- return;
} else if (event.data.action === "get_room_enc_state") {
getRoomEncState(event, roomId);
return;
diff --git a/src/SdkConfig.js b/src/SdkConfig.js
index 64bf21ecf8..8df725a913 100644
--- a/src/SdkConfig.js
+++ b/src/SdkConfig.js
@@ -21,13 +21,6 @@ const DEFAULTS = {
integrations_rest_url: "https://scalar.vector.im/api",
// Where to send bug reports. If not specified, bugs cannot be sent.
bug_report_endpoint_url: null,
-
- piwik: {
- url: "https://piwik.riot.im/",
- whitelistedHSUrls: ["https://matrix.org"],
- whitelistedISUrls: ["https://vector.im", "https://matrix.org"],
- siteId: 1,
- },
};
class SdkConfig {
@@ -52,4 +45,3 @@ class SdkConfig {
}
module.exports = SdkConfig;
-module.exports.DEFAULTS = DEFAULTS;
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index 1bdf5ad90c..712150af4d 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -52,14 +52,13 @@ function textForMemberEvent(ev) {
case 'join':
if (prevContent && prevContent.membership === 'join') {
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
- return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {
- senderName,
+ 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,
+ senderName: ev.getSender(),
displayName: content.displayname,
});
} else if (prevContent.displayname && !content.displayname) {
diff --git a/src/Tinter.js b/src/Tinter.js
index c7402c15be..d24a4c3e74 100644
--- a/src/Tinter.js
+++ b/src/Tinter.js
@@ -252,7 +252,6 @@ class Tinter {
setTheme(theme) {
- console.trace("setTheme " + theme);
this.theme = theme;
// update keyRgb from the current theme CSS itself, if it defines it
@@ -299,56 +298,66 @@ class Tinter {
for (let i = 0; i < document.styleSheets.length; i++) {
const ss = document.styleSheets[i];
- if (!ss) continue; // well done safari >:(
- // Chromium apparently sometimes returns null here; unsure why.
- // see $14534907369972FRXBx:matrix.org in HQ
- // ...ah, it's because there's a third party extension like
- // privacybadger inserting its own stylesheet in there with a
- // resource:// URI or something which results in a XSS error.
- // See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
- // ...except some browsers apparently return stylesheets without
- // hrefs, which we have no choice but ignore right now
+ 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
+ // 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
+ // Yes, tinting assumes that you are using the Riot skin for now.
+ // The right solution will be to move the CSS over to react-sdk.
+ // And yes, the default assets for the base skin might as well use
+ // Vector Green as any other colour.
+ // --matthew
- if (ss.href && !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue;
- if (ss.disabled) continue;
- if (!ss.cssRules) continue;
+ // 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);
+ 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,
- });
+ 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) {
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/Unread.js b/src/Unread.js
index 383b5c2e5a..55e60f2a9a 100644
--- a/src/Unread.js
+++ b/src/Unread.js
@@ -28,6 +28,8 @@ 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') {
diff --git a/src/VectorConferenceHandler.js b/src/VectorConferenceHandler.js
new file mode 100644
index 0000000000..19081726b2
--- /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].getJoinedMembers().length === 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);
+ });
+};
+
+/**
+ * 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 af4e6dcb60..6a4666305c 100644
--- a/src/Velociraptor.js
+++ b/src/Velociraptor.js
@@ -147,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;
},
diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js
index 0f23413b5f..5b722df65f 100644
--- a/src/WidgetMessaging.js
+++ b/src/WidgetMessaging.js
@@ -15,312 +15,103 @@ limitations under the License.
*/
/*
-Listens for incoming postMessage requests from embedded widgets. The following API is exposed:
-{
- api: "widget",
- action: "content_loaded",
- widgetId: $WIDGET_ID,
- data: {}
- // additional request fields
-}
-
-The complete request object is returned to the caller with an additional "response" key like so:
-{
- api: "widget",
- action: "content_loaded",
- widgetId: $WIDGET_ID,
- data: {},
- // additional request fields
- response: { ... }
-}
-
-The "api" field is required to use this API, and must be set to "widget" in all requests.
-
-The "action" determines the format of the request and response. All actions can return an error response.
-
-Additional data can be sent as additional, abritrary fields. However, typically the data object should be used.
-
-A success response is an object with zero or more keys.
-
-An error response is a "response" object which consists of a sole "error" key to indicate an error.
-They look like:
-{
- error: {
- message: "Unable to invite user into room.",
- _error:
- }
-}
-The "message" key should be a human-friendly string.
-
-ACTIONS
-=======
-** All actions must include an "api" field with valie "widget".**
-All actions can return an error response instead of the response outlined below.
-
-content_loaded
---------------
-Indicates that widget contet has fully loaded
-
-Request:
- - widgetId is the unique ID of the widget instance in riot / matrix state.
- - No additional fields.
-Response:
-{
- success: true
-}
-Example:
-{
- api: "widget",
- action: "content_loaded",
- widgetId: $WIDGET_ID
-}
-
-
-api_version
------------
-Get the current version of the widget postMessage API
-
-Request:
- - No additional fields.
-Response:
-{
- api_version: "0.0.1"
-}
-Example:
-{
- api: "widget",
- action: "api_version",
-}
-
-supported_api_versions
-----------------------
-Get versions of the widget postMessage API that are currently supported
-
-Request:
- - No additional fields.
-Response:
-{
- api: "widget"
- supported_versions: ["0.0.1"]
-}
-Example:
-{
- api: "widget",
- action: "supported_api_versions",
-}
-
+* See - https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing for
+* spec. details / documentation.
*/
-import URL from 'url';
+import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
+import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
-const WIDGET_API_VERSION = '0.0.1'; // Current API version
-const SUPPORTED_WIDGET_API_VERSIONS = [
- '0.0.1',
-];
-
-import dis from './dispatcher';
-
-if (!global.mxWidgetMessagingListenerCount) {
- global.mxWidgetMessagingListenerCount = 0;
+if (!global.mxFromWidgetMessaging) {
+ global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
+ global.mxFromWidgetMessaging.start();
}
-if (!global.mxWidgetMessagingMessageEndpoints) {
- global.mxWidgetMessagingMessageEndpoints = [];
+if (!global.mxToWidgetMessaging) {
+ global.mxToWidgetMessaging = new ToWidgetPostMessageApi();
+ global.mxToWidgetMessaging.start();
}
+const OUTBOUND_API_NAME = 'toWidget';
-/**
- * Register widget message event listeners
- */
-function startListening() {
- if (global.mxWidgetMessagingListenerCount === 0) {
- window.addEventListener("message", onMessage, false);
- }
- global.mxWidgetMessagingListenerCount += 1;
-}
-
-/**
- * De-register widget message event listeners
- */
-function stopListening() {
- global.mxWidgetMessagingListenerCount -= 1;
- if (global.mxWidgetMessagingListenerCount === 0) {
- window.removeEventListener("message", onMessage);
- }
- if (global.mxWidgetMessagingListenerCount < 0) {
- // Make an error so we get a stack trace
- const e = new Error(
- "WidgetMessaging: mismatched startListening / stopListening detected." +
- " Negative count",
- );
- console.error(e);
- }
-}
-
-/**
- * Register a widget endpoint for trusted postMessage communication
- * @param {string} widgetId Unique widget identifier
- * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
- */
-function addEndpoint(widgetId, endpointUrl) {
- const u = URL.parse(endpointUrl);
- if (!u || !u.protocol || !u.host) {
- console.warn("Invalid origin:", endpointUrl);
- return;
- }
-
- const origin = u.protocol + '//' + u.host;
- const endpoint = new WidgetMessageEndpoint(widgetId, origin);
- if (global.mxWidgetMessagingMessageEndpoints) {
- if (global.mxWidgetMessagingMessageEndpoints.some(function(ep) {
- return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
- })) {
- // Message endpoint already registered
- console.warn("Endpoint already registered");
- return;
- }
- global.mxWidgetMessagingMessageEndpoints.push(endpoint);
- }
-}
-
-/**
- * De-register a widget endpoint from trusted communication sources
- * @param {string} widgetId Unique widget identifier
- * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
- * @return {boolean} True if endpoint was successfully removed
- */
-function removeEndpoint(widgetId, endpointUrl) {
- const u = URL.parse(endpointUrl);
- if (!u || !u.protocol || !u.host) {
- console.warn("Invalid origin");
- return;
- }
-
- const origin = u.protocol + '//' + u.host;
- if (global.mxWidgetMessagingMessageEndpoints && global.mxWidgetMessagingMessageEndpoints.length > 0) {
- const length = global.mxWidgetMessagingMessageEndpoints.length;
- global.mxWidgetMessagingMessageEndpoints = global.mxWidgetMessagingMessageEndpoints.filter(function(endpoint) {
- return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin);
- });
- return (length > global.mxWidgetMessagingMessageEndpoints.length);
- }
- return false;
-}
-
-
-/**
- * Handle widget postMessage events
- * @param {Event} event Event to handle
- * @return {undefined}
- */
-function onMessage(event) {
- if (!event.origin) { // Handle chrome
- event.origin = event.originalEvent.origin;
- }
-
- // Event origin is empty string if undefined
- if (
- event.origin.length === 0 ||
- !trustedEndpoint(event.origin) ||
- event.data.api !== "widget" ||
- !event.data.widgetId
- ) {
- return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
- }
-
- const action = event.data.action;
- const widgetId = event.data.widgetId;
- if (action === 'content_loaded') {
- dis.dispatch({
- action: 'widget_content_loaded',
- widgetId: widgetId,
- });
- sendResponse(event, {success: true});
- } else if (action === 'supported_api_versions') {
- sendResponse(event, {
- api: "widget",
- supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
- });
- } else if (action === 'api_version') {
- sendResponse(event, {
- api: "widget",
- version: WIDGET_API_VERSION,
- });
- } else {
- console.warn("Widget postMessage event unhandled");
- sendError(event, {message: "The postMessage was unhandled"});
- }
-}
-
-/**
- * Check if message origin is registered as trusted
- * @param {string} origin PostMessage origin to check
- * @return {boolean} True if trusted
- */
-function trustedEndpoint(origin) {
- if (!origin) {
- return false;
- }
-
- return global.mxWidgetMessagingMessageEndpoints.some((endpoint) => {
- return endpoint.endpointUrl === origin;
- });
-}
-
-/**
- * Send a postmessage response to a postMessage request
- * @param {Event} event The original postMessage request event
- * @param {Object} res Response data
- */
-function sendResponse(event, res) {
- const data = JSON.parse(JSON.stringify(event.data));
- data.response = res;
- event.source.postMessage(data, event.origin);
-}
-
-/**
- * Send an error response to a postMessage request
- * @param {Event} event The original postMessage request event
- * @param {string} msg Error message
- * @param {Error} nestedError Nested error event (optional)
- */
-function sendError(event, msg, nestedError) {
- console.error("Action:" + event.data.action + " failed with message: " + msg);
- const data = JSON.parse(JSON.stringify(event.data));
- data.response = {
- error: {
- message: msg,
- },
- };
- if (nestedError) {
- data.response.error._error = nestedError;
- }
- event.source.postMessage(data, event.origin);
-}
-
-/**
- * Represents mapping of widget instance to URLs for trusted postMessage communication.
- */
-class WidgetMessageEndpoint {
- /**
- * Mapping of widget instance to URL for trusted postMessage communication.
- * @param {string} widgetId Unique widget identifier
- * @param {string} endpointUrl Widget wurl origin.
- */
- constructor(widgetId, endpointUrl) {
- if (!widgetId) {
- throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
- }
- if (!endpointUrl) {
- throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
- }
+export default class WidgetMessaging {
+ constructor(widgetId, widgetUrl, target) {
this.widgetId = widgetId;
- this.endpointUrl = endpointUrl;
+ 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);
}
}
-
-export default {
- startListening: startListening,
- stopListening: stopListening,
- addEndpoint: addEndpoint,
- removeEndpoint: removeEndpoint,
-};
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
index 34c998978d..5f45a8c58c 100644
--- a/src/WidgetUtils.js
+++ b/src/WidgetUtils.js
@@ -17,8 +17,8 @@ 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
+ * (Does not apply to non-room-based / user widgets)
* @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
diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js
index 33bdb53799..6e1d52a88f 100644
--- a/src/actions/MatrixActionCreators.js
+++ b/src/actions/MatrixActionCreators.js
@@ -62,6 +62,127 @@ function createAccountDataAction(matrixClient, accountDataEvent) {
};
}
+/**
+ * @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.RoomMember.membership action that represents
+ * a MatrixClient `RoomMember.membership` matrix event, emitted when a
+ * 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 createRoomMembershipAction(matrixClient, membershipEvent, member, oldMembership) {
+ return { action: 'MatrixActions.RoomMember.membership', member };
+}
+
+/**
+ * @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.
@@ -78,6 +199,11 @@ export default {
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', createRoomMembershipAction);
+ this._addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction);
},
/**
@@ -91,7 +217,7 @@ export default {
*/
_addMatrixClientListener(matrixClient, eventName, actionCreator) {
const listener = (...args) => {
- dis.dispatch(actionCreator(matrixClient, ...args));
+ dis.dispatch(actionCreator(matrixClient, ...args), true);
};
matrixClient.on(eventName, listener);
this._matrixClientListenersStop.push(() => {
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
index 60946ea7f1..a257ff16d8 100644
--- a/src/actions/TagOrderActions.js
+++ b/src/actions/TagOrderActions.js
@@ -22,25 +22,87 @@ const TagOrderActions = {};
/**
* Creates an action thunk that will do an asynchronous request to
- * commit TagOrderStore.getOrderedTags() to account data and dispatch
- * actions to indicate the status of the request.
+ * 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.commitTagOrdering = function(matrixClient) {
- return asyncAction('TagOrderActions.commitTagOrdering', () => {
- // Only commit tags if the state is ready, i.e. not null
- const tags = TagOrderStore.getOrderedTags();
- if (!tags) {
- return;
- }
+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});
+ 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};
});
};
diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js
index bddfbc7c63..967ce609e7 100644
--- a/src/actions/actionCreators.js
+++ b/src/actions/actionCreators.js
@@ -22,16 +22,32 @@ limitations under the License.
* 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) {
+export function asyncAction(id, fn, pendingFn) {
return (dispatch) => {
- dispatch({action: id + '.pending'});
+ dispatch({
+ action: id + '.pending',
+ request:
+ typeof pendingFn === 'function' ? pendingFn() : undefined,
+ });
fn().then((result) => {
dispatch({action: id + '.success', result});
}).catch((err) => {
diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js
index d47f1a161a..e33fa7861f 100644
--- a/src/autocomplete/CommandProvider.js
+++ b/src/autocomplete/CommandProvider.js
@@ -105,6 +105,11 @@ const COMMANDS = [
args: '',
description: _td('Stops ignoring a user, showing their messages going forward'),
},
+ {
+ command: '/devtools',
+ args: '',
+ description: _td('Opens the Developer Tools dialog'),
+ },
// Omitting `/markdown` as it only seems to apply to OldComposer
];
diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js
index fefe77f6cd..ce8f1020a1 100644
--- a/src/autocomplete/UserProvider.js
+++ b/src/autocomplete/UserProvider.js
@@ -44,6 +44,7 @@ export default class UserProvider extends AutocompleteProvider {
this.matcher = new FuzzyMatcher([], {
keys: ['name', 'userId'],
shouldMatchPrefix: true,
+ shouldMatchWordsOnly: false
});
this._onRoomTimelineBound = this._onRoomTimeline.bind(this);
@@ -72,6 +73,7 @@ export default class UserProvider extends AutocompleteProvider {
// 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);
}
@@ -99,8 +101,13 @@ export default class UserProvider extends AutocompleteProvider {
let completions = [];
const {command, range} = this.getCurrentCommand(query, selection, force);
- if (command) {
- completions = this.matcher.match(command[0]).map((user) => {
+
+ 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
@@ -147,6 +154,7 @@ export default class UserProvider extends AutocompleteProvider {
onUserSpoke(user: RoomMember) {
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
@@ -158,7 +166,7 @@ export default class UserProvider extends AutocompleteProvider {
}
renderCompletions(completions: [React.Component]): ?React.Component {
- return
+ 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 94f5713a79..daac294d12 100644
--- a/src/components/structures/ContextualMenu.js
+++ b/src/components/structures/ContextualMenu.js
@@ -26,43 +26,49 @@ import PropTypes from 'prop-types';
// 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: {
+ 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() {
- let container = document.getElementById(this.ContextualMenuContainerId);
-
- if (!container) {
- container = document.createElement("div");
- container.id = this.ContextualMenuContainerId;
- document.body.appendChild(container);
- }
-
- return container;
- },
-
- createMenu: function(Element, props) {
- const self = this;
-
- const closeMenu = function() {
- ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
-
- if (props && props.onFinished) {
- props.onFinished.apply(null, arguments);
- }
- };
+ // If true, insert an invisible screen-sized element behind the
+ // menu that when clicked will close it.
+ hasBackground: PropTypes.bool,
+ }
+ render() {
const position = {};
let chevronFace = null;
+ const props = this.props;
+
if (props.top) {
position.top = props.top;
} else {
@@ -130,21 +136,53 @@ 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!
- const menu = (
-
-
- { chevron }
-
-
-
-
+ return
+
+ { chevron }
+
- );
+ { props.hasBackground && }
+
+
;
+ }
+}
- ReactDOM.render(menu, this.getOrCreateContainer());
+export function createMenu(ElementClass, props) {
+ 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/FilePanel.js b/src/components/structures/FilePanel.js
index e86b76333d..3249cae22c 100644
--- a/src/components/structures/FilePanel.js
+++ b/src/components/structures/FilePanel.js
@@ -68,6 +68,9 @@ const FilePanel = React.createClass({
"room": {
"timeline": {
"contains_url": true,
+ "not_types": [
+ "m.sticker",
+ ],
},
},
},
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index de96935838..ce79ccadfa 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -1,6 +1,6 @@
/*
Copyright 2017 Vector Creations 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.
@@ -27,10 +27,9 @@ import AccessibleButton from '../views/elements/AccessibleButton';
import Modal from '../../Modal';
import classnames from 'classnames';
-import GroupStoreCache from '../../stores/GroupStoreCache';
import GroupStore from '../../stores/GroupStore';
+import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
-import GeminiScrollbar from 'react-gemini-scrollbar';
import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to";
const LONG_DESC_PLACEHOLDER = _td(
@@ -93,8 +92,8 @@ const CategoryRoomList = React.createClass({
if (!success) return;
const errorList = [];
Promise.all(addrs.map((addr) => {
- return this.context.groupStore
- .addRoomToGroupSummary(addr.address)
+ return GroupStore
+ .addRoomToGroupSummary(this.props.groupId, addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
})).then(() => {
@@ -174,7 +173,8 @@ const FeaturedRoom = React.createClass({
onDeleteClicked: function(e) {
e.preventDefault();
e.stopPropagation();
- this.context.groupStore.removeRoomFromGroupSummary(
+ GroupStore.removeRoomFromGroupSummary(
+ this.props.groupId,
this.props.summaryInfo.room_id,
).catch((err) => {
console.error('Error whilst removing room from group summary', err);
@@ -269,7 +269,7 @@ const RoleUserList = React.createClass({
if (!success) return;
const errorList = [];
Promise.all(addrs.map((addr) => {
- return this.context.groupStore
+ return GroupStore
.addUserToGroupSummary(addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
@@ -344,7 +344,8 @@ const FeaturedUser = React.createClass({
onDeleteClicked: function(e) {
e.preventDefault();
e.stopPropagation();
- this.context.groupStore.removeUserFromGroupSummary(
+ GroupStore.removeUserFromGroupSummary(
+ this.props.groupId,
this.props.summaryInfo.user_id,
).catch((err) => {
console.error('Error whilst removing user from group summary', err);
@@ -390,14 +391,8 @@ const FeaturedUser = React.createClass({
},
});
-const GroupContext = {
- groupStore: PropTypes.instanceOf(GroupStore).isRequired,
-};
-
-CategoryRoomList.contextTypes = GroupContext;
-FeaturedRoom.contextTypes = GroupContext;
-RoleUserList.contextTypes = GroupContext;
-FeaturedUser.contextTypes = GroupContext;
+const GROUP_JOINPOLICY_OPEN = "open";
+const GROUP_JOINPOLICY_INVITE = "invite";
export default React.createClass({
displayName: 'GroupView',
@@ -412,12 +407,6 @@ export default React.createClass({
groupStore: PropTypes.instanceOf(GroupStore),
},
- getChildContext: function() {
- return {
- groupStore: this._groupStore,
- };
- },
-
getInitialState: function() {
return {
summary: null,
@@ -429,6 +418,7 @@ export default React.createClass({
editing: false,
saving: false,
uploadingAvatar: false,
+ avatarChanged: false,
membershipBusy: false,
publicityBusy: false,
inviterProfile: null,
@@ -436,6 +426,7 @@ export default React.createClass({
},
componentWillMount: function() {
+ this._unmounted = false;
this._matrixClient = MatrixClientPeg.get();
this._matrixClient.on("Group.myMembership", this._onGroupMyMembership);
@@ -444,8 +435,8 @@ export default React.createClass({
},
componentWillUnmount: function() {
+ this._unmounted = true;
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
- this._groupStore.removeAllListeners();
},
componentWillReceiveProps: function(newProps) {
@@ -460,8 +451,11 @@ export default React.createClass({
},
_onGroupMyMembership: function(group) {
- if (group.groupId !== this.props.groupId) return;
-
+ if (this._unmounted || group.groupId !== this.props.groupId) return;
+ if (group.myMembership === 'leave') {
+ // Leave settings - the user might have clicked the "Leave" button
+ this._closeSettings();
+ }
this.setState({membershipBusy: false});
},
@@ -470,34 +464,11 @@ export default React.createClass({
if (group && group.inviter && group.inviter.userId) {
this._fetchInviterProfile(group.inviter.userId);
}
- this._groupStore = GroupStoreCache.getGroupStore(groupId);
- this._groupStore.registerListener(() => {
- const summary = this._groupStore.getSummary();
- if (summary.profile) {
- // Default profile fields should be "" for later sending to the server (which
- // requires that the fields are strings, not null)
- ["avatar_url", "long_description", "name", "short_description"].forEach((k) => {
- summary.profile[k] = summary.profile[k] || "";
- });
- }
- this.setState({
- summary,
- summaryLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
- isGroupPublicised: this._groupStore.getGroupPublicity(),
- isUserPrivileged: this._groupStore.isUserPrivileged(),
- groupRooms: this._groupStore.getGroupRooms(),
- groupRoomsLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.GroupRooms),
- isUserMember: this._groupStore.getGroupMembers().some(
- (m) => m.userId === this._matrixClient.credentials.userId,
- ),
- error: null,
- });
- if (this.props.groupIsNew && firstInit) {
- this._onEditClick();
- }
- });
+ GroupStore.registerListener(groupId, this.onGroupStoreUpdated.bind(this, firstInit));
let willDoOnboarding = false;
- this._groupStore.on('error', (err) => {
+ // XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
+ GroupStore.on('error', (err, errorGroupId) => {
+ if (this._unmounted || groupId !== errorGroupId) return;
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) {
dis.dispatch({
action: 'do_after_sync_prepared',
@@ -512,15 +483,45 @@ export default React.createClass({
this.setState({
summary: null,
error: err,
+ editing: false,
});
});
},
+ onGroupStoreUpdated(firstInit) {
+ if (this._unmounted) return;
+ const summary = GroupStore.getSummary(this.props.groupId);
+ if (summary.profile) {
+ // Default profile fields should be "" for later sending to the server (which
+ // requires that the fields are strings, not null)
+ ["avatar_url", "long_description", "name", "short_description"].forEach((k) => {
+ summary.profile[k] = summary.profile[k] || "";
+ });
+ }
+ this.setState({
+ summary,
+ summaryLoading: !GroupStore.isStateReady(this.props.groupId, GroupStore.STATE_KEY.Summary),
+ isGroupPublicised: GroupStore.getGroupPublicity(this.props.groupId),
+ isUserPrivileged: GroupStore.isUserPrivileged(this.props.groupId),
+ groupRooms: GroupStore.getGroupRooms(this.props.groupId),
+ groupRoomsLoading: !GroupStore.isStateReady(this.props.groupId, GroupStore.STATE_KEY.GroupRooms),
+ isUserMember: GroupStore.getGroupMembers(this.props.groupId).some(
+ (m) => m.userId === this._matrixClient.credentials.userId,
+ ),
+ error: null,
+ });
+ // XXX: This might not work but this.props.groupIsNew unused anyway
+ if (this.props.groupIsNew && firstInit) {
+ this._onEditClick();
+ }
+ },
+
_fetchInviterProfile(userId) {
this.setState({
inviterProfileBusy: true,
});
this._matrixClient.getProfileInfo(userId).then((resp) => {
+ if (this._unmounted) return;
this.setState({
inviterProfile: {
avatarUrl: resp.avatar_url,
@@ -530,6 +531,7 @@ export default React.createClass({
}).catch((e) => {
console.error('Error getting group inviter profile', e);
}).finally(() => {
+ if (this._unmounted) return;
this.setState({
inviterProfileBusy: false,
});
@@ -544,6 +546,12 @@ export default React.createClass({
this.setState({
editing: true,
profileForm: Object.assign({}, this.state.summary.profile),
+ joinableForm: {
+ policyType:
+ this.state.summary.profile.is_openly_joinable ?
+ GROUP_JOINPOLICY_OPEN :
+ GROUP_JOINPOLICY_INVITE,
+ },
});
dis.dispatch({
action: 'panel_disable',
@@ -552,6 +560,10 @@ export default React.createClass({
},
_onCancelClick: function() {
+ this._closeSettings();
+ },
+
+ _closeSettings() {
this.setState({
editing: false,
profileForm: null,
@@ -590,6 +602,10 @@ export default React.createClass({
this.setState({
uploadingAvatar: false,
profileForm: newProfileForm,
+
+ // Indicate that FlairStore needs to be poked to show this change
+ // in TagTile (TagPanel), Flair and GroupTile (MyGroups).
+ avatarChanged: true,
});
}).catch((e) => {
this.setState({uploadingAvatar: false});
@@ -602,11 +618,15 @@ export default React.createClass({
}).done();
},
+ _onJoinableChange: function(ev) {
+ this.setState({
+ joinableForm: { policyType: ev.target.value },
+ });
+ },
+
_onSaveClick: function() {
this.setState({saving: true});
- const savePromise = this.state.isUserPrivileged ?
- this._matrixClient.setGroupProfile(this.props.groupId, this.state.profileForm) :
- Promise.resolve();
+ const savePromise = this.state.isUserPrivileged ? this._saveGroup() : Promise.resolve();
savePromise.then((result) => {
this.setState({
saving: false,
@@ -615,6 +635,11 @@ export default React.createClass({
});
dis.dispatch({action: 'panel_disable'});
this._initGroupStore(this.props.groupId);
+
+ if (this.state.avatarChanged) {
+ // XXX: Evil - poking a store should be done from an async action
+ FlairStore.refreshGroupProfile(this._matrixClient, this.props.groupId);
+ }
}).catch((e) => {
this.setState({
saving: false,
@@ -625,12 +650,28 @@ export default React.createClass({
title: _t('Error'),
description: _t('Failed to update community'),
});
+ }).finally(() => {
+ this.setState({
+ avatarChanged: false,
+ });
}).done();
},
- _onAcceptInviteClick: function() {
+ _saveGroup: async function() {
+ await this._matrixClient.setGroupProfile(this.props.groupId, this.state.profileForm);
+ await this._matrixClient.setGroupJoinPolicy(this.props.groupId, {
+ type: this.state.joinableForm.policyType,
+ });
+ },
+
+ _onAcceptInviteClick: async function() {
this.setState({membershipBusy: true});
- this._groupStore.acceptGroupInvite().then(() => {
+
+ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
+ // spinner disappearing after we have fetched new group data.
+ await Promise.delay(500);
+
+ GroupStore.acceptGroupInvite(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@@ -642,9 +683,14 @@ export default React.createClass({
});
},
- _onRejectInviteClick: function() {
+ _onRejectInviteClick: async function() {
this.setState({membershipBusy: true});
- this._matrixClient.leaveGroup(this.props.groupId).then(() => {
+
+ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
+ // spinner disappearing after we have fetched new group data.
+ await Promise.delay(500);
+
+ GroupStore.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@@ -656,6 +702,25 @@ export default React.createClass({
});
},
+ _onJoinClick: async function() {
+ this.setState({membershipBusy: true});
+
+ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
+ // spinner disappearing after we have fetched new group data.
+ await Promise.delay(500);
+
+ GroupStore.joinGroup(this.props.groupId).then(() => {
+ // don't reset membershipBusy here: wait for the membership change to come down the sync
+ }).catch((e) => {
+ this.setState({membershipBusy: false});
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ Modal.createTrackedDialog('Error joining room', '', ErrorDialog, {
+ title: _t("Error"),
+ description: _t("Unable to join community"),
+ });
+ });
+ },
+
_onLeaveClick: function() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
@@ -663,18 +728,23 @@ export default React.createClass({
description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}),
button: _t("Leave"),
danger: true,
- onFinished: (confirmed) => {
+ onFinished: async (confirmed) => {
if (!confirmed) return;
this.setState({membershipBusy: true});
- this._matrixClient.leaveGroup(this.props.groupId).then(() => {
+
+ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
+ // spinner disappearing after we have fetched new group data.
+ await Promise.delay(500);
+
+ GroupStore.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
- Modal.createTrackedDialog('Error leaving room', '', ErrorDialog, {
+ Modal.createTrackedDialog('Error leaving community', '', ErrorDialog, {
title: _t("Error"),
- description: _t("Unable to leave room"),
+ description: _t("Unable to leave community"),
});
});
},
@@ -692,8 +762,22 @@ export default React.createClass({
});
const header = this.state.editing ?
+ { _t(
+ 'Changes made to your community name and avatar ' +
+ 'might not be seen by other users for up to 30 minutes.',
+ {},
+ {
+ 'bold1': (sub) => { sub } ,
+ 'bold2': (sub) => { sub } ,
+ },
+ ) }
+
);
} else if (this.state.error) {
diff --git a/src/components/structures/HomePage.js b/src/components/structures/HomePage.js
new file mode 100644
index 0000000000..457796f5dc
--- /dev/null
+++ b/src/components/structures/HomePage.js
@@ -0,0 +1,109 @@
+/*
+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.
+*/
+
+'use strict';
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import request from 'browser-request';
+import { _t } from '../../languageHandler';
+import sanitizeHtml from 'sanitize-html';
+import sdk from '../../index';
+
+class HomePage extends React.Component {
+ static displayName = 'HomePage';
+
+ static propTypes = {
+ // URL base of the team server. Optional.
+ teamServerUrl: PropTypes.string,
+ // Team token. Optional. If set, used to get the static homepage of the team
+ // associated. If unset, homePageUrl will be used.
+ teamToken: PropTypes.string,
+ // URL to use as the iFrame src. Defaults to /home.html.
+ homePageUrl: PropTypes.string,
+ };
+
+ state = {
+ iframeSrc: '',
+ page: '',
+ };
+
+ translate(s) {
+ // default implementation - skins may wish to extend this
+ return sanitizeHtml(_t(s));
+ }
+
+ componentWillMount() {
+ this._unmounted = false;
+
+ if (this.props.teamToken && this.props.teamServerUrl) {
+ this.setState({
+ iframeSrc: `${this.props.teamServerUrl}/static/${this.props.teamToken}/home.html`
+ });
+ }
+ else {
+ // we use request() to inline the homepage into the react component
+ // so that it can inherit CSS and theming easily rather than mess around
+ // with iframes and trying to synchronise document.stylesheets.
+
+ let src = this.props.homePageUrl || 'home.html';
+
+ request(
+ { method: "GET", url: src },
+ (err, response, body) => {
+ if (this._unmounted) {
+ return;
+ }
+
+ if (err || response.status < 200 || response.status >= 300) {
+ console.warn(`Error loading home page: ${err}`);
+ this.setState({ page: _t("Couldn't load home page") });
+ return;
+ }
+
+ body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
+ this.setState({ page: body });
+ }
+ );
+ }
+ }
+
+ componentWillUnmount() {
+ this._unmounted = true;
+ }
+
+ render() {
+ if (this.state.iframeSrc) {
+ return (
+
+ { _t(
+ "Did you know: you can use communities to filter your Riot.im experience!",
+ ) }
+
+
+ { _t(
+ "To set up a filter, drag a community avatar over to the filter panel on " +
+ "the far left hand side of the screen. You can click on an avatar in the " +
+ "filter panel at any time to see only the rooms and people associated " +
+ "with that community.",
+ ) }
+
+
+
+ { groupNodes }
+
+ :
{ _t(
"You're not currently a member of any communities.",
diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js
new file mode 100644
index 0000000000..18523ceb59
--- /dev/null
+++ b/src/components/structures/RightPanel.js
@@ -0,0 +1,421 @@
+/*
+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.
+*/
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { _t } from '../../languageHandler';
+import sdk from '../../index';
+import dis from '../../dispatcher';
+import { MatrixClient } from 'matrix-js-sdk';
+import Analytics from '../../Analytics';
+import RateLimitedFunc from '../../ratelimitedfunc';
+import AccessibleButton from '../../components/views/elements/AccessibleButton';
+import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
+import GroupStore from '../../stores/GroupStore';
+
+import { formatCount } from '../../utils/FormattingUtils';
+
+class HeaderButton extends React.Component {
+ constructor() {
+ super();
+ this.onClick = this.onClick.bind(this);
+ }
+
+ onClick(ev) {
+ Analytics.trackEvent(...this.props.analytics);
+ dis.dispatch({
+ action: 'view_right_panel_phase',
+ phase: this.props.clickPhase,
+ });
+ }
+
+ render() {
+ const TintableSvg = sdk.getComponent("elements.TintableSvg");
+ const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
+
+ return
+
+
+ { this.props.badge ? this.props.badge : }
+
+
+ { this.props.isHighlighted ? : }
+
+ ;
+ }
+}
+
+HeaderButton.propTypes = {
+ // Whether this button is highlighted
+ isHighlighted: PropTypes.bool.isRequired,
+ // The phase to swap to when the button is clicked
+ clickPhase: PropTypes.string.isRequired,
+ // The source file of the icon to display
+ iconSrc: PropTypes.string.isRequired,
+
+ // The badge to display above the icon
+ badge: PropTypes.node,
+ // The parameters to track the click event
+ analytics: PropTypes.arrayOf(PropTypes.string).isRequired,
+
+ // Button title
+ title: PropTypes.string.isRequired,
+};
+
+module.exports = React.createClass({
+ displayName: 'RightPanel',
+
+ propTypes: {
+ // TODO: We're trying to move away from these being props, but we need to know
+ // whether we should be displaying a room or group member list
+ roomId: React.PropTypes.string, // if showing panels for a given room, this is set
+ groupId: React.PropTypes.string, // if showing panels for a given group, this is set
+ collapsed: React.PropTypes.bool, // currently unused property to request for a minimized view of the panel
+ },
+
+ contextTypes: {
+ matrixClient: PropTypes.instanceOf(MatrixClient),
+ },
+
+ Phase: {
+ RoomMemberList: 'RoomMemberList',
+ GroupMemberList: 'GroupMemberList',
+ GroupRoomList: 'GroupRoomList',
+ GroupRoomInfo: 'GroupRoomInfo',
+ FilePanel: 'FilePanel',
+ NotificationPanel: 'NotificationPanel',
+ RoomMemberInfo: 'RoomMemberInfo',
+ GroupMemberInfo: 'GroupMemberInfo',
+ },
+
+ componentWillMount: function() {
+ this.dispatcherRef = dis.register(this.onAction);
+ const cli = this.context.matrixClient;
+ cli.on("RoomState.members", this.onRoomStateMember);
+ this._initGroupStore(this.props.groupId);
+ },
+
+ componentWillUnmount: function() {
+ dis.unregister(this.dispatcherRef);
+ if (this.context.matrixClient) {
+ this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
+ }
+ this._unregisterGroupStore(this.props.groupId);
+ },
+
+ getInitialState: function() {
+ return {
+ phase: this.props.groupId ? this.Phase.GroupMemberList : this.Phase.RoomMemberList,
+ isUserPrivilegedInGroup: null,
+ };
+ },
+
+ componentWillReceiveProps(newProps) {
+ if (newProps.groupId !== this.props.groupId) {
+ this._unregisterGroupStore(this.props.groupId);
+ this._initGroupStore(newProps.groupId);
+ }
+ },
+
+ _initGroupStore(groupId) {
+ if (!groupId) return;
+ GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
+ },
+
+ _unregisterGroupStore() {
+ GroupStore.unregisterListener(this.onGroupStoreUpdated);
+ },
+
+ onGroupStoreUpdated: function() {
+ this.setState({
+ isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
+ });
+ },
+
+ onCollapseClick: function() {
+ dis.dispatch({
+ action: 'hide_right_panel',
+ });
+ },
+
+ onInviteButtonClick: function() {
+ if (this.context.matrixClient.isGuest()) {
+ dis.dispatch({action: 'view_set_mxid'});
+ return;
+ }
+
+ // call AddressPickerDialog
+ dis.dispatch({
+ action: 'view_invite',
+ roomId: this.props.roomId,
+ });
+ },
+
+ onInviteToGroupButtonClick: function() {
+ showGroupInviteDialog(this.props.groupId).then(() => {
+ this.setState({
+ phase: this.Phase.GroupMemberList,
+ });
+ });
+ },
+
+ onAddRoomToGroupButtonClick: function() {
+ showGroupAddRoomDialog(this.props.groupId).then(() => {
+ this.forceUpdate();
+ });
+ },
+
+ onRoomStateMember: function(ev, state, member) {
+ // redraw the badge on the membership list
+ if (this.state.phase === this.Phase.RoomMemberList && member.roomId === this.props.roomId) {
+ this._delayedUpdate();
+ } else if (this.state.phase === this.Phase.RoomMemberInfo && member.roomId === this.props.roomId &&
+ member.userId === this.state.member.userId) {
+ // refresh the member info (e.g. new power level)
+ this._delayedUpdate();
+ }
+ },
+
+ _delayedUpdate: new RateLimitedFunc(function() {
+ this.forceUpdate(); // eslint-disable-line babel/no-invalid-this
+ }, 500),
+
+ onAction: function(payload) {
+ if (payload.action === "view_user") {
+ dis.dispatch({
+ action: 'show_right_panel',
+ });
+ if (payload.member) {
+ this.setState({
+ phase: this.Phase.RoomMemberInfo,
+ member: payload.member,
+ });
+ } else {
+ if (this.props.roomId) {
+ this.setState({
+ phase: this.Phase.RoomMemberList,
+ });
+ } else if (this.props.groupId) {
+ this.setState({
+ phase: this.Phase.GroupMemberList,
+ member: payload.member,
+ });
+ }
+ }
+ } else if (payload.action === "view_group") {
+ this.setState({
+ phase: this.Phase.GroupMemberList,
+ member: null,
+ });
+ } else if (payload.action === "view_group_room") {
+ this.setState({
+ phase: this.Phase.GroupRoomInfo,
+ groupRoomId: payload.groupRoomId,
+ });
+ } else if (payload.action === "view_group_room_list") {
+ this.setState({
+ phase: this.Phase.GroupRoomList,
+ });
+ } else if (payload.action === "view_group_member_list") {
+ this.setState({
+ phase: this.Phase.GroupMemberList,
+ });
+ } else if (payload.action === "view_group_user") {
+ this.setState({
+ phase: this.Phase.GroupMemberInfo,
+ member: payload.member,
+ });
+ } else if (payload.action === "view_room") {
+ this.setState({
+ phase: this.Phase.RoomMemberList,
+ });
+ } else if (payload.action === "view_right_panel_phase") {
+ this.setState({
+ phase: payload.phase,
+ });
+ }
+ },
+
+ render: function() {
+ const MemberList = sdk.getComponent('rooms.MemberList');
+ const MemberInfo = sdk.getComponent('rooms.MemberInfo');
+ const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
+ const FilePanel = sdk.getComponent('structures.FilePanel');
+
+ const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
+ const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo');
+ const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
+ const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
+
+ const TintableSvg = sdk.getComponent("elements.TintableSvg");
+
+ let inviteGroup;
+
+ let membersBadge;
+ let membersTitle = _t('Members');
+ if ((this.state.phase === this.Phase.RoomMemberList || this.state.phase === this.Phase.RoomMemberInfo)
+ && this.props.roomId
+ ) {
+ const cli = this.context.matrixClient;
+ const room = cli.getRoom(this.props.roomId);
+ let isUserInRoom;
+ if (room) {
+ const numMembers = room.getJoinedMembers().length;
+ membersTitle = _t('%(count)s Members', { count: numMembers });
+ membersBadge =
+ ;
+ }
+ }
+
+ const isPhaseGroup = [
+ this.Phase.GroupMemberInfo,
+ this.Phase.GroupMemberList,
+ ].includes(this.state.phase);
+
+ let headerButtons = [];
+ if (this.props.roomId) {
+ headerButtons = [
+ ,
+ ,
+ ,
+ ];
+ } else if (this.props.groupId) {
+ headerButtons = [
+ ,
+ ,
+ ];
+ }
+
+ if (this.props.roomId || this.props.groupId) {
+ // Hiding the right panel hides it completely and relies on an 'expand' button
+ // being put in the RoomHeader or GroupView header, so only show the minimise
+ // button on these 2 screens or you won't be able to re-expand the panel.
+ headerButtons.push(
+
+
+ );
+ }
+
+ const classes = classNames("mx_RightPanel", "mx_fadable", {
+ "collapsed": this.props.collapsed,
+ "mx_fadable_faded": this.props.disabled,
+ });
+
+ return (
+
+ );
+ },
+});
diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js
new file mode 100644
index 0000000000..76360383d6
--- /dev/null
+++ b/src/components/structures/RoomDirectory.js
@@ -0,0 +1,587 @@
+/*
+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');
+
+var MatrixClientPeg = require('../../MatrixClientPeg');
+var ContentRepo = require("matrix-js-sdk").ContentRepo;
+var Modal = require('../../Modal');
+var sdk = require('../../index');
+var dis = require('../../dispatcher');
+
+var linkify = require('linkifyjs');
+var linkifyString = require('linkifyjs/string');
+var linkifyMatrix = require('../../linkify-matrix');
+var sanitizeHtml = require('sanitize-html');
+import Promise from 'bluebird';
+
+import { _t } from '../../languageHandler';
+
+import {instanceForInstanceId, protocolNameForInstanceId} from '../../utils/DirectoryUtils';
+
+linkifyMatrix(linkify);
+
+module.exports = React.createClass({
+ displayName: 'RoomDirectory',
+
+ propTypes: {
+ config: React.PropTypes.object,
+ },
+
+ getDefaultProps: function() {
+ return {
+ config: {},
+ }
+ },
+
+ getInitialState: function() {
+ return {
+ publicRooms: [],
+ loading: true,
+ protocolsLoading: true,
+ instanceId: null,
+ includeAll: false,
+ roomServer: null,
+ filterString: null,
+ }
+ },
+
+ componentWillMount: function() {
+ this._unmounted = false;
+ this.nextBatch = null;
+ this.filterTimeout = null;
+ this.scrollPanel = null;
+ this.protocols = null;
+
+ this.setState({protocolsLoading: true});
+ MatrixClientPeg.get().getThirdpartyProtocols().done((response) => {
+ this.protocols = response;
+ this.setState({protocolsLoading: false});
+ }, (err) => {
+ console.warn(`error loading thirdparty protocols: ${err}`);
+ this.setState({protocolsLoading: false});
+ if (MatrixClientPeg.get().isGuest()) {
+ // Guests currently aren't allowed to use this API, so
+ // ignore this as otherwise this error is literally the
+ // thing you see when loading the client!
+ return;
+ }
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ Modal.createTrackedDialog('Failed to get protocol list from Home Server', '', ErrorDialog, {
+ title: _t('Failed to get protocol list from Home Server'),
+ description: _t('The Home Server may be too old to support third party networks'),
+ });
+ });
+
+ // dis.dispatch({
+ // action: 'panel_disable',
+ // sideDisabled: true,
+ // middleDisabled: true,
+ // });
+ },
+
+ componentWillUnmount: function() {
+ // dis.dispatch({
+ // action: 'panel_disable',
+ // sideDisabled: false,
+ // middleDisabled: false,
+ // });
+ if (this.filterTimeout) {
+ clearTimeout(this.filterTimeout);
+ }
+ this._unmounted = true;
+ },
+
+ refreshRoomList: function() {
+ this.nextBatch = null;
+ this.setState({
+ publicRooms: [],
+ loading: true,
+ });
+ this.getMoreRooms().done();
+ },
+
+ getMoreRooms: function() {
+ if (!MatrixClientPeg.get()) return Promise.resolve();
+
+ const my_filter_string = this.state.filterString;
+ const my_server = this.state.roomServer;
+ // remember the next batch token when we sent the request
+ // too. If it's changed, appending to the list will corrupt it.
+ const my_next_batch = this.nextBatch;
+ const opts = {limit: 20};
+ if (my_server != MatrixClientPeg.getHomeServerName()) {
+ opts.server = my_server;
+ }
+ if (this.state.instanceId) {
+ opts.third_party_instance_id = this.state.instanceId;
+ } else if (this.state.includeAll) {
+ opts.include_all_networks = true;
+ }
+ if (this.nextBatch) opts.since = this.nextBatch;
+ if (my_filter_string) opts.filter = { generic_search_term: my_filter_string } ;
+ return MatrixClientPeg.get().publicRooms(opts).then((data) => {
+ if (
+ my_filter_string != this.state.filterString ||
+ my_server != this.state.roomServer ||
+ my_next_batch != this.nextBatch)
+ {
+ // if the filter or server has changed since this request was sent,
+ // throw away the result (don't even clear the busy flag
+ // since we must still have a request in flight)
+ return;
+ }
+
+ if (this._unmounted) {
+ // if we've been unmounted, we don't care either.
+ return;
+ }
+
+ this.nextBatch = data.next_batch;
+ this.setState((s) => {
+ s.publicRooms.push(...data.chunk);
+ s.loading = false;
+ return s;
+ });
+ return Boolean(data.next_batch);
+ }, (err) => {
+ if (
+ my_filter_string != this.state.filterString ||
+ my_server != this.state.roomServer ||
+ my_next_batch != this.nextBatch)
+ {
+ // as above: we don't care about errors for old
+ // requests either
+ return;
+ }
+
+ if (this._unmounted) {
+ // if we've been unmounted, we don't care either.
+ return;
+ }
+
+ this.setState({ loading: false });
+ console.error("Failed to get publicRooms: %s", JSON.stringify(err));
+ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ Modal.createTrackedDialog('Failed to get public room list', '', ErrorDialog, {
+ title: _t('Failed to get public room list'),
+ description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded'))
+ });
+ });
+ },
+
+ /**
+ * A limited interface for removing rooms from the directory.
+ * Will set the room to not be publicly visible and delete the
+ * default alias. In the long term, it would be better to allow
+ * HS admins to do this through the RoomSettings interface, but
+ * this needs SPEC-417.
+ */
+ removeFromDirectory: function(room) {
+ var alias = get_display_alias_for_room(room);
+ var name = room.name || alias || _t('Unnamed room');
+
+ var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+
+ var desc;
+ if (alias) {
+ desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
+ } else {
+ desc = _t('Remove %(name)s from the directory?', {name: name});
+ }
+
+ Modal.createTrackedDialog('Remove from Directory', '', QuestionDialog, {
+ title: _t('Remove from Directory'),
+ description: desc,
+ onFinished: (should_delete) => {
+ if (!should_delete) return;
+
+ var Loader = sdk.getComponent("elements.Spinner");
+ var modal = Modal.createDialog(Loader);
+ var step = _t('remove %(name)s from the directory.', {name: name});
+
+ MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
+ if (!alias) return;
+ step = _t('delete the alias.');
+ return MatrixClientPeg.get().deleteAlias(alias);
+ }).done(() => {
+ modal.close();
+ this.refreshRoomList();
+ }, (err) => {
+ modal.close();
+ this.refreshRoomList();
+ console.error("Failed to " + step + ": " + err);
+ Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, {
+ title: _t('Error'),
+ description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded'))
+ });
+ });
+ }
+ });
+ },
+
+ onRoomClicked: function(room, ev) {
+ if (ev.shiftKey) {
+ ev.preventDefault();
+ this.removeFromDirectory(room);
+ } else {
+ this.showRoom(room);
+ }
+ },
+
+ onOptionChange: function(server, instanceId, includeAll) {
+ // clear next batch so we don't try to load more rooms
+ this.nextBatch = null;
+ this.setState({
+ // Clear the public rooms out here otherwise we needlessly
+ // spend time filtering lots of rooms when we're about to
+ // to clear the list anyway.
+ publicRooms: [],
+ roomServer: server,
+ instanceId: instanceId,
+ includeAll: includeAll,
+ }, this.refreshRoomList);
+ // We also refresh the room list each time even though this
+ // filtering is client-side. It hopefully won't be client side
+ // for very long, and we may have fetched a thousand rooms to
+ // find the five gitter ones, at which point we do not want
+ // to render all those rooms when switching back to 'all networks'.
+ // Easiest to just blow away the state & re-fetch.
+ },
+
+ onFillRequest: function(backwards) {
+ if (backwards || !this.nextBatch) return Promise.resolve(false);
+
+ return this.getMoreRooms();
+ },
+
+ onFilterChange: function(alias) {
+ this.setState({
+ filterString: alias || null,
+ });
+
+ // don't send the request for a little bit,
+ // no point hammering the server with a
+ // request for every keystroke, let the
+ // user finish typing.
+ if (this.filterTimeout) {
+ clearTimeout(this.filterTimeout);
+ }
+ this.filterTimeout = setTimeout(() => {
+ this.filterTimeout = null;
+ this.refreshRoomList();
+ }, 700);
+ },
+
+ onFilterClear: function() {
+ // update immediately
+ this.setState({
+ filterString: null,
+ }, this.refreshRoomList);
+
+ if (this.filterTimeout) {
+ clearTimeout(this.filterTimeout);
+ }
+ },
+
+ onJoinClick: function(alias) {
+ // If we don't have a particular instance id selected, just show that rooms alias
+ if (!this.state.instanceId) {
+ // If the user specified an alias without a domain, add on whichever server is selected
+ // in the dropdown
+ if (alias.indexOf(':') == -1) {
+ alias = alias + ':' + this.state.roomServer;
+ }
+ this.showRoomAlias(alias);
+ } else {
+ // This is a 3rd party protocol. Let's see if we can join it
+ const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
+ const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
+ const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null;
+ if (!fields) {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, {
+ title: _t('Unable to join network'),
+ description: _t('Riot does not know how to join a room on this network'),
+ });
+ return;
+ }
+ MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => {
+ if (resp.length > 0 && resp[0].alias) {
+ this.showRoomAlias(resp[0].alias);
+ } else {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
+ title: _t('Room not found'),
+ description: _t('Couldn\'t find a matching Matrix room'),
+ });
+ }
+ }, (e) => {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ Modal.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, {
+ title: _t('Fetching third party location failed'),
+ description: _t('Unable to look up room ID from server'),
+ });
+ });
+ }
+ },
+
+ showRoomAlias: function(alias) {
+ this.showRoom(null, alias);
+ },
+
+ showRoom: function(room, room_alias) {
+ var payload = {action: 'view_room'};
+ if (room) {
+ // Don't let the user view a room they won't be able to either
+ // peek or join: fail earlier so they don't have to click back
+ // to the directory.
+ if (MatrixClientPeg.get().isGuest()) {
+ if (!room.world_readable && !room.guest_can_join) {
+ dis.dispatch({action: 'view_set_mxid'});
+ return;
+ }
+ }
+
+ if (!room_alias) {
+ room_alias = get_display_alias_for_room(room);
+ }
+
+ payload.oob_data = {
+ avatarUrl: room.avatar_url,
+ // XXX: This logic is duplicated from the JS SDK which
+ // would normally decide what the name is.
+ name: room.name || room_alias || _t('Unnamed room'),
+ };
+ }
+ // It's not really possible to join Matrix rooms by ID because the HS has no way to know
+ // which servers to start querying. However, there's no other way to join rooms in
+ // this list without aliases at present, so if roomAlias isn't set here we have no
+ // choice but to supply the ID.
+ if (room_alias) {
+ payload.room_alias = room_alias;
+ } else {
+ payload.room_id = room.room_id;
+ }
+ dis.dispatch(payload);
+ },
+
+ getRows: function() {
+ var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
+
+ if (!this.state.publicRooms) return [];
+
+ var rooms = this.state.publicRooms;
+ var rows = [];
+ var self = this;
+ var guestRead, guestJoin, perms;
+ for (var i = 0; i < rooms.length; i++) {
+ var name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room');
+ guestRead = null;
+ guestJoin = null;
+
+ if (rooms[i].world_readable) {
+ guestRead = (
+
+ );
+ }
+ return rows;
+ },
+
+ collectScrollPanel: function(element) {
+ this.scrollPanel = element;
+ },
+
+ _stringLooksLikeId: function(s, field_type) {
+ let pat = /^#[^\s]+:[^\s]/;
+ if (field_type && field_type.regexp) {
+ pat = new RegExp(field_type.regexp);
+ }
+
+ return pat.test(s);
+ },
+
+ _getFieldsForThirdPartyLocation: function(userInput, protocol, instance) {
+ // make an object with the fields specified by that protocol. We
+ // require that the values of all but the last field come from the
+ // instance. The last is the user input.
+ const requiredFields = protocol.location_fields;
+ if (!requiredFields) return null;
+ const fields = {};
+ for (let i = 0; i < requiredFields.length - 1; ++i) {
+ const thisField = requiredFields[i];
+ if (instance.fields[thisField] === undefined) return null;
+ fields[thisField] = instance.fields[thisField];
+ }
+ fields[requiredFields[requiredFields.length - 1]] = userInput;
+ return fields;
+ },
+
+ /**
+ * called by the parent component when PageUp/Down/etc is pressed.
+ *
+ * We pass it down to the scroll panel.
+ */
+ handleScrollKey: function(ev) {
+ if (this.scrollPanel) {
+ this.scrollPanel.handleScrollKey(ev);
+ }
+ },
+
+ render: function() {
+ const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
+ const Loader = sdk.getComponent("elements.Spinner");
+
+ if (this.state.protocolsLoading) {
+ return (
+
+
+
+
+ );
+ }
+
+ let content;
+ if (this.state.loading) {
+ content =
+
+
;
+ } else {
+ const rows = this.getRows();
+ // we still show the scrollpanel, at least for now, because
+ // otherwise we don't fetch more because we don't get a fill
+ // request from the scrollpanel because there isn't one
+ let scrollpanel_content;
+ if (rows.length == 0) {
+ scrollpanel_content = { _t('No rooms to show') };
+ } else {
+ scrollpanel_content =
+ );
+ }
+});
+
+// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
+// but works with the objects we get from the public room list
+function get_display_alias_for_room(room) {
+ return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
+}
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index 47da4096c0..8034923158 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -296,7 +296,7 @@ module.exports = React.createClass({
if (hasUDE) {
title = _t("Message not sent due to unknown devices being present");
content = _t(
- "Show devices, mark devices known and send or cancel all.",
+ "Show devices, send anyway or cancel.",
{},
{
'showDevicesText': (sub) => { sub },
diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js
new file mode 100644
index 0000000000..fb82ee067b
--- /dev/null
+++ b/src/components/structures/RoomSubList.js
@@ -0,0 +1,401 @@
+/*
+Copyright 2017 Vector Creations Ltd
+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');
+var ReactDOM = require('react-dom');
+var classNames = require('classnames');
+var sdk = require('../../index');
+import { Droppable } from 'react-beautiful-dnd';
+import { _t } from '../../languageHandler';
+var dis = require('../../dispatcher');
+var Unread = require('../../Unread');
+var MatrixClientPeg = require('../../MatrixClientPeg');
+var RoomNotifs = require('../../RoomNotifs');
+var FormattingUtils = require('../../utils/FormattingUtils');
+var AccessibleButton = require('../../components/views/elements/AccessibleButton');
+import Modal from '../../Modal';
+import { KeyCode } from '../../Keyboard';
+
+
+// turn this on for drop & drag console debugging galore
+var debug = false;
+
+const TRUNCATE_AT = 10;
+
+var RoomSubList = React.createClass({
+ displayName: 'RoomSubList',
+
+ debug: debug,
+
+ propTypes: {
+ list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ label: React.PropTypes.string.isRequired,
+ tagName: React.PropTypes.string,
+ editable: React.PropTypes.bool,
+
+ order: React.PropTypes.string.isRequired,
+
+ // passed through to RoomTile and used to highlight room with `!` regardless of notifications count
+ isInvite: React.PropTypes.bool,
+
+ startAsHidden: React.PropTypes.bool,
+ showSpinner: React.PropTypes.bool, // true to show a spinner if 0 elements when expanded
+ collapsed: React.PropTypes.bool.isRequired, // is LeftPanel collapsed?
+ onHeaderClick: React.PropTypes.func,
+ alwaysShowHeader: React.PropTypes.bool,
+ incomingCall: React.PropTypes.object,
+ onShowMoreRooms: React.PropTypes.func,
+ searchFilter: React.PropTypes.string,
+ emptyContent: React.PropTypes.node, // content shown if the list is empty
+ headerItems: React.PropTypes.node, // content shown in the sublist header
+ extraTiles: React.PropTypes.arrayOf(React.PropTypes.node), // extra elements added beneath tiles
+ },
+
+ getInitialState: function() {
+ return {
+ hidden: this.props.startAsHidden || false,
+ truncateAt: TRUNCATE_AT,
+ sortedList: [],
+ };
+ },
+
+ getDefaultProps: function() {
+ return {
+ onHeaderClick: function() {}, // NOP
+ onShowMoreRooms: function() {}, // NOP
+ extraTiles: [],
+ isInvite: false,
+ };
+ },
+
+ componentWillMount: function() {
+ this.setState({
+ sortedList: this.applySearchFilter(this.props.list, this.props.searchFilter),
+ });
+ this.dispatcherRef = dis.register(this.onAction);
+ },
+
+ componentWillUnmount: function() {
+ dis.unregister(this.dispatcherRef);
+ },
+
+ componentWillReceiveProps: function(newProps) {
+ // order the room list appropriately before we re-render
+ //if (debug) console.log("received new props, list = " + newProps.list);
+ this.setState({
+ sortedList: this.applySearchFilter(newProps.list, newProps.searchFilter),
+ });
+ },
+
+ applySearchFilter: function(list, filter) {
+ if (filter === "") return list;
+ return list.filter((room) => {
+ return room.name && room.name.toLowerCase().indexOf(filter.toLowerCase()) >= 0
+ });
+ },
+
+ // The header is collapsable if it is hidden or not stuck
+ // The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
+ isCollapsableOnClick: function() {
+ var stuck = this.refs.header.dataset.stuck;
+ if (this.state.hidden || stuck === undefined || stuck === "none") {
+ return true;
+ } else {
+ return false;
+ }
+ },
+
+ onAction: function(payload) {
+ // XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
+ // but this is no longer true, so we must do it here (and can apply the small
+ // optimisation of checking that we care about the room being read).
+ //
+ // Ultimately we need to transition to a state pushing flow where something
+ // explicitly notifies the components concerned that the notif count for a room
+ // has change (e.g. a Flux store).
+ if (payload.action === 'on_room_read' &&
+ this.props.list.some((r) => r.roomId === payload.roomId)
+ ) {
+ this.forceUpdate();
+ }
+ },
+
+ onClick: function(ev) {
+ if (this.isCollapsableOnClick()) {
+ // The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
+ var isHidden = !this.state.hidden;
+ this.setState({ hidden : isHidden });
+
+ if (isHidden) {
+ // as good a way as any to reset the truncate state
+ this.setState({ truncateAt : TRUNCATE_AT });
+ }
+
+ this.props.onShowMoreRooms();
+ this.props.onHeaderClick(isHidden);
+ } else {
+ // The header is stuck, so the click is to be interpreted as a scroll to the header
+ this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition);
+ }
+ },
+
+ onRoomTileClick(roomId, ev) {
+ dis.dispatch({
+ action: 'view_room',
+ room_id: roomId,
+ clear_search: (ev && (ev.keyCode == KeyCode.ENTER || ev.keyCode == KeyCode.SPACE)),
+ });
+ },
+
+ _shouldShowNotifBadge: function(roomNotifState) {
+ const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD];
+ return showBadgeInStates.indexOf(roomNotifState) > -1;
+ },
+
+ _shouldShowMentionBadge: function(roomNotifState) {
+ return roomNotifState != RoomNotifs.MUTE;
+ },
+
+ /**
+ * Total up all the notification counts from the rooms
+ *
+ * @param {Number} If supplied will only total notifications for rooms outside the truncation number
+ * @returns {Array} The array takes the form [total, highlight] where highlight is a bool
+ */
+ roomNotificationCount: function(truncateAt) {
+ var self = this;
+
+ if (this.props.isInvite) {
+ return [0, true];
+ }
+
+ return this.props.list.reduce(function(result, room, index) {
+ if (truncateAt === undefined || index >= truncateAt) {
+ var roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
+ var highlight = room.getUnreadNotificationCount('highlight') > 0;
+ var notificationCount = room.getUnreadNotificationCount();
+
+ const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState);
+ const mentionBadges = highlight && self._shouldShowMentionBadge(roomNotifState);
+ const badges = notifBadges || mentionBadges;
+
+ if (badges) {
+ result[0] += notificationCount;
+ if (highlight) {
+ result[1] = true;
+ }
+ }
+ }
+ return result;
+ }, [0, false]);
+ },
+
+ _updateSubListCount: function() {
+ // Force an update by setting the state to the current state
+ // Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate()
+ // method is honoured
+ this.setState(this.state);
+ },
+
+ makeRoomTiles: function() {
+ const DNDRoomTile = sdk.getComponent("rooms.DNDRoomTile");
+ const RoomTile = sdk.getComponent("rooms.RoomTile");
+ return this.state.sortedList.map((room, index) => {
+ // XXX: is it evil to pass in this as a prop to RoomTile? Yes.
+
+ // We should only use when editable
+ const RoomTileComponent = this.props.editable ? DNDRoomTile : RoomTile;
+ return 0 || this.props.isInvite}
+ isInvite={this.props.isInvite}
+ refreshSubList={this._updateSubListCount}
+ incomingCall={null}
+ onClick={this.onRoomTileClick}
+ />;
+ });
+ },
+
+ _getHeaderJsx: function() {
+ var TintableSvg = sdk.getComponent("elements.TintableSvg");
+
+ var subListNotifications = this.roomNotificationCount();
+ var subListNotifCount = subListNotifications[0];
+ var subListNotifHighlight = subListNotifications[1];
+
+ var totalTiles = this.props.list.length + (this.props.extraTiles || []).length;
+ var roomCount = totalTiles > 0 ? totalTiles : '';
+
+ var chevronClasses = classNames({
+ 'mx_RoomSubList_chevron': true,
+ 'mx_RoomSubList_chevronRight': this.state.hidden,
+ 'mx_RoomSubList_chevronDown': !this.state.hidden,
+ });
+
+ var badgeClasses = classNames({
+ 'mx_RoomSubList_badge': true,
+ 'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
+ });
+
+ var badge;
+ if (subListNotifCount > 0) {
+ badge =
;
+ } else if (this.props.isInvite) {
+ // no notifications but highlight anyway because this is an invite badge
+ badge =
!
;
+ }
+
+ // When collapsed, allow a long hover on the header to show user
+ // the full tag name and room count
+ var title;
+ if (this.props.collapsed) {
+ title = this.props.label;
+ if (roomCount !== '') {
+ title += " [" + roomCount + "]";
+ }
+ }
+
+ var incomingCall;
+ if (this.props.incomingCall) {
+ var self = this;
+ // Check if the incoming call is for this section
+ var incomingCallRoom = this.props.list.filter(function(room) {
+ return self.props.incomingCall.roomId === room.roomId;
+ });
+
+ if (incomingCallRoom.length === 1) {
+ var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
+ incomingCall = ;
+ }
+ }
+
+ var tabindex = this.props.searchFilter === "" ? "0" : "-1";
+
+ return (
+
+ );
+ }
+ }
+});
+
+module.exports = RoomSubList;
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 27a70fdb10..c5f6a75cc5 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -264,12 +264,19 @@ module.exports = React.createClass({
isPeeking: true, // this will change to false if peeking fails
});
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
+ if (this.unmounted) {
+ return;
+ }
this.setState({
room: room,
peekLoading: false,
});
this._onRoomLoaded(room);
}, (err) => {
+ if (this.unmounted) {
+ return;
+ }
+
// Stop peeking if anything went wrong
this.setState({
isPeeking: false,
@@ -286,7 +293,7 @@ module.exports = React.createClass({
} else {
throw err;
}
- }).done();
+ });
}
} else if (room) {
// Stop peeking because we have joined this room previously
@@ -460,6 +467,15 @@ module.exports = React.createClass({
case 'message_sent':
this._checkIfAlone(this.state.room);
break;
+ case 'post_sticker_message':
+ this.injectSticker(
+ payload.data.content.url,
+ payload.data.content.info,
+ payload.data.description || payload.data.name);
+ break;
+ case 'picture_snapshot':
+ this.uploadFile(payload.file);
+ break;
case 'notifier_enabled':
case 'upload_failed':
case 'upload_started':
@@ -620,8 +636,8 @@ module.exports = React.createClass({
const room = this.state.room;
if (!room) return;
- const color_scheme = SettingsStore.getValue("roomColor", room.room_id);
console.log("Tinter.tint from updateTint");
+ const color_scheme = SettingsStore.getValue("roomColor", room.roomId);
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
},
@@ -670,23 +686,7 @@ module.exports = React.createClass({
// a member state changed in this room
// refresh the conf call notification state
this._updateConfCallNotification();
-
- // if we are now a member of the room, where we were not before, that
- // means we have finished joining a room we were previously peeking
- // into.
- const me = MatrixClientPeg.get().credentials.userId;
- if (this.state.joining && this.state.room.hasMembershipState(me, "join")) {
- // Having just joined a room, check to see if it looks like a DM room, and if so,
- // mark it as one. This is to work around the fact that some clients don't support
- // is_direct. We should remove this once they do.
- const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
- if (Rooms.looksLikeDirectMessageRoom(this.state.room, me)) {
- // XXX: There's not a whole lot we can really do if this fails: at best
- // perhaps we could try a couple more times, but since it's a temporary
- // compatability workaround, let's not bother.
- Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()).done();
- }
- }
+ this._updateDMState();
}, 500),
_checkIfAlone: function(room) {
@@ -727,6 +727,44 @@ module.exports = React.createClass({
});
},
+ _updateDMState() {
+ const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
+ if (!me || me.membership !== "join") {
+ return;
+ }
+
+ // The user may have accepted an invite with is_direct set
+ if (me.events.member.getPrevContent().membership === "invite" &&
+ me.events.member.getPrevContent().is_direct
+ ) {
+ // This is a DM with the sender of the invite event (which we assume
+ // preceded the join event)
+ Rooms.setDMRoom(
+ this.state.room.roomId,
+ me.events.member.getUnsigned().prev_sender,
+ );
+ return;
+ }
+
+ const invitedMembers = this.state.room.getMembersWithMembership("invite");
+ const joinedMembers = this.state.room.getMembersWithMembership("join");
+
+ // There must be one invited member and one joined member
+ if (invitedMembers.length !== 1 || joinedMembers.length !== 1) {
+ return;
+ }
+
+ // The user may have sent an invite with is_direct sent
+ const other = invitedMembers[0];
+ if (other &&
+ other.membership === "invite" &&
+ other.events.member.getContent().is_direct
+ ) {
+ Rooms.setDMRoom(this.state.room.roomId, other.userId);
+ return;
+ }
+ },
+
onSearchResultsResize: function() {
dis.dispatch({ action: 'timeline_resize' }, true);
},
@@ -819,18 +857,6 @@ module.exports = React.createClass({
action: 'join_room',
opts: { inviteSignUrl: signUrl },
});
-
- // if this is an invite and has the 'direct' hint set, mark it as a DM room now.
- if (this.state.room) {
- const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
- if (me && me.membership == 'invite') {
- if (me.events.member.getContent().is_direct) {
- // The 'direct' hint is there, so declare that this is a DM room for
- // whoever invited us.
- return Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender());
- }
- }
- }
return Promise.resolve();
});
},
@@ -882,28 +908,52 @@ module.exports = React.createClass({
this.setState({ draggingFile: false });
},
- uploadFile: function(file) {
+ uploadFile: async function(file) {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'view_set_mxid'});
return;
}
- ContentMessages.sendContentToRoom(
- file, this.state.room.roomId, MatrixClientPeg.get(),
- ).done(undefined, (error) => {
+ try {
+ await ContentMessages.sendContentToRoom(file, this.state.room.roomId, MatrixClientPeg.get());
+ } catch (error) {
if (error.name === "UnknownDeviceError") {
- // Let the staus bar handle this
+ // Let the status bar handle this
return;
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to upload file " + file + " " + error);
Modal.createTrackedDialog('Failed to upload file', '', ErrorDialog, {
title: _t('Failed to upload file'),
- description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
+ description: ((error && error.message)
+ ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
});
+
+ // bail early to avoid calling the dispatch below
+ return;
+ }
+
+ // Send message_sent callback, for things like _checkIfAlone because after all a file is still a message.
+ dis.dispatch({
+ action: 'message_sent',
});
},
+ injectSticker: function(url, info, text) {
+ if (MatrixClientPeg.get().isGuest()) {
+ dis.dispatch({action: 'view_set_mxid'});
+ return;
+ }
+
+ ContentMessages.sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
+ .done(undefined, (error) => {
+ if (error.name === "UnknownDeviceError") {
+ // Let the staus bar handle this
+ return;
+ }
+ });
+ },
+
onSearch: function(term, scope) {
this.setState({
searchTerm: term,
@@ -1586,7 +1636,8 @@ module.exports = React.createClass({
displayConfCallNotification={this.state.displayConfCallNotification}
maxHeight={this.state.auxPanelMaxHeight}
onResize={this.onChildResize}
- showApps={this.state.showApps && !this.state.editingRoomSettings} >
+ showApps={this.state.showApps}
+ hideAppsDrawer={this.state.editingRoomSettings} >
{ aux }
);
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index cbb6001d5f..0fdbc9a349 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -17,9 +17,9 @@ limitations under the License.
const React = require("react");
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
-const GeminiScrollbar = require('react-gemini-scrollbar');
import Promise from 'bluebird';
import { KeyCode } from '../../Keyboard';
+import sdk from '../../index.js';
const DEBUG_SCROLL = false;
// var DEBUG_SCROLL = true;
@@ -224,7 +224,7 @@ module.exports = React.createClass({
onResize: function() {
this.props.onResize();
this.checkScroll();
- this.refs.geminiPanel.forceUpdate();
+ if (this._gemScroll) this._gemScroll.forceUpdate();
},
// after an update to the contents of the panel, check that the scroll is
@@ -665,14 +665,25 @@ module.exports = React.createClass({
throw new Error("ScrollPanel._getScrollNode called when unmounted");
}
- return this.refs.geminiPanel.scrollbar.getViewElement();
+ if (!this._gemScroll) {
+ // Likewise, we should have the ref by this point, but if not
+ // turn the NPE into something meaningful.
+ throw new Error("ScrollPanel._getScrollNode called before gemini ref collected");
+ }
+
+ return this._gemScroll.scrollbar.getViewElement();
+ },
+
+ _collectGeminiScroll: function(gemScroll) {
+ this._gemScroll = gemScroll;
},
render: function() {
+ const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
// TODO: the classnames on the div and ol could do with being updated to
// reflect the fact that we don't necessarily contain a list of messages.
// it's not obvious why we have a separate div and ol anyway.
- return (
;
},
});
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index 3760bb37c5..1a03b5d994 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -302,6 +302,8 @@ var TimelinePanel = React.createClass({
// set off a pagination request.
onMessageListFillRequest: function(backwards) {
+ if (!this._shouldPaginate()) return Promise.resolve(false);
+
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
@@ -622,6 +624,7 @@ var TimelinePanel = React.createClass({
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
dis.dispatch({
action: 'on_room_read',
+ roomId: this.props.timelineSet.room.roomId,
});
}
}
@@ -1091,6 +1094,17 @@ var TimelinePanel = React.createClass({
}, this.props.onReadMarkerUpdated);
},
+ _shouldPaginate: function() {
+ // don't try to paginate while events in the timeline are
+ // still being decrypted. We don't render events while they're
+ // being decrypted, so they don't take up space in the timeline.
+ // This means we can pull quite a lot of events into the timeline
+ // and end up trying to render a lot of events.
+ return !this.state.events.some((e) => {
+ return e.isBeingDecrypted();
+ });
+ },
+
render: function() {
const MessagePanel = sdk.getComponent("structures.MessagePanel");
const Loader = sdk.getComponent("elements.Spinner");
@@ -1108,9 +1122,9 @@ var TimelinePanel = React.createClass({
// exist.
if (this.state.timelineLoading) {
return (
-
);
},
diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
new file mode 100644
index 0000000000..3a5d35a561
--- /dev/null
+++ b/src/components/structures/ViewSource.js
@@ -0,0 +1,57 @@
+/*
+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 React from 'react';
+import PropTypes from 'prop-types';
+import SyntaxHighlight from '../views/elements/SyntaxHighlight';
+
+
+module.exports = React.createClass({
+ displayName: 'ViewSource',
+
+ propTypes: {
+ content: PropTypes.object.isRequired,
+ onFinished: PropTypes.func.isRequired,
+ },
+
+ componentDidMount: function() {
+ document.addEventListener("keydown", this.onKeyDown);
+ },
+
+ componentWillUnmount: function() {
+ document.removeEventListener("keydown", this.onKeyDown);
+ },
+
+ onKeyDown: function(ev) {
+ if (ev.keyCode == 27) { // escape
+ ev.stopPropagation();
+ ev.preventDefault();
+ this.props.onFinished();
+ }
+ },
+
+ render: function() {
+ return (
+
+ );
+ }
+});
diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js
index 53688ee6c3..ca50b9db6e 100644
--- a/src/components/structures/login/ForgotPassword.js
+++ b/src/components/structures/login/ForgotPassword.js
@@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import Modal from "../../../Modal";
import MatrixClientPeg from "../../../MatrixClientPeg";
+import SdkConfig from "../../../SdkConfig";
import PasswordReset from "../../../PasswordReset";
@@ -185,7 +186,7 @@ module.exports = React.createClass({
);
} else {
let serverConfigSection;
- if (!config.disable_custom_urls) {
+ if (!SdkConfig.get().disable_custom_urls) {
serverConfigSection = (
{ _t('Sign in') };
+ header =
{ _t('Sign in') } { loader }
;
} else {
if (!this.state.errorText) {
- header =
{ _t('Sign in to get started') }
;
+ header =
{ _t('Sign in to get started') } { loader }
;
}
}
diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js
index b8a85c5f82..62a3ee4f68 100644
--- a/src/components/structures/login/Registration.js
+++ b/src/components/structures/login/Registration.js
@@ -61,6 +61,7 @@ module.exports = React.createClass({
// registration shouldn't know or care how login is done.
onLoginClick: PropTypes.func.isRequired,
onCancelClick: PropTypes.func,
+ onServerConfigChange: PropTypes.func.isRequired,
},
getInitialState: function() {
@@ -131,6 +132,7 @@ module.exports = React.createClass({
if (config.isUrl !== undefined) {
newState.isUrl = config.isUrl;
}
+ this.props.onServerConfigChange(config);
this.setState(newState, function() {
this._replaceClient();
});
diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js
index 47c217eb96..6fb86c9cd8 100644
--- a/src/components/views/avatars/BaseAvatar.js
+++ b/src/components/views/avatars/BaseAvatar.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.
@@ -16,6 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
+import { MatrixClient } from 'matrix-js-sdk';
import AvatarLogic from '../../../Avatar';
import sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton';
@@ -36,6 +38,10 @@ module.exports = React.createClass({
defaultToInitialLetter: PropTypes.bool, // true to add default url
},
+ contextTypes: {
+ matrixClient: PropTypes.instanceOf(MatrixClient),
+ },
+
getDefaultProps: function() {
return {
width: 40,
@@ -49,6 +55,16 @@ module.exports = React.createClass({
return this._getState(this.props);
},
+ componentWillMount() {
+ this.unmounted = false;
+ this.context.matrixClient.on('sync', this.onClientSync);
+ },
+
+ componentWillUnmount() {
+ this.unmounted = true;
+ this.context.matrixClient.removeListener('sync', this.onClientSync);
+ },
+
componentWillReceiveProps: function(nextProps) {
// work out if we need to call setState (if the image URLs array has changed)
const newState = this._getState(nextProps);
@@ -67,6 +83,23 @@ module.exports = React.createClass({
}
},
+ onClientSync: function(syncState, prevState) {
+ if (this.unmounted) return;
+
+ // Consider the client reconnected if there is no error with syncing.
+ // This means the state could be RECONNECTING, SYNCING or PREPARED.
+ const reconnected = syncState !== "ERROR" && prevState !== syncState;
+ if (reconnected &&
+ // Did we fall back?
+ this.state.urlsIndex > 0
+ ) {
+ // Start from the highest priority URL again
+ this.setState({
+ urlsIndex: 0,
+ });
+ }
+ },
+
_getState: function(props) {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, props.urls, default image ]
diff --git a/src/components/views/avatars/MemberPresenceAvatar.js b/src/components/views/avatars/MemberPresenceAvatar.js
deleted file mode 100644
index aa6def00ae..0000000000
--- a/src/components/views/avatars/MemberPresenceAvatar.js
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- 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.
- */
-
-'use strict';
-
-import React from "react";
-import PropTypes from 'prop-types';
-import * as sdk from "../../../index";
-import MatrixClientPeg from "../../../MatrixClientPeg";
-import AccessibleButton from '../elements/AccessibleButton';
-import Presence from "../../../Presence";
-import dispatcher from "../../../dispatcher";
-import * as ContextualMenu from "../../structures/ContextualMenu";
-import SettingsStore from "../../../settings/SettingsStore";
-
-// This is an avatar with presence information and controls on it.
-module.exports = React.createClass({
- displayName: 'MemberPresenceAvatar',
-
- propTypes: {
- member: PropTypes.object.isRequired,
- width: PropTypes.number,
- height: PropTypes.number,
- resizeMethod: PropTypes.string,
- },
-
- getDefaultProps: function() {
- return {
- width: 40,
- height: 40,
- resizeMethod: 'crop',
- };
- },
-
- getInitialState: function() {
- let presenceState = null;
- let presenceMessage = null;
-
- // RoomMembers do not necessarily have a user.
- if (this.props.member.user) {
- presenceState = this.props.member.user.presence;
- presenceMessage = this.props.member.user.presenceStatusMsg;
- }
-
- return {
- status: presenceState,
- message: presenceMessage,
- };
- },
-
- componentWillMount: function() {
- MatrixClientPeg.get().on("User.presence", this.onUserPresence);
- this.dispatcherRef = dispatcher.register(this.onAction);
- },
-
- componentWillUnmount: function() {
- if (MatrixClientPeg.get()) {
- MatrixClientPeg.get().removeListener("User.presence", this.onUserPresence);
- }
- dispatcher.unregister(this.dispatcherRef);
- },
-
- onAction: function(payload) {
- if (payload.action !== "self_presence_updated") return;
- if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) return;
- this.setState({
- status: payload.statusInfo.presence,
- message: payload.statusInfo.status_msg,
- });
- },
-
- onUserPresence: function(event, user) {
- if (user.userId !== MatrixClientPeg.get().getUserId()) return;
- this.setState({
- status: user.presence,
- message: user.presenceStatusMsg,
- });
- },
-
- onStatusChange: function(newStatus) {
- Presence.stopMaintainingStatus();
- if (newStatus === "online") {
- Presence.setState(newStatus);
- } else Presence.setState(newStatus, null, true);
- },
-
- onClick: function(e) {
- const PresenceContextMenu = sdk.getComponent('context_menus.PresenceContextMenu');
- const elementRect = e.target.getBoundingClientRect();
-
- // The window X and Y offsets are to adjust position when zoomed in to page
- const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3;
- const chevronOffset = 12;
- let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
- y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron
-
- ContextualMenu.createMenu(PresenceContextMenu, {
- chevronOffset: chevronOffset,
- chevronFace: 'bottom',
- left: x,
- top: y,
- menuWidth: 125,
- currentStatus: this.state.status,
- onChange: this.onStatusChange,
- });
-
- e.stopPropagation();
-
- // XXX NB the following assumes that user is non-null, which is not valid
- // const presenceState = this.props.member.user.presence;
- // const presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
- // const presenceLastTs = this.props.member.user.lastPresenceTs;
- // const presenceCurrentlyActive = this.props.member.user.currentlyActive;
- // const presenceMessage = this.props.member.user.presenceStatusMsg;
- },
-
- render: function() {
- const MemberAvatar = sdk.getComponent("avatars.MemberAvatar");
-
- let onClickFn = null;
- if (this.props.member.userId === MatrixClientPeg.get().getUserId()) {
- onClickFn = this.onClick;
- }
-
- const avatarNode = (
-
- );
- let statusNode = (
-
- );
-
- // LABS: Disable presence management functions for now
- // Also disable the presence information if there's no status information
- if (!SettingsStore.isFeatureEnabled("feature_presence_management") || !this.state.status) {
- statusNode = null;
- onClickFn = null;
- }
-
- let avatar = (
-
- { avatarNode }
- { statusNode }
-
- );
- if (onClickFn) {
- avatar = (
-
- { avatarNode }
- { statusNode }
-
- );
- }
- return avatar;
- },
-});
diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js
index cae02ac408..821448207f 100644
--- a/src/components/views/avatars/RoomAvatar.js
+++ b/src/components/views/avatars/RoomAvatar.js
@@ -17,6 +17,7 @@ import React from "react";
import PropTypes from 'prop-types';
import {ContentRepo} from "matrix-js-sdk";
import MatrixClientPeg from "../../../MatrixClientPeg";
+import Modal from '../../../Modal';
import sdk from "../../../index";
module.exports = React.createClass({
@@ -31,6 +32,7 @@ module.exports = React.createClass({
width: PropTypes.number,
height: PropTypes.number,
resizeMethod: PropTypes.string,
+ viewAvatarOnClick: PropTypes.bool,
},
getDefaultProps: function() {
@@ -48,12 +50,34 @@ module.exports = React.createClass({
};
},
+ componentWillMount: function() {
+ MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
+ },
+
+ componentWillUnmount: function() {
+ const cli = MatrixClientPeg.get();
+ if (cli) {
+ cli.removeListener("RoomState.events", this.onRoomStateEvents);
+ }
+ },
+
componentWillReceiveProps: function(newProps) {
this.setState({
urls: this.getImageUrls(newProps),
});
},
+ onRoomStateEvents: function(ev) {
+ if (!this.props.room ||
+ ev.getRoomId() !== this.props.room.roomId ||
+ ev.getType() !== 'm.room.avatar'
+ ) return;
+
+ this.setState({
+ urls: this.getImageUrls(this.props),
+ });
+ },
+
getImageUrls: function(props) {
return [
ContentRepo.getHttpUriForMxc(
@@ -87,10 +111,15 @@ module.exports = React.createClass({
const mlist = props.room.currentState.members;
const userIds = [];
+ const leftUserIds = [];
// for .. in optimisation to return early if there are >2 keys
for (const uid in mlist) {
if (mlist.hasOwnProperty(uid)) {
- userIds.push(uid);
+ if (["join", "invite"].includes(mlist[uid].membership)) {
+ userIds.push(uid);
+ } else {
+ leftUserIds.push(uid);
+ }
}
if (userIds.length > 2) {
return null;
@@ -112,6 +141,14 @@ module.exports = React.createClass({
false,
);
} else if (userIds.length == 1) {
+ // The other 1-1 user left, leaving just the current user, so show the left user's avatar
+ if (leftUserIds.length === 1) {
+ return mlist[leftUserIds[0]].getAvatarUrl(
+ MatrixClientPeg.get().getHomeserverUrl(),
+ props.width, props.height, props.resizeMethod,
+ false,
+ );
+ }
return mlist[userIds[0]].getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
Math.floor(props.width * window.devicePixelRatio),
@@ -124,17 +161,32 @@ module.exports = React.createClass({
}
},
+ onRoomAvatarClick: function() {
+ const avatarUrl = this.props.room.getAvatarUrl(
+ MatrixClientPeg.get().getHomeserverUrl(),
+ null, null, null, false);
+ const ImageView = sdk.getComponent("elements.ImageView");
+ const params = {
+ src: avatarUrl,
+ name: this.props.room.name,
+ };
+
+ Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
+ },
+
render: function() {
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
- const {room, oobData, ...otherProps} = this.props;
+ /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
+ const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props;
const roomName = room ? room.name : oobData.name;
return (
+ urls={this.state.urls}
+ onClick={this.props.viewAvatarOnClick ? this.onRoomAvatarClick : null} />
);
},
});
diff --git a/src/components/views/context_menus/GenericElementContextMenu.js b/src/components/views/context_menus/GenericElementContextMenu.js
new file mode 100644
index 0000000000..3f4804dbd1
--- /dev/null
+++ b/src/components/views/context_menus/GenericElementContextMenu.js
@@ -0,0 +1,60 @@
+/*
+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.
+*/
+
+'use strict';
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+/*
+ * This component can be used to display generic HTML content in a contextual
+ * menu.
+ */
+
+
+export default class GenericElementContextMenu extends React.Component {
+ static PropTypes = {
+ element: PropTypes.element.isRequired,
+ // Function to be called when the parent window is resized
+ // This can be used to reposition or close the menu on resize and
+ // ensure that it is not displayed in a stale position.
+ onResize: PropTypes.func,
+ };
+
+ constructor(props) {
+ super(props);
+ this.resize = this.resize.bind(this);
+ }
+
+ componentDidMount() {
+ this.resize = this.resize.bind(this);
+ window.addEventListener("resize", this.resize);
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("resize", this.resize);
+ }
+
+ resize() {
+ if (this.props.onResize) {
+ this.props.onResize();
+ }
+ }
+
+ render() {
+ return
{ this.props.element }
;
+ }
+}
diff --git a/src/components/views/context_menus/GenericTextContextMenu.js b/src/components/views/context_menus/GenericTextContextMenu.js
new file mode 100644
index 0000000000..2319fe05a2
--- /dev/null
+++ b/src/components/views/context_menus/GenericTextContextMenu.js
@@ -0,0 +1,30 @@
+/*
+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.
+*/
+
+'use strict';
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+export default class GenericTextContextMenu extends React.Component {
+ static PropTypes = {
+ message: PropTypes.string.isRequired,
+ };
+
+ render() {
+ return
{ this.props.message }
;
+ }
+}
diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
new file mode 100644
index 0000000000..99ec493ced
--- /dev/null
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -0,0 +1,344 @@
+/*
+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.
+*/
+
+'use strict';
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import MatrixClientPeg from '../../../MatrixClientPeg';
+import dis from '../../../dispatcher';
+import sdk from '../../../index';
+import { _t } from '../../../languageHandler';
+import Modal from '../../../Modal';
+import Resend from '../../../Resend';
+import SettingsStore from '../../../settings/SettingsStore';
+import {makeEventPermalink} from '../../../matrix-to';
+import { isUrlPermitted } from '../../../HtmlUtils';
+
+module.exports = React.createClass({
+ displayName: 'MessageContextMenu',
+
+ propTypes: {
+ /* the MatrixEvent associated with the context menu */
+ mxEvent: PropTypes.object.isRequired,
+
+ /* an optional EventTileOps implementation that can be used to unhide preview widgets */
+ eventTileOps: PropTypes.object,
+
+ /* an optional function to be called when the user clicks collapse thread, if not provided hide button */
+ collapseReplyThread: PropTypes.func,
+
+ /* callback called when the menu is dismissed */
+ onFinished: PropTypes.func,
+ },
+
+ getInitialState: function() {
+ return {
+ canRedact: false,
+ canPin: false,
+ };
+ },
+
+ componentWillMount: function() {
+ MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
+ this._checkPermissions();
+ },
+
+ componentWillUnmount: function() {
+ const cli = MatrixClientPeg.get();
+ if (cli) {
+ cli.removeListener('RoomMember.powerLevel', this._checkPermissions);
+ }
+ },
+
+ _checkPermissions: function() {
+ const cli = MatrixClientPeg.get();
+ const room = cli.getRoom(this.props.mxEvent.getRoomId());
+
+ const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId);
+ let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
+
+ // HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
+ if (!SettingsStore.isFeatureEnabled("feature_pinning")) canPin = false;
+
+ this.setState({canRedact, canPin});
+ },
+
+ _isPinned: function() {
+ const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
+ const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
+ if (!pinnedEvent) return false;
+ return pinnedEvent.getContent().pinned.includes(this.props.mxEvent.getId());
+ },
+
+ onResendClick: function() {
+ Resend.resend(this.props.mxEvent);
+ this.closeMenu();
+ },
+
+ onViewSourceClick: function() {
+ const ViewSource = sdk.getComponent('structures.ViewSource');
+ Modal.createTrackedDialog('View Event Source', '', ViewSource, {
+ content: this.props.mxEvent.event,
+ }, 'mx_Dialog_viewsource');
+ this.closeMenu();
+ },
+
+ onViewClearSourceClick: function() {
+ const ViewSource = sdk.getComponent('structures.ViewSource');
+ Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
+ // FIXME: _clearEvent is private
+ content: this.props.mxEvent._clearEvent,
+ }, 'mx_Dialog_viewsource');
+ this.closeMenu();
+ },
+
+ onRedactClick: function() {
+ const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
+ Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
+ onFinished: (proceed) => {
+ if (!proceed) return;
+
+ const cli = MatrixClientPeg.get();
+ cli.redactEvent(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId()).catch(function(e) {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ // display error message stating you couldn't delete this.
+ const code = e.errcode || e.statusCode;
+ Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
+ title: _t('Error'),
+ description: _t('You cannot delete this message. (%(code)s)', {code}),
+ });
+ }).done();
+ },
+ }, 'mx_Dialog_confirmredact');
+ this.closeMenu();
+ },
+
+ onCancelSendClick: function() {
+ Resend.removeFromQueue(this.props.mxEvent);
+ this.closeMenu();
+ },
+
+ onForwardClick: function() {
+ dis.dispatch({
+ action: 'forward_event',
+ event: this.props.mxEvent,
+ });
+ this.closeMenu();
+ },
+
+ onPinClick: function() {
+ MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '')
+ .catch((e) => {
+ // Intercept the Event Not Found error and fall through the promise chain with no event.
+ if (e.errcode === "M_NOT_FOUND") return null;
+ throw e;
+ })
+ .then((event) => {
+ const eventIds = (event ? event.pinned : []) || [];
+ if (!eventIds.includes(this.props.mxEvent.getId())) {
+ // Not pinned - add
+ eventIds.push(this.props.mxEvent.getId());
+ } else {
+ // Pinned - remove
+ eventIds.splice(eventIds.indexOf(this.props.mxEvent.getId()), 1);
+ }
+
+ const cli = MatrixClientPeg.get();
+ cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, '');
+ });
+ this.closeMenu();
+ },
+
+ closeMenu: function() {
+ if (this.props.onFinished) this.props.onFinished();
+ },
+
+ onUnhidePreviewClick: function() {
+ if (this.props.eventTileOps) {
+ this.props.eventTileOps.unhideWidget();
+ }
+ this.closeMenu();
+ },
+
+ onQuoteClick: function() {
+ dis.dispatch({
+ action: 'quote',
+ text: this.props.eventTileOps.getInnerText(),
+ });
+ this.closeMenu();
+ },
+
+ onReplyClick: function() {
+ dis.dispatch({
+ action: 'reply_to_event',
+ event: this.props.mxEvent,
+ });
+ this.closeMenu();
+ },
+
+ onCollapseReplyThreadClick: function() {
+ this.props.collapseReplyThread();
+ this.closeMenu();
+ },
+
+ render: function() {
+ const eventStatus = this.props.mxEvent.status;
+ let resendButton;
+ let redactButton;
+ let cancelButton;
+ let forwardButton;
+ let pinButton;
+ let viewClearSourceButton;
+ let unhidePreviewButton;
+ let externalURLButton;
+ let quoteButton;
+ let replyButton;
+ let collapseReplyThread;
+
+ if (eventStatus === 'not_sent') {
+ resendButton = (
+
+ );
+ }
+
+ if (this.props.eventTileOps) {
+ if (this.props.eventTileOps.isWidgetHidden()) {
+ unhidePreviewButton = (
+
+ { _t('Unhide Preview') }
+
+ );
+ }
+ }
+
+ // XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
+ const permalinkButton = (
+
+ );
+ }
+
+ // Bridges can provide a 'external_url' to link back to the source.
+ if (
+ typeof(this.props.mxEvent.event.content.external_url) === "string" &&
+ isUrlPermitted(this.props.mxEvent.event.content.external_url)
+ ) {
+ externalURLButton = (
+
+ );
+ },
+});
diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js
new file mode 100644
index 0000000000..32f5365b82
--- /dev/null
+++ b/src/components/views/context_menus/TagTileContextMenu.js
@@ -0,0 +1,75 @@
+/*
+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 React from 'react';
+import PropTypes from 'prop-types';
+import { _t } from '../../../languageHandler';
+import dis from '../../../dispatcher';
+import TagOrderActions from '../../../actions/TagOrderActions';
+import MatrixClientPeg from '../../../MatrixClientPeg';
+import sdk from '../../../index';
+
+export default class TagTileContextMenu extends React.Component {
+ static propTypes = {
+ tag: PropTypes.string.isRequired,
+ /* callback called when the menu is dismissed */
+ onFinished: PropTypes.func.isRequired,
+ };
+
+ constructor() {
+ super();
+
+ this._onViewCommunityClick = this._onViewCommunityClick.bind(this);
+ this._onRemoveClick = this._onRemoveClick.bind(this);
+ }
+
+ _onViewCommunityClick() {
+ dis.dispatch({
+ action: 'view_group',
+ group_id: this.props.tag,
+ });
+ this.props.onFinished();
+ }
+
+ _onRemoveClick() {
+ dis.dispatch(TagOrderActions.removeTag(
+ // XXX: Context menus don't have a MatrixClient context
+ MatrixClientPeg.get(),
+ this.props.tag,
+ ));
+ this.props.onFinished();
+ }
+
+ render() {
+ const TintableSvg = sdk.getComponent("elements.TintableSvg");
+ return
+
+
+ { _t('View Community') }
+
+
+
+
+ { _t('Remove') }
+
+
;
+ }
+}
diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js
index 685c4fcde3..0d0b7456b5 100644
--- a/src/components/views/dialogs/AddressPickerDialog.js
+++ b/src/components/views/dialogs/AddressPickerDialog.js
@@ -22,7 +22,7 @@ import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
-import GroupStoreCache from '../../../stores/GroupStoreCache';
+import GroupStore from '../../../stores/GroupStore';
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@@ -243,9 +243,8 @@ module.exports = React.createClass({
_doNaiveGroupRoomSearch: function(query) {
const lowerCaseQuery = query.toLowerCase();
- const groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
const results = [];
- groupStore.getGroupRooms().forEach((r) => {
+ GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js
index 8e6f944df3..71a5da224c 100644
--- a/src/components/views/dialogs/BaseDialog.js
+++ b/src/components/views/dialogs/BaseDialog.js
@@ -1,5 +1,6 @@
/*
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.
@@ -15,11 +16,15 @@ limitations under the License.
*/
import React from 'react';
+import FocusTrap from 'focus-trap-react';
import PropTypes from 'prop-types';
+import { MatrixClient } from 'matrix-js-sdk';
+
import { KeyCode } from '../../../Keyboard';
import AccessibleButton from '../elements/AccessibleButton';
import sdk from '../../../index';
+import MatrixClientPeg from '../../../MatrixClientPeg';
/**
* Basic container for modal dialogs.
@@ -32,10 +37,20 @@ export default React.createClass({
propTypes: {
// onFinished callback to call when Escape is pressed
+ // Take a boolean which is true if the dialog was dismissed
+ // with a positive / confirm action or false if it was
+ // cancelled (BaseDialog itself only calls this with false).
onFinished: PropTypes.func.isRequired,
- // callback to call when Enter is pressed
- onEnterPressed: PropTypes.func,
+ // Whether the dialog should have a 'close' button that will
+ // cause the dialog to be cancelled. This should only be set
+ // to false if there is nothing the app can sensibly do if the
+ // dialog is cancelled, eg. "We can't restore your session and
+ // the app cannot work". Default: true.
+ hasCancel: PropTypes.bool,
+
+ // called when a key is pressed
+ onKeyDown: PropTypes.func,
// CSS class to apply to dialog div
className: PropTypes.string,
@@ -46,41 +61,73 @@ export default React.createClass({
// children should be the content of the dialog
children: PropTypes.node,
+
+ // Id of content element
+ // If provided, this is used to add a aria-describedby attribute
+ contentId: React.PropTypes.string,
+ },
+
+ getDefaultProps: function() {
+ return {
+ hasCancel: true,
+ };
+ },
+
+ childContextTypes: {
+ matrixClient: PropTypes.instanceOf(MatrixClient),
+ },
+
+ getChildContext: function() {
+ return {
+ matrixClient: this._matrixClient,
+ };
+ },
+
+ componentWillMount() {
+ this._matrixClient = MatrixClientPeg.get();
},
_onKeyDown: function(e) {
- if (e.keyCode === KeyCode.ESCAPE) {
+ if (this.props.onKeyDown) {
+ this.props.onKeyDown(e);
+ }
+ if (this.props.hasCancel && e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation();
e.preventDefault();
- this.props.onFinished();
- } else if (e.keyCode === KeyCode.ENTER) {
- if (this.props.onEnterPressed) {
- e.stopPropagation();
- e.preventDefault();
- this.props.onEnterPressed(e);
- }
+ this.props.onFinished(false);
}
},
_onCancelClick: function(e) {
- this.props.onFinished();
+ this.props.onFinished(false);
},
render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
return (
-
-
+ { this.props.hasCancel ?
-
-
+ : null }
+
{ this.props.title }
{ this.props.children }
-
+
);
},
});
diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js
new file mode 100644
index 0000000000..83cdb1f07f
--- /dev/null
+++ b/src/components/views/dialogs/BugReportDialog.js
@@ -0,0 +1,203 @@
+/*
+Copyright 2017 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.
+*/
+
+import React from 'react';
+import sdk from '../../../index';
+import SdkConfig from '../../../SdkConfig';
+import Modal from '../../../Modal';
+import { _t } from '../../../languageHandler';
+
+export default class BugReportDialog extends React.Component {
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ sendLogs: true,
+ busy: false,
+ err: null,
+ issueUrl: "",
+ text: "",
+ progress: null,
+ };
+ this._unmounted = false;
+ this._onSubmit = this._onSubmit.bind(this);
+ this._onCancel = this._onCancel.bind(this);
+ this._onTextChange = this._onTextChange.bind(this);
+ this._onIssueUrlChange = this._onIssueUrlChange.bind(this);
+ this._onSendLogsChange = this._onSendLogsChange.bind(this);
+ this._sendProgressCallback = this._sendProgressCallback.bind(this);
+ }
+
+ componentWillUnmount() {
+ this._unmounted = true;
+ }
+
+ _onCancel(ev) {
+ this.props.onFinished(false);
+ }
+
+ _onSubmit(ev) {
+ const userText =
+ (this.state.text.length > 0 ? this.state.text + '\n\n': '') + 'Issue: ' +
+ (this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given');
+
+ this.setState({ busy: true, progress: null, err: null });
+ this._sendProgressCallback(_t("Preparing to send logs"));
+
+ require(['../../../rageshake/submit-rageshake'], (s) => {
+ s(SdkConfig.get().bug_report_endpoint_url, {
+ userText,
+ sendLogs: true,
+ progressCallback: this._sendProgressCallback,
+ }).then(() => {
+ if (!this._unmounted) {
+ this.props.onFinished(false);
+ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+ // N.B. first param is passed to piwik and so doesn't want i18n
+ Modal.createTrackedDialog('Bug report sent', '', QuestionDialog, {
+ title: _t('Logs sent'),
+ description: _t('Thank you!'),
+ hasCancelButton: false,
+ });
+ }
+ }, (err) => {
+ if (!this._unmounted) {
+ this.setState({
+ busy: false,
+ progress: null,
+ err: _t("Failed to send logs: ") + `${err.message}`,
+ });
+ }
+ });
+ });
+ }
+
+ _onTextChange(ev) {
+ this.setState({ text: ev.target.value });
+ }
+
+ _onIssueUrlChange(ev) {
+ this.setState({ issueUrl: ev.target.value });
+ }
+
+ _onSendLogsChange(ev) {
+ this.setState({ sendLogs: ev.target.checked });
+ }
+
+ _sendProgressCallback(progress) {
+ if (this._unmounted) {
+ return;
+ }
+ this.setState({progress: progress});
+ }
+
+ render() {
+ const Loader = sdk.getComponent("elements.Spinner");
+ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
+ const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+
+ let error = null;
+ if (this.state.err) {
+ error =
+ {this.state.err}
+
;
+ }
+
+ let progress = null;
+ if (this.state.busy) {
+ progress = (
+
+
+ {this.state.progress} ...
+
+ );
+ }
+
+ return (
+
+
+
+ { _t(
+ "Debug logs contain application usage data including your " +
+ "username, the IDs or aliases of the rooms or groups you " +
+ "have visited and the usernames of other users. They do " +
+ "not contain messages.",
+ ) }
+
+
+
+ );
+ }
+}
+
+BugReportDialog.propTypes = {
+ onFinished: React.PropTypes.func.isRequired,
+};
diff --git a/src/components/views/dialogs/ChangelogDialog.js b/src/components/views/dialogs/ChangelogDialog.js
new file mode 100644
index 0000000000..e71fcdb624
--- /dev/null
+++ b/src/components/views/dialogs/ChangelogDialog.js
@@ -0,0 +1,94 @@
+/*
+ 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.
+ */
+
+import React from 'react';
+import sdk from '../../../index';
+import request from 'browser-request';
+import { _t } from '../../../languageHandler';
+
+const REPOS = ['vector-im/riot-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk'];
+
+export default class ChangelogDialog extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {};
+ }
+
+ componentDidMount() {
+ const version = this.props.newVersion.split('-');
+ const version2 = this.props.version.split('-');
+ if(version == null || version2 == null) return;
+ // parse versions of form: [vectorversion]-react-[react-sdk-version]-js-[js-sdk-version]
+ for(let i=0; i {
+ if(body == null) return;
+ this.setState({[REPOS[i]]: JSON.parse(body).commits});
+ });
+ }
+ }
+
+ _elementsForCommit(commit) {
+ return (
+
diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js
index d811dde09e..04f99a0e15 100644
--- a/src/components/views/dialogs/CreateGroupDialog.js
+++ b/src/components/views/dialogs/CreateGroupDialog.js
@@ -55,11 +55,15 @@ export default React.createClass({
_checkGroupId: function(e) {
let error = null;
- if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
+ if (!this.state.groupId) {
+ error = _t("Community IDs cannot not be empty.");
+ } else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
}
this.setState({
groupIdError: error,
+ // Reset createError to get rid of now stale error message
+ createError: null,
});
return error;
},
@@ -108,7 +112,7 @@ export default React.createClass({
// XXX: We should catch errcodes and give sensible i18ned messages for them,
// rather than displaying what the server gives us, but synapse doesn't give
// any yet.
- createErrorNode =
+ createErrorNode =
{ _t('Something went wrong whilst creating your community') }