Merge branches 'develop' and 'devtools_serverlist' of github.com:matrix-org/matrix-react-sdk into devtools_serverlist
21
.babelrc
|
@ -1,4 +1,21 @@
|
|||
{
|
||||
"presets": ["react", "es2015", "es2016"],
|
||||
"plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-bluebird", "transform-runtime", "add-module-exports", "syntax-dynamic-import"]
|
||||
"presets": [
|
||||
"react",
|
||||
"es2015",
|
||||
"es2016"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"transform-builtin-extend",
|
||||
{
|
||||
"globals": ["Error"]
|
||||
}
|
||||
],
|
||||
"transform-class-properties",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-bluebird",
|
||||
"transform-runtime",
|
||||
"add-module-exports",
|
||||
"syntax-dynamic-import"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -3,25 +3,37 @@ steps:
|
|||
command:
|
||||
- "yarn install"
|
||||
- "yarn lintwithexclusions"
|
||||
- "yarn stylelint"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
|
||||
# - label: ":chains: End-to-End Tests"
|
||||
# command:
|
||||
# # TODO: Remove hacky chmod for BuildKite
|
||||
# - "chmod +x ./scripts/ci/*.sh"
|
||||
# - "chmod +x ./scripts/*"
|
||||
# - "sudo apt-get install build-essential python2.7-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev"
|
||||
# - "./scripts/ci/install-deps.sh"
|
||||
# - "./scripts/ci/end-to-end-tests.sh"
|
||||
# plugins:
|
||||
# - docker#v3.0.1:
|
||||
# image: "node:10"
|
||||
- label: ":chains: End-to-End Tests"
|
||||
agents:
|
||||
# We use a medium sized instance instead of the normal small ones because
|
||||
# e2e tests otherwise take +-8min
|
||||
queue: "medium"
|
||||
command:
|
||||
# TODO: Remove hacky chmod for BuildKite
|
||||
- "echo '--- Setup'"
|
||||
- "chmod +x ./scripts/ci/*.sh"
|
||||
- "chmod +x ./scripts/*"
|
||||
- "echo '--- Install js-sdk'"
|
||||
- "./scripts/ci/install-deps.sh"
|
||||
- "./scripts/ci/end-to-end-tests.sh"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "matrixdotorg/riotweb-ci-e2etests-env:latest"
|
||||
propagate-environment: true
|
||||
|
||||
- label: ":karma: Tests"
|
||||
agents:
|
||||
# We use a medium sized instance instead of the normal small ones because
|
||||
# webpack loves to gorge itself on resources.
|
||||
queue: "medium"
|
||||
command:
|
||||
# Install chrome
|
||||
- "echo '--- Installing Chrome'"
|
||||
- "wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -"
|
||||
- "sh -c 'echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list'"
|
||||
- "apt-get update"
|
||||
|
@ -30,7 +42,9 @@ steps:
|
|||
# TODO: Remove hacky chmod for BuildKite
|
||||
- "chmod +x ./scripts/ci/*.sh"
|
||||
- "chmod +x ./scripts/*"
|
||||
- "echo '--- Installing Dependencies'"
|
||||
- "./scripts/ci/install-deps.sh"
|
||||
- "echo '+++ Running Tests'"
|
||||
- "./scripts/ci/unit-tests.sh"
|
||||
env:
|
||||
CHROME_BIN: "/usr/bin/google-chrome-stable"
|
||||
|
@ -40,8 +54,13 @@ steps:
|
|||
propagate-environment: true
|
||||
|
||||
- label: "🔧 Riot Tests"
|
||||
agents:
|
||||
# We use a medium sized instance instead of the normal small ones because
|
||||
# webpack loves to gorge itself on resources.
|
||||
queue: "medium"
|
||||
command:
|
||||
# Install chrome
|
||||
- "echo '--- Installing Chrome'"
|
||||
- "wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -"
|
||||
- "sh -c 'echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list'"
|
||||
- "apt-get update"
|
||||
|
@ -50,7 +69,9 @@ steps:
|
|||
# TODO: Remove hacky chmod for BuildKite
|
||||
- "chmod +x ./scripts/ci/*.sh"
|
||||
- "chmod +x ./scripts/*"
|
||||
- "echo '--- Installing Dependencies'"
|
||||
- "./scripts/ci/install-deps.sh"
|
||||
- "echo '+++ Running Tests'"
|
||||
- "./scripts/ci/riot-unit-tests.sh"
|
||||
env:
|
||||
CHROME_BIN: "/usr/bin/google-chrome-stable"
|
||||
|
@ -58,3 +79,13 @@ steps:
|
|||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
propagate-environment: true
|
||||
|
||||
- wait
|
||||
|
||||
- label: "🐴 Trigger riot-web"
|
||||
trigger: "riot-web"
|
||||
branches: "develop"
|
||||
build:
|
||||
branch: "develop"
|
||||
message: "[react-sdk] ${BUILDKITE_MESSAGE}"
|
||||
async: true
|
||||
|
|
|
@ -10,7 +10,6 @@ src/components/structures/RoomStatusBar.js
|
|||
src/components/structures/RoomView.js
|
||||
src/components/structures/ScrollPanel.js
|
||||
src/components/structures/SearchBox.js
|
||||
src/components/structures/TimelinePanel.js
|
||||
src/components/structures/UploadBar.js
|
||||
src/components/views/avatars/BaseAvatar.js
|
||||
src/components/views/avatars/MemberAvatar.js
|
||||
|
@ -52,7 +51,6 @@ 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/GroupAddressPicker.js
|
||||
src/HtmlUtils.js
|
||||
src/ImageUtils.js
|
||||
|
@ -60,7 +58,6 @@ src/languageHandler.js
|
|||
src/linkify-matrix.js
|
||||
src/Markdown.js
|
||||
src/MatrixClientPeg.js
|
||||
src/Modal.js
|
||||
src/notifications/ContentRules.js
|
||||
src/notifications/PushRuleVectorState.js
|
||||
src/notifications/VectorPushRulesDefinitions.js
|
||||
|
|
3
.gitignore
vendored
|
@ -9,9 +9,6 @@ package-lock.json
|
|||
/git-revision.txt
|
||||
/matrix-react-sdk-*.tgz
|
||||
|
||||
# test reports created by karma
|
||||
/karma-reports
|
||||
|
||||
/.idea
|
||||
/src/component-index.js
|
||||
|
||||
|
|
15
.stylelintrc.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"indentation": 4,
|
||||
"comment-empty-line-before": null,
|
||||
"declaration-empty-line-before": null,
|
||||
"length-zero-no-unit": null,
|
||||
"rule-empty-line-before": null,
|
||||
"color-hex-length": null,
|
||||
"max-empty-lines": null,
|
||||
"number-no-trailing-zeros": null,
|
||||
"number-leading-zero": null,
|
||||
"selector-list-comma-newline-after": null,
|
||||
}
|
||||
}
|
310
CHANGELOG.md
|
@ -1,3 +1,313 @@
|
|||
Changes in [1.1.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.1.0) (2019-05-07)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.1.0-rc.1...v1.1.0)
|
||||
|
||||
* Relax password requirements to score of 3 out of 4
|
||||
[\#2949](https://github.com/matrix-org/matrix-react-sdk/pull/2949)
|
||||
* Restore access to message quote option on first click
|
||||
[\#2948](https://github.com/matrix-org/matrix-react-sdk/pull/2948)
|
||||
* Check for `room` in all `Room.timeline*` handlers
|
||||
[\#2946](https://github.com/matrix-org/matrix-react-sdk/pull/2946)
|
||||
|
||||
Changes in [1.1.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.1.0-rc.1) (2019-04-30)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.0.7...v1.1.0-rc.1)
|
||||
|
||||
* Add important info to new preview bar
|
||||
[\#2936](https://github.com/matrix-org/matrix-react-sdk/pull/2936)
|
||||
* Add a message action bar
|
||||
[\#2935](https://github.com/matrix-org/matrix-react-sdk/pull/2935)
|
||||
* Trigger riot-web build
|
||||
[\#2934](https://github.com/matrix-org/matrix-react-sdk/pull/2934)
|
||||
* Input validation tooltips for registration
|
||||
[\#2933](https://github.com/matrix-org/matrix-react-sdk/pull/2933)
|
||||
* Also say "Connect ..." on remaining key backup buttons
|
||||
[\#2931](https://github.com/matrix-org/matrix-react-sdk/pull/2931)
|
||||
* Mark a few CSS classes as not selectable
|
||||
[\#2929](https://github.com/matrix-org/matrix-react-sdk/pull/2929)
|
||||
* Cleanup message composer render() method
|
||||
[\#2883](https://github.com/matrix-org/matrix-react-sdk/pull/2883)
|
||||
* Redesigned room preview bar
|
||||
[\#2925](https://github.com/matrix-org/matrix-react-sdk/pull/2925)
|
||||
* Prevent user pills containing only emoji from embiggening
|
||||
[\#2907](https://github.com/matrix-org/matrix-react-sdk/pull/2907)
|
||||
* Make alt-enter insert new line on macOS
|
||||
[\#2923](https://github.com/matrix-org/matrix-react-sdk/pull/2923)
|
||||
* Test `defaultServerName` before showing it on forgot password
|
||||
[\#2924](https://github.com/matrix-org/matrix-react-sdk/pull/2924)
|
||||
* Add a function to append/overwrite objects in the config on the fly
|
||||
[\#2922](https://github.com/matrix-org/matrix-react-sdk/pull/2922)
|
||||
* use SdkConfig brand name instead of static "Riot"
|
||||
[\#2921](https://github.com/matrix-org/matrix-react-sdk/pull/2921)
|
||||
* Use dedicated permalink creators in search results with multiple rooms
|
||||
[\#2898](https://github.com/matrix-org/matrix-react-sdk/pull/2898)
|
||||
* Clarify that use backup means restore
|
||||
[\#2917](https://github.com/matrix-org/matrix-react-sdk/pull/2917)
|
||||
* Fix key backup status when missing device
|
||||
[\#2919](https://github.com/matrix-org/matrix-react-sdk/pull/2919)
|
||||
* Ensure `<b>` tags appear bold for all browsers
|
||||
[\#2918](https://github.com/matrix-org/matrix-react-sdk/pull/2918)
|
||||
* Add a link in room settings to get at the tombstoned room if it exists
|
||||
[\#2908](https://github.com/matrix-org/matrix-react-sdk/pull/2908)
|
||||
* Add a generic error page element for startup errors
|
||||
[\#2915](https://github.com/matrix-org/matrix-react-sdk/pull/2915)
|
||||
* Add strings for js-sdk autodiscovery errors
|
||||
[\#2916](https://github.com/matrix-org/matrix-react-sdk/pull/2916)
|
||||
* Focus the composer view on file upload
|
||||
[\#2914](https://github.com/matrix-org/matrix-react-sdk/pull/2914)
|
||||
* use medium agent for e2e tests
|
||||
[\#2911](https://github.com/matrix-org/matrix-react-sdk/pull/2911)
|
||||
* adjust prop in HeaderButton
|
||||
[\#2912](https://github.com/matrix-org/matrix-react-sdk/pull/2912)
|
||||
* Remove breadcrumb scroll tolerances and use sensible defaults
|
||||
[\#2913](https://github.com/matrix-org/matrix-react-sdk/pull/2913)
|
||||
* Fix having to click the member list button twice to show it after having
|
||||
changed room.
|
||||
[\#2906](https://github.com/matrix-org/matrix-react-sdk/pull/2906)
|
||||
* Add period to the end of upgrade notice
|
||||
[\#2909](https://github.com/matrix-org/matrix-react-sdk/pull/2909)
|
||||
* Remove duplicate space in credits
|
||||
[\#2889](https://github.com/matrix-org/matrix-react-sdk/pull/2889)
|
||||
* Handle M_UNSUPPORTED_ROOM_VERSION in invites and room creation
|
||||
[\#2905](https://github.com/matrix-org/matrix-react-sdk/pull/2905)
|
||||
* Re-enable E2E tests
|
||||
[\#2867](https://github.com/matrix-org/matrix-react-sdk/pull/2867)
|
||||
* Remove BottomLeftMenu and supporting bits
|
||||
[\#2903](https://github.com/matrix-org/matrix-react-sdk/pull/2903)
|
||||
* Fix for retina thumbnails being massive
|
||||
[\#2439](https://github.com/matrix-org/matrix-react-sdk/pull/2439)
|
||||
* Send breadcrumb updates only when they change
|
||||
[\#2894](https://github.com/matrix-org/matrix-react-sdk/pull/2894)
|
||||
* Add some tolerances to breadcrumb scrolling
|
||||
[\#2892](https://github.com/matrix-org/matrix-react-sdk/pull/2892)
|
||||
* Fix validation to avoid `undefined` class on fields
|
||||
[\#2902](https://github.com/matrix-org/matrix-react-sdk/pull/2902)
|
||||
* Always return a client from onRegistered
|
||||
[\#2895](https://github.com/matrix-org/matrix-react-sdk/pull/2895)
|
||||
* Fix room upgrade warnings popping up in upgraded rooms
|
||||
[\#2897](https://github.com/matrix-org/matrix-react-sdk/pull/2897)
|
||||
* Fix style lint errors & enable on CI
|
||||
[\#2901](https://github.com/matrix-org/matrix-react-sdk/pull/2901)
|
||||
* Add stylelint
|
||||
[\#2900](https://github.com/matrix-org/matrix-react-sdk/pull/2900)
|
||||
* Key backup: Handle case where your onw sig is invalid
|
||||
[\#2899](https://github.com/matrix-org/matrix-react-sdk/pull/2899)
|
||||
* Simplify settings dialog CSS
|
||||
[\#2891](https://github.com/matrix-org/matrix-react-sdk/pull/2891)
|
||||
* Fix upload cancel in e2e rooms
|
||||
[\#2893](https://github.com/matrix-org/matrix-react-sdk/pull/2893)
|
||||
* Set E2E room status to warning when crypto is disabled
|
||||
[\#2890](https://github.com/matrix-org/matrix-react-sdk/pull/2890)
|
||||
* Move SettingsDialog width override to fixedWidth
|
||||
[\#2888](https://github.com/matrix-org/matrix-react-sdk/pull/2888)
|
||||
* Prevent the permalink creator from causing cascading failure
|
||||
[\#2882](https://github.com/matrix-org/matrix-react-sdk/pull/2882)
|
||||
* Don't include all networks by default in the room directory
|
||||
[\#2881](https://github.com/matrix-org/matrix-react-sdk/pull/2881)
|
||||
* Fix fixed width dialogs
|
||||
[\#2886](https://github.com/matrix-org/matrix-react-sdk/pull/2886)
|
||||
* Fix settings dialog layout
|
||||
[\#2885](https://github.com/matrix-org/matrix-react-sdk/pull/2885)
|
||||
* Update from Weblate
|
||||
[\#2884](https://github.com/matrix-org/matrix-react-sdk/pull/2884)
|
||||
* Design tweaks to dialogs
|
||||
[\#2868](https://github.com/matrix-org/matrix-react-sdk/pull/2868)
|
||||
* Remove 'try the app' link from login
|
||||
[\#2880](https://github.com/matrix-org/matrix-react-sdk/pull/2880)
|
||||
* Track store failures after startup
|
||||
[\#2870](https://github.com/matrix-org/matrix-react-sdk/pull/2870)
|
||||
* Translate vertical scrolling to horizontal movement in breadcrumbs
|
||||
[\#2877](https://github.com/matrix-org/matrix-react-sdk/pull/2877)
|
||||
* Add telemetry for breadcrumbs and have the setting apply without refresh
|
||||
[\#2873](https://github.com/matrix-org/matrix-react-sdk/pull/2873)
|
||||
* Fix a few bugs introduced in file upload rework
|
||||
[\#2879](https://github.com/matrix-org/matrix-react-sdk/pull/2879)
|
||||
* Sync breadcrumb rooms through account data
|
||||
[\#2875](https://github.com/matrix-org/matrix-react-sdk/pull/2875)
|
||||
* Scroll breadcrumbs to the left when they change
|
||||
[\#2878](https://github.com/matrix-org/matrix-react-sdk/pull/2878)
|
||||
* Add an indicator to show a room is a direct chat in breadcrumbs
|
||||
[\#2874](https://github.com/matrix-org/matrix-react-sdk/pull/2874)
|
||||
* Use the most recent version of the room in breadcrumbs
|
||||
[\#2872](https://github.com/matrix-org/matrix-react-sdk/pull/2872)
|
||||
* Autohide the scrollbar on breadcrumbs
|
||||
[\#2876](https://github.com/matrix-org/matrix-react-sdk/pull/2876)
|
||||
* Ensure the page URL is redacted before tracking analytics events
|
||||
[\#2871](https://github.com/matrix-org/matrix-react-sdk/pull/2871)
|
||||
* fix NPE for rooms with redacted tombstones
|
||||
[\#2869](https://github.com/matrix-org/matrix-react-sdk/pull/2869)
|
||||
* Don't re-init the stickerpicker unless something actually changes
|
||||
[\#2862](https://github.com/matrix-org/matrix-react-sdk/pull/2862)
|
||||
* Add option to rotate images
|
||||
[\#2855](https://github.com/matrix-org/matrix-react-sdk/pull/2855)
|
||||
* Add badges to breadcrumb rooms
|
||||
[\#2861](https://github.com/matrix-org/matrix-react-sdk/pull/2861)
|
||||
* Include the current power level in the selector
|
||||
[\#2866](https://github.com/matrix-org/matrix-react-sdk/pull/2866)
|
||||
* Apply 50% opacity to left breadcrumbs
|
||||
[\#2860](https://github.com/matrix-org/matrix-react-sdk/pull/2860)
|
||||
* Small scroll fixes
|
||||
[\#2865](https://github.com/matrix-org/matrix-react-sdk/pull/2865)
|
||||
* Put the stickerpicker below dialogs
|
||||
[\#2863](https://github.com/matrix-org/matrix-react-sdk/pull/2863)
|
||||
* Logging tweaks
|
||||
[\#2864](https://github.com/matrix-org/matrix-react-sdk/pull/2864)
|
||||
* Implement redesigned upload confirmation screens
|
||||
[\#2858](https://github.com/matrix-org/matrix-react-sdk/pull/2858)
|
||||
* Use Field component in bug report dialog
|
||||
[\#2859](https://github.com/matrix-org/matrix-react-sdk/pull/2859)
|
||||
* Notify user when crypto data is missing
|
||||
[\#2841](https://github.com/matrix-org/matrix-react-sdk/pull/2841)
|
||||
* Update from Weblate
|
||||
[\#2857](https://github.com/matrix-org/matrix-react-sdk/pull/2857)
|
||||
* Download PDFs as blobs to avoid empty grey screens
|
||||
[\#2847](https://github.com/matrix-org/matrix-react-sdk/pull/2847)
|
||||
* Set title attribute on images in lightbox
|
||||
[\#2848](https://github.com/matrix-org/matrix-react-sdk/pull/2848)
|
||||
* Add MemberInfo for 3pid invites and support revoking those invites
|
||||
[\#2843](https://github.com/matrix-org/matrix-react-sdk/pull/2843)
|
||||
* round scrollTop upwards to prevent never detecting bottom
|
||||
[\#2846](https://github.com/matrix-org/matrix-react-sdk/pull/2846)
|
||||
* Notifier is how singleton is known outside of this module
|
||||
[\#2845](https://github.com/matrix-org/matrix-react-sdk/pull/2845)
|
||||
* Delay `Notifier` check until we have push rules
|
||||
[\#2844](https://github.com/matrix-org/matrix-react-sdk/pull/2844)
|
||||
* BACAT Scrolling
|
||||
[\#2842](https://github.com/matrix-org/matrix-react-sdk/pull/2842)
|
||||
* Handle storage fallback cases in consistency check
|
||||
[\#2840](https://github.com/matrix-org/matrix-react-sdk/pull/2840)
|
||||
* Handle all the segments of a v3 event ID
|
||||
[\#2827](https://github.com/matrix-org/matrix-react-sdk/pull/2827)
|
||||
* Add custom tooltips and scrolling to breadcrumbs
|
||||
[\#2839](https://github.com/matrix-org/matrix-react-sdk/pull/2839)
|
||||
* Check if the message panel is at the end of the timeline on init
|
||||
[\#2829](https://github.com/matrix-org/matrix-react-sdk/pull/2829)
|
||||
* Persist breadcrumb state between sessions
|
||||
[\#2837](https://github.com/matrix-org/matrix-react-sdk/pull/2837)
|
||||
* Always append the current room to the breadcrumbs
|
||||
[\#2838](https://github.com/matrix-org/matrix-react-sdk/pull/2838)
|
||||
* Alert the user to unread notifications in prior versions of rooms
|
||||
[\#2831](https://github.com/matrix-org/matrix-react-sdk/pull/2831)
|
||||
* Filter out upgraded rooms from autocomplete results
|
||||
[\#2830](https://github.com/matrix-org/matrix-react-sdk/pull/2830)
|
||||
|
||||
Changes in [1.0.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.0.7) (2019-04-08)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.0.6...v1.0.7)
|
||||
|
||||
* Hotfix: bump js-sdk to 1.0.4, see https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.4
|
||||
|
||||
Changes in [1.0.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.0.6) (2019-04-01)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.0.6-rc.1...v1.0.6)
|
||||
|
||||
* Handle storage fallback cases in consistency check
|
||||
[\#2853](https://github.com/matrix-org/matrix-react-sdk/pull/2853)
|
||||
* Set title attribute on images in lightbox
|
||||
[\#2852](https://github.com/matrix-org/matrix-react-sdk/pull/2852)
|
||||
* Download PDFs as blobs to avoid empty grey screens
|
||||
[\#2851](https://github.com/matrix-org/matrix-react-sdk/pull/2851)
|
||||
* Add MemberInfo for 3pid invites and support revoking those invites
|
||||
[\#2850](https://github.com/matrix-org/matrix-react-sdk/pull/2850)
|
||||
|
||||
Changes in [1.0.6-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.0.6-rc.1) (2019-03-27)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.0.5...v1.0.6-rc.1)
|
||||
|
||||
* Catch errors when checking IndexedDB
|
||||
[\#2836](https://github.com/matrix-org/matrix-react-sdk/pull/2836)
|
||||
* Remove noreferrer on widget pop-out
|
||||
[\#2835](https://github.com/matrix-org/matrix-react-sdk/pull/2835)
|
||||
* Rework room directory so that new room is always available
|
||||
[\#2834](https://github.com/matrix-org/matrix-react-sdk/pull/2834)
|
||||
* Send telemetry about storage consistency
|
||||
[\#2832](https://github.com/matrix-org/matrix-react-sdk/pull/2832)
|
||||
* Widget OpenID reauth implementation
|
||||
[\#2781](https://github.com/matrix-org/matrix-react-sdk/pull/2781)
|
||||
* Log results of basic storage consistency check
|
||||
[\#2826](https://github.com/matrix-org/matrix-react-sdk/pull/2826)
|
||||
* Clarify devices affected by notification settings
|
||||
[\#2828](https://github.com/matrix-org/matrix-react-sdk/pull/2828)
|
||||
* Add a command for creating custom widgets without an integration manager
|
||||
[\#2824](https://github.com/matrix-org/matrix-react-sdk/pull/2824)
|
||||
* Minimize stickerpicker when the title is clicked
|
||||
[\#2822](https://github.com/matrix-org/matrix-react-sdk/pull/2822)
|
||||
* Add <code> blocks around homeserver and identity server urls
|
||||
[\#2825](https://github.com/matrix-org/matrix-react-sdk/pull/2825)
|
||||
* Fixed drop shadow for tooltip.
|
||||
[\#2815](https://github.com/matrix-org/matrix-react-sdk/pull/2815)
|
||||
* Ask the user for debug logs when the timeline explodes
|
||||
[\#2820](https://github.com/matrix-org/matrix-react-sdk/pull/2820)
|
||||
* Fix typo preventing users from adding more widgets easily
|
||||
[\#2823](https://github.com/matrix-org/matrix-react-sdk/pull/2823)
|
||||
* Attach an onChange listener to the room's blacklist devices option
|
||||
[\#2817](https://github.com/matrix-org/matrix-react-sdk/pull/2817)
|
||||
* Use leaveRoomChain when leaving a room
|
||||
[\#2818](https://github.com/matrix-org/matrix-react-sdk/pull/2818)
|
||||
* Fix bug with NetworkList dropdown
|
||||
[\#2821](https://github.com/matrix-org/matrix-react-sdk/pull/2821)
|
||||
* Trim the logging for URL previews
|
||||
[\#2816](https://github.com/matrix-org/matrix-react-sdk/pull/2816)
|
||||
* Explicitly create `cryptoStore` in React SDK
|
||||
[\#2814](https://github.com/matrix-org/matrix-react-sdk/pull/2814)
|
||||
* Change to new consistent name for `MemoryStore`
|
||||
[\#2812](https://github.com/matrix-org/matrix-react-sdk/pull/2812)
|
||||
* Use medium agents for the more resource intensive builds
|
||||
[\#2813](https://github.com/matrix-org/matrix-react-sdk/pull/2813)
|
||||
* Add log grouping to buildkite
|
||||
[\#2810](https://github.com/matrix-org/matrix-react-sdk/pull/2810)
|
||||
* Switch to `git` protocol for CI dependencies
|
||||
[\#2809](https://github.com/matrix-org/matrix-react-sdk/pull/2809)
|
||||
* Go back to using mainine velocity
|
||||
[\#2808](https://github.com/matrix-org/matrix-react-sdk/pull/2808)
|
||||
* Warn that members won't be autojoined to upgraded rooms
|
||||
[\#2796](https://github.com/matrix-org/matrix-react-sdk/pull/2796)
|
||||
* Support CI for matching branches on forks
|
||||
[\#2807](https://github.com/matrix-org/matrix-react-sdk/pull/2807)
|
||||
* Discard old sticker picker when the URL changes
|
||||
[\#2801](https://github.com/matrix-org/matrix-react-sdk/pull/2801)
|
||||
* Reload widget messaging when widgets reload
|
||||
[\#2799](https://github.com/matrix-org/matrix-react-sdk/pull/2799)
|
||||
* Don't show calculated room name in room settings name input field
|
||||
[\#2806](https://github.com/matrix-org/matrix-react-sdk/pull/2806)
|
||||
* Disable big emoji for m.emote messages as it looks weird
|
||||
[\#2805](https://github.com/matrix-org/matrix-react-sdk/pull/2805)
|
||||
* Remove Edge from browser support statements
|
||||
[\#2803](https://github.com/matrix-org/matrix-react-sdk/pull/2803)
|
||||
* Update from Weblate
|
||||
[\#2802](https://github.com/matrix-org/matrix-react-sdk/pull/2802)
|
||||
* Really fix tag panel
|
||||
[\#2800](https://github.com/matrix-org/matrix-react-sdk/pull/2800)
|
||||
* Update CompatibilityPage to match officially supported browsers
|
||||
[\#2793](https://github.com/matrix-org/matrix-react-sdk/pull/2793)
|
||||
* Use Buildkite for CI
|
||||
[\#2788](https://github.com/matrix-org/matrix-react-sdk/pull/2788)
|
||||
* Fix CSS syntax errors preventing offline member opacity from working
|
||||
[\#2794](https://github.com/matrix-org/matrix-react-sdk/pull/2794)
|
||||
* Make the EntityTile chevron a masked SVG for theming
|
||||
[\#2795](https://github.com/matrix-org/matrix-react-sdk/pull/2795)
|
||||
* Remove refs from `RegistrationForm`
|
||||
[\#2791](https://github.com/matrix-org/matrix-react-sdk/pull/2791)
|
||||
* Fix initial letter avatar vertical offset in Firefox
|
||||
[\#2792](https://github.com/matrix-org/matrix-react-sdk/pull/2792)
|
||||
* Fix the custom tag panel
|
||||
[\#2797](https://github.com/matrix-org/matrix-react-sdk/pull/2797)
|
||||
* Ensure freshly invited members don't count towards the alone warning
|
||||
[\#2786](https://github.com/matrix-org/matrix-react-sdk/pull/2786)
|
||||
* Fix 'forgot password' warning to represent the reality of e2ee
|
||||
[\#2787](https://github.com/matrix-org/matrix-react-sdk/pull/2787)
|
||||
* Restore `Field` value getter for `RegistrationForm`
|
||||
[\#2790](https://github.com/matrix-org/matrix-react-sdk/pull/2790)
|
||||
* Initial portions of support for Field validation
|
||||
[\#2780](https://github.com/matrix-org/matrix-react-sdk/pull/2780)
|
||||
|
||||
Changes in [1.0.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.0.5) (2019-03-21)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.0.4...v1.0.5)
|
||||
|
||||
* Hotfix: disable typing notifs jumping prevention for now
|
||||
[\#2811](https://github.com/matrix-org/matrix-react-sdk/pull/2811)
|
||||
|
||||
Changes in [1.0.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.0.4) (2019-03-18)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.0.4-rc.1...v1.0.4)
|
||||
|
|
|
@ -229,7 +229,7 @@ Controllers are notified of changes by the `SettingsStore`, and are given the op
|
|||
|
||||
### Features
|
||||
|
||||
Features automatically get considered as `disabled` if they are not listed in the `SdkConfig` or `enable_labs` is
|
||||
Features automatically get considered as `disabled` if they are not listed in the `SdkConfig` or `enableLabs` is
|
||||
false/not set. Features are always checked against the configuration before going through the level order as they have
|
||||
the option of being forced-on or forced-off for the application. This is done by the `features` section and looks
|
||||
something like this:
|
||||
|
|
|
@ -29,6 +29,9 @@ yarn lintall -f checkstyle -o eslint.xml || true
|
|||
# re-run the linter, excluding any files known to have errors or warnings.
|
||||
yarn lintwithexclusions
|
||||
|
||||
# lint styles
|
||||
yarn stylelint
|
||||
|
||||
# delete the old tarball, if it exists
|
||||
rm -f matrix-react-sdk-*.tgz
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ module.exports = function (config) {
|
|||
// test results reporter to use
|
||||
// possible values: 'dots', 'progress'
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ['logcapture', 'spec', 'junit', 'summary'],
|
||||
reporters: ['logcapture', 'spec', 'summary'],
|
||||
|
||||
specReporter: {
|
||||
suppressErrorSummary: false, // do print error summary
|
||||
|
@ -156,10 +156,6 @@ module.exports = function (config) {
|
|||
// how many browser should be started simultaneous
|
||||
concurrency: Infinity,
|
||||
|
||||
junitReporter: {
|
||||
outputDir: 'karma-reports',
|
||||
},
|
||||
|
||||
webpack: {
|
||||
module: {
|
||||
rules: [
|
||||
|
|
14
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "1.0.4",
|
||||
"version": "1.1.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -49,6 +49,7 @@
|
|||
"lint": "eslint src/",
|
||||
"lintall": "eslint src/ test/",
|
||||
"lintwithexclusions": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
|
||||
"stylelint": "stylelint res/css/**/*.scss",
|
||||
"clean": "rimraf lib",
|
||||
"prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt",
|
||||
"test": "karma start --single-run=true --browsers VectorChromeHeadless",
|
||||
|
@ -79,9 +80,10 @@
|
|||
"linkifyjs": "^2.1.6",
|
||||
"lodash": "^4.13.1",
|
||||
"lolex": "2.3.2",
|
||||
"matrix-js-sdk": "1.0.2",
|
||||
"matrix-js-sdk": "1.1.0",
|
||||
"optimist": "^0.6.1",
|
||||
"pako": "^1.0.5",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"prop-types": "^15.5.8",
|
||||
"qrcode-react": "^0.1.16",
|
||||
"qs": "^6.6.0",
|
||||
|
@ -99,7 +101,7 @@
|
|||
"slate-react": "^0.18.10",
|
||||
"text-encoding-utf-8": "^1.0.1",
|
||||
"url": "^0.11.0",
|
||||
"velocity-vector": "github:vector-im/velocity#059e3b2",
|
||||
"velocity-animate": "^1.5.2",
|
||||
"whatwg-fetch": "^1.1.1",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
|
@ -110,6 +112,7 @@
|
|||
"babel-loader": "^7.1.5",
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
|
||||
"babel-plugin-transform-builtin-extend": "^1.1.2",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
|
@ -133,14 +136,13 @@
|
|||
"karma": "^4.0.1",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"karma-cli": "^1.0.1",
|
||||
"karma-junit-reporter": "^2.0.0",
|
||||
"karma-logcapture-reporter": "0.0.1",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-spec-reporter": "^0.0.31",
|
||||
"karma-summary-reporter": "^1.5.1",
|
||||
"karma-webpack": "^4.0.0-beta.0",
|
||||
"matrix-mock-request": "^1.2.1",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"matrix-react-test-utils": "^0.1.1",
|
||||
"mocha": "^5.0.5",
|
||||
"react-addons-test-utils": "^15.4.0",
|
||||
|
@ -148,6 +150,8 @@
|
|||
"rimraf": "^2.4.3",
|
||||
"sinon": "^5.0.7",
|
||||
"source-map-loader": "^0.2.3",
|
||||
"stylelint": "^9.10.1",
|
||||
"stylelint-config-standard": "^18.2.0",
|
||||
"walk": "^2.3.9",
|
||||
"webpack": "^4.20.2",
|
||||
"webpack-cli": "^3.1.1"
|
||||
|
|
|
@ -36,6 +36,12 @@ body {
|
|||
color: $warning-color;
|
||||
}
|
||||
|
||||
b {
|
||||
// On Firefox, the default weight for `<b>` is `bolder` which results in no bold
|
||||
// effect since we only have specific weights of our fonts available.
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: $primary-fg-color;
|
||||
font-weight: 400;
|
||||
|
@ -118,7 +124,7 @@ textarea {
|
|||
background-color: transparent;
|
||||
color: $input-darker-fg-color;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #c1c1c1;
|
||||
border: 1px solid $dialog-close-fg-color;
|
||||
// these things should probably not be defined
|
||||
// globally
|
||||
margin: 9px;
|
||||
|
@ -267,14 +273,18 @@ textarea {
|
|||
font-weight: 300;
|
||||
font-size: 15px;
|
||||
position: relative;
|
||||
padding: 40px 58px 36px 58px;
|
||||
width: 60%;
|
||||
max-width: 704px;
|
||||
box-shadow: 2px 15px 30px 0 $dialog-shadow-color;
|
||||
padding: 25px 30px 30px 30px;
|
||||
max-height: 80%;
|
||||
box-shadow: 2px 15px 30px 0 $dialog-shadow-color;
|
||||
border-radius: 4px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mx_Dialog_fixedWidth {
|
||||
width: 60vw;
|
||||
max-width: 704px;
|
||||
}
|
||||
|
||||
.mx_Dialog_staticWrapper .mx_Dialog {
|
||||
z-index: 4010;
|
||||
}
|
||||
|
@ -317,13 +327,13 @@ textarea {
|
|||
|
||||
.mx_Dialog_header {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mx_Dialog_title {
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
line-height: 36px;
|
||||
color: $primary-fg-color;
|
||||
color: $dialog-title-fg-color;
|
||||
}
|
||||
|
||||
.mx_Dialog_header.mx_Dialog_headerWithButton > .mx_Dialog_title {
|
||||
|
@ -338,13 +348,14 @@ textarea {
|
|||
mask: url('$(res)/img/feather-customised/cancel.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background-color: $primary-fg-color;
|
||||
mask-size: cover;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: $dialog-close-fg-color;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
top: 4px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.mx_Dialog_content {
|
||||
|
@ -355,6 +366,7 @@ textarea {
|
|||
}
|
||||
|
||||
.mx_Dialog_buttons {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
@ -370,6 +382,10 @@ textarea {
|
|||
background-color: $button-secondary-bg-color;
|
||||
}
|
||||
|
||||
.mx_Dialog button:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.mx_Dialog button:hover, .mx_Dialog input[type="submit"]:hover {
|
||||
@mixin mx_DialogButton_hover;
|
||||
}
|
||||
|
@ -381,6 +397,7 @@ textarea {
|
|||
.mx_Dialog button.mx_Dialog_primary, .mx_Dialog input[type="submit"].mx_Dialog_primary {
|
||||
color: $accent-fg-color;
|
||||
background-color: $accent-color;
|
||||
min-width: 156px;
|
||||
}
|
||||
|
||||
.mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger {
|
||||
|
@ -506,3 +523,38 @@ textarea {
|
|||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// username colors
|
||||
// used by SenderProfile & RoomPreviewBar
|
||||
.mx_Username_color1 {
|
||||
color: $username-variant1-color;
|
||||
}
|
||||
|
||||
.mx_Username_color2 {
|
||||
color: $username-variant2-color;
|
||||
}
|
||||
|
||||
.mx_Username_color3 {
|
||||
color: $username-variant3-color;
|
||||
}
|
||||
|
||||
.mx_Username_color4 {
|
||||
color: $username-variant4-color;
|
||||
}
|
||||
|
||||
.mx_Username_color5 {
|
||||
color: $username-variant5-color;
|
||||
}
|
||||
|
||||
.mx_Username_color6 {
|
||||
color: $username-variant6-color;
|
||||
}
|
||||
|
||||
.mx_Username_color7 {
|
||||
color: $username-variant7-color;
|
||||
}
|
||||
|
||||
.mx_Username_color8 {
|
||||
color: $username-variant8-color;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
@import "./structures/_CreateRoom.scss";
|
||||
@import "./structures/_CustomRoomTagPanel.scss";
|
||||
@import "./structures/_FilePanel.scss";
|
||||
@import "./structures/_GenericErrorPage.scss";
|
||||
@import "./structures/_GroupView.scss";
|
||||
@import "./structures/_HeaderButtons.scss";
|
||||
@import "./structures/_HomePage.scss";
|
||||
|
@ -19,6 +20,7 @@
|
|||
@import "./structures/_RoomStatusBar.scss";
|
||||
@import "./structures/_RoomSubList.scss";
|
||||
@import "./structures/_RoomView.scss";
|
||||
@import "./structures/_ScrollPanel.scss";
|
||||
@import "./structures/_SearchBox.scss";
|
||||
@import "./structures/_TabbedView.scss";
|
||||
@import "./structures/_TagPanel.scss";
|
||||
|
@ -46,11 +48,11 @@
|
|||
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
||||
@import "./views/context_menus/_TagTileContextMenu.scss";
|
||||
@import "./views/context_menus/_TopLeftMenu.scss";
|
||||
@import "./views/dialogs/_AddressPickerDialog.scss";
|
||||
@import "./views/dialogs/_Analytics.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";
|
||||
|
@ -69,7 +71,9 @@
|
|||
@import "./views/dialogs/_SettingsDialog.scss";
|
||||
@import "./views/dialogs/_ShareDialog.scss";
|
||||
@import "./views/dialogs/_UnknownDeviceDialog.scss";
|
||||
@import "./views/dialogs/_UploadConfirmDialog.scss";
|
||||
@import "./views/dialogs/_UserSettingsDialog.scss";
|
||||
@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
|
||||
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
|
||||
@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss";
|
||||
@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
|
||||
|
@ -96,6 +100,7 @@
|
|||
@import "./views/elements/_ToggleSwitch.scss";
|
||||
@import "./views/elements/_ToolTipButton.scss";
|
||||
@import "./views/elements/_Tooltip.scss";
|
||||
@import "./views/elements/_Validation.scss";
|
||||
@import "./views/globals/_MatrixToolbar.scss";
|
||||
@import "./views/groups/_GroupPublicityToggle.scss";
|
||||
@import "./views/groups/_GroupRoomList.scss";
|
||||
|
@ -108,7 +113,11 @@
|
|||
@import "./views/messages/_MNoticeBody.scss";
|
||||
@import "./views/messages/_MStickerBody.scss";
|
||||
@import "./views/messages/_MTextBody.scss";
|
||||
@import "./views/messages/_MessageActionBar.scss";
|
||||
@import "./views/messages/_MessageTimestamp.scss";
|
||||
@import "./views/messages/_ReactionDimension.scss";
|
||||
@import "./views/messages/_ReactionsRow.scss";
|
||||
@import "./views/messages/_ReactionsRowButton.scss";
|
||||
@import "./views/messages/_RoomAvatarEvent.scss";
|
||||
@import "./views/messages/_SenderProfile.scss";
|
||||
@import "./views/messages/_TextualEvent.scss";
|
||||
|
|
|
@ -14,6 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* This file has CSS for both native and non-native scrollbars in an
|
||||
* order that's fairly logic to read but violates stylelints descending
|
||||
* specificity rule, so turn it off for this file. It also duplicates
|
||||
* a selector to separate the hiding/showing from the sizing.
|
||||
*/
|
||||
/* stylelint-disable no-descending-specificity, no-duplicate-selectors */
|
||||
|
||||
/*
|
||||
1. for browsers that support native overlay auto-hiding scrollbars
|
||||
*/
|
||||
|
@ -59,8 +66,7 @@ body.mx_scrollbar_nooverlay {
|
|||
*/
|
||||
.mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow > .mx_AutoHideScrollbar_offset,
|
||||
.mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow::before,
|
||||
.mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow::after
|
||||
{
|
||||
.mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow::after {
|
||||
margin-right: calc(-1 * var(--scrollbar-width));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,14 +54,14 @@ limitations under the License.
|
|||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
|
||||
.mx_ContextualMenu_chevron_right:after {
|
||||
content:'';
|
||||
.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;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
right: 1px;
|
||||
}
|
||||
|
@ -81,14 +81,14 @@ limitations under the License.
|
|||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
|
||||
.mx_ContextualMenu_chevron_left:after{
|
||||
content:'';
|
||||
.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;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: 1px;
|
||||
}
|
||||
|
@ -108,14 +108,14 @@ limitations under the License.
|
|||
border-right: 8px solid transparent;
|
||||
}
|
||||
|
||||
.mx_ContextualMenu_chevron_top:after{
|
||||
content:'';
|
||||
.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;
|
||||
position: absolute;
|
||||
left: -7px;
|
||||
top: 1px;
|
||||
}
|
||||
|
@ -135,14 +135,14 @@ limitations under the License.
|
|||
border-right: 8px solid transparent;
|
||||
}
|
||||
|
||||
.mx_ContextualMenu_chevron_bottom:after{
|
||||
content:'';
|
||||
.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;
|
||||
position: absolute;
|
||||
left: -7px;
|
||||
bottom: 1px;
|
||||
}
|
||||
|
|
|
@ -101,10 +101,10 @@ limitations under the License.
|
|||
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;
|
||||
}
|
||||
|
||||
.mx_FilePanel .mx_EventTile:hover .mx_EventTile_line {
|
||||
background-color: $primary-bg-color;
|
||||
}
|
||||
|
|
19
res/css/structures/_GenericErrorPage.scss
Normal file
|
@ -0,0 +1,19 @@
|
|||
.mx_GenericErrorPage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.mx_GenericErrorPage_box {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
width: 500px;
|
||||
height: 200px;
|
||||
border: 1px solid #f22;
|
||||
padding: 10px;
|
||||
background-color: #fcc;
|
||||
}
|
|
@ -67,13 +67,13 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_GroupView_editable {
|
||||
border-bottom: 1px solid $strong-input-border-color ! important;
|
||||
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;
|
||||
border-bottom: 1px solid $accent-color !important;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ limitations under the License.
|
|||
|
||||
.mx_GroupView_avatarPicker .mx_Spinner {
|
||||
width: 48px;
|
||||
height: 48px ! important;
|
||||
height: 48px !important;
|
||||
}
|
||||
|
||||
.mx_GroupView_header_leftCol {
|
||||
|
@ -333,7 +333,7 @@ limitations under the License.
|
|||
display: none;
|
||||
}
|
||||
|
||||
.mx_GroupView_body .gm-scroll-view > *{
|
||||
.mx_GroupView_body .gm-scroll-view > * {
|
||||
margin: 11px 50px 0px 68px;
|
||||
}
|
||||
|
||||
|
|
|
@ -67,11 +67,6 @@ limitations under the License.
|
|||
z-index: 6;
|
||||
}
|
||||
|
||||
.mx_LeftPanel_container.collapsed .mx_BottomLeftMenu {
|
||||
flex: 0 0 160px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.mx_LeftPanel .mx_BottomLeftMenu {
|
||||
order: 3;
|
||||
|
||||
|
@ -82,6 +77,11 @@ limitations under the License.
|
|||
z-index: 1;
|
||||
}
|
||||
|
||||
.mx_LeftPanel_container.collapsed .mx_BottomLeftMenu {
|
||||
flex: 0 0 160px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.mx_LeftPanel .mx_BottomLeftMenu_options {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
|
|
@ -19,3 +19,9 @@ limitations under the License.
|
|||
flex-direction: row;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// move hit area 5px to the right so it doesn't overlap with the timeline scrollbar
|
||||
.mx_MainSplit > .mx_ResizeHandle.mx_ResizeHandle_horizontal {
|
||||
margin: 0 -10px 0 0;
|
||||
padding: 0 10px 0 0;
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ limitations under the License.
|
|||
background-color: $roomheader-addroom-bg-color;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
background-color: $roomheader-addroom-fg-color;
|
||||
mask: url('$(res)/img/icons-create-room.svg');
|
||||
mask-repeat: no-repeat;
|
||||
|
@ -113,8 +113,7 @@ limitations under the License.
|
|||
overflow-x: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-flow: wrap;
|
||||
flex-flow: row wrap;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
|
@ -153,6 +152,7 @@ limitations under the License.
|
|||
|
||||
.mx_GroupTile_profile .mx_GroupTile_groupId {
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.mx_GroupTile_profile .mx_GroupTile_desc {
|
||||
|
@ -163,7 +163,3 @@ limitations under the License.
|
|||
max-height: 36px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mx_GroupTile_profile .mx_GroupTile_groupId {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ limitations under the License.
|
|||
|
||||
.mx_NotificationPanel .mx_EventTile_roomName a,
|
||||
.mx_NotificationPanel .mx_EventTile_senderDetails a {
|
||||
text-decoration: none ! important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.mx_NotificationPanel .mx_EventTile .mx_MessageTimestamp {
|
||||
|
@ -83,14 +83,14 @@ limitations under the License.
|
|||
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:hover .mx_EventTile_line {
|
||||
background-color: $primary-bg-color;
|
||||
}
|
||||
|
||||
.mx_NotificationPanel .mx_EventTile_content {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
|
|
@ -40,12 +40,12 @@ limitations under the License.
|
|||
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) {
|
||||
|
@ -138,8 +138,8 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_RoomStatusBar_resend_link {
|
||||
color: $primary-fg-color ! important;
|
||||
text-decoration: underline ! important;
|
||||
color: $primary-fg-color !important;
|
||||
text-decoration: underline !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ limitations under the License.
|
|||
flex: 0 0 16px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
background-color: $roomheader-addroom-fg-color;
|
||||
mask: url('$(res)/img/icons-room-add.svg');
|
||||
mask-repeat: no-repeat;
|
||||
|
@ -137,6 +137,26 @@ limitations under the License.
|
|||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
.mx_RoomSubList_scroll {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mx_RoomSubList_labelContainer {
|
||||
margin-right: 14px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.mx_RoomSubList_addRoom {
|
||||
margin-left: 3px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mx_RoomSubList_label > span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// overflow indicators
|
||||
.mx_RoomSubList:not(.resized-all) > .mx_RoomSubList_scroll {
|
||||
&.mx_IndicatorScrollbar_topOverflow::before,
|
||||
|
@ -164,7 +184,7 @@ limitations under the License.
|
|||
background: linear-gradient(to top, $panel-gradient);
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
// for now, we remove the bottomOverflow entirely as we don't want to
|
||||
// lose the screen real-estate due to a bg-colored gradient, but we also
|
||||
// don't want to use drop shadows and risk a confusing hierarchy of cards.
|
||||
|
@ -175,26 +195,5 @@ limitations under the License.
|
|||
margin: 0px -8px;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.1), rgba(0,0,0,0.0));
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
|
||||
.mx_RoomSubList_scroll {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mx_RoomSubList_labelContainer {
|
||||
margin-right: 14px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.mx_RoomSubList_addRoom {
|
||||
margin-left: 3px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mx_RoomSubList_label > span {
|
||||
display: none;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
|
@ -70,8 +70,15 @@ limitations under the License.
|
|||
background-color: $primary-bg-color;
|
||||
}
|
||||
|
||||
.mx_RoomView_auxPanel_hiddenHighlights {
|
||||
border-bottom: 1px solid $primary-hairline-color;
|
||||
padding: 10px 26px;
|
||||
color: $warning-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_RoomView_auxPanel_apps {
|
||||
max-width: 1920px ! important;
|
||||
max-width: 1920px !important;
|
||||
}
|
||||
|
||||
|
||||
|
@ -79,38 +86,11 @@ limitations under the License.
|
|||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.mx_RoomView_body {
|
||||
position: relative; //for .mx_RoomView_auxPanel_fullHeight
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mx_RoomView_body .mx_RoomView_timeline {
|
||||
/* offset parent for mx_RoomView_topUnreadMessagesBar */
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mx_RoomView_body {
|
||||
.mx_RoomView_messagePanel, .mx_RoomView_messagePanelSpinner, .mx_RoomView_messagePanelSearchSpinner{
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomView_body .mx_RoomView_statusArea {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.mx_RoomView_body .mx_MessageComposer {
|
||||
order: 4;
|
||||
}
|
||||
|
||||
.mx_RoomView_messagePanel {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
flex: 1 1 0;
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
.mx_RoomView_messagePanelSearchSpinner {
|
||||
|
@ -122,7 +102,7 @@ limitations under the License.
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.mx_RoomView_messagePanelSearchSpinner:before {
|
||||
.mx_RoomView_messagePanelSearchSpinner::before {
|
||||
background-color: $greyed-fg-color;
|
||||
mask: url('$(res)/img/feather-customised/search-input.svg');
|
||||
mask-repeat: no-repeat;
|
||||
|
@ -136,6 +116,53 @@ limitations under the License.
|
|||
height: 50px;
|
||||
}
|
||||
|
||||
.mx_RoomView_body {
|
||||
position: relative; //for .mx_RoomView_auxPanel_fullHeight
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.mx_RoomView_messagePanel, .mx_RoomView_messagePanelSpinner, .mx_RoomView_messagePanelSearchSpinner {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomView_body .mx_RoomView_timeline {
|
||||
/* offset parent for mx_RoomView_topUnreadMessagesBar */
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mx_RoomView_statusArea {
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
|
||||
max-height: 0px;
|
||||
background-color: $primary-bg-color;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
|
||||
transition: all .2s ease-out;
|
||||
}
|
||||
|
||||
.mx_RoomView_statusArea_expanded {
|
||||
max-height: 100px;
|
||||
}
|
||||
|
||||
.mx_RoomView_statusAreaBox {
|
||||
margin: auto;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.mx_RoomView_statusAreaBox_line {
|
||||
margin-left: 65px;
|
||||
border-top: 1px solid $primary-hairline-color;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.mx_RoomView_messageListWrapper {
|
||||
min-height: 100%;
|
||||
|
||||
|
@ -196,33 +223,6 @@ hr.mx_RoomView_myReadMarker {
|
|||
z-index: 1;
|
||||
}
|
||||
|
||||
.mx_RoomView_statusArea {
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
|
||||
max-height: 0px;
|
||||
background-color: $primary-bg-color;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
|
||||
transition: all .2s ease-out;
|
||||
}
|
||||
|
||||
.mx_RoomView_statusArea_expanded {
|
||||
max-height: 100px;
|
||||
}
|
||||
|
||||
.mx_RoomView_statusAreaBox {
|
||||
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;
|
||||
}
|
||||
|
@ -283,7 +283,7 @@ hr.mx_RoomView_myReadMarker {
|
|||
}
|
||||
|
||||
.mx_RoomView_ongoingConfCallNotification a {
|
||||
color: $accent-fg-color ! important;
|
||||
color: $accent-fg-color !important;
|
||||
}
|
||||
|
||||
.mx_MatrixChat_useCompactLayout {
|
||||
|
|
26
res/css/structures/_ScrollPanel.scss
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Copyright 2019 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_ScrollPanel {
|
||||
|
||||
.mx_RoomView_MessageList {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
|
@ -50,7 +50,7 @@ limitations under the License.
|
|||
color: $tab-label-active-fg-color;
|
||||
}
|
||||
|
||||
.mx_TabbedView_maskedIcon {;
|
||||
.mx_TabbedView_maskedIcon {
|
||||
margin-left: 6px;
|
||||
margin-right: 9px;
|
||||
margin-top: 1px;
|
||||
|
@ -59,7 +59,7 @@ limitations under the License.
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.mx_TabbedView_maskedIcon:before {
|
||||
.mx_TabbedView_maskedIcon::before {
|
||||
display: inline-block;
|
||||
background-color: $tab-label-icon-bg-color;
|
||||
mask-repeat: no-repeat;
|
||||
|
@ -71,7 +71,7 @@ limitations under the License.
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon:before {
|
||||
.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before {
|
||||
background-color: $tab-label-active-icon-bg-color;
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ limitations under the License.
|
|||
|
||||
flex: none;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
|
||||
|
@ -75,13 +74,13 @@ limitations under the License.
|
|||
|
||||
.mx_TagPanel .mx_TagTile {
|
||||
margin: 9px 0;
|
||||
// opacity: 0.5;
|
||||
// opacity: 0.5;
|
||||
position: relative;
|
||||
}
|
||||
.mx_TagPanel .mx_TagTile:focus,
|
||||
.mx_TagPanel .mx_TagTile:hover,
|
||||
.mx_TagPanel .mx_TagTile.mx_TagTile_selected {
|
||||
// opacity: 1;
|
||||
// opacity: 1;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile.mx_TagTile_selected .mx_TagTile_avatar .mx_BaseAvatar {
|
||||
|
|
|
@ -23,12 +23,12 @@ limitations under the License.
|
|||
padding: 17px 0 3px 0;
|
||||
}
|
||||
|
||||
.mx_TagPanelButtons > .mx_GroupsButton:before {
|
||||
.mx_TagPanelButtons > .mx_GroupsButton::before {
|
||||
mask: url('$(res)/img/feather-customised/users.svg');
|
||||
mask-position: center 11px;
|
||||
}
|
||||
|
||||
.mx_TagPanelButtons > .mx_TagPanelButtons_report:before {
|
||||
.mx_TagPanelButtons > .mx_TagPanelButtons_report::before {
|
||||
mask: url('$(res)/img/feather-customised/life-buoy.svg');
|
||||
mask-position: center 9px;
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ limitations under the License.
|
|||
/* overwrite mx_RoleButton inline-block */
|
||||
display: block !important;
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
background-color: $tagpanel-bg-color;
|
||||
mask-repeat: no-repeat;
|
||||
content: '';
|
||||
|
|
|
@ -130,3 +130,27 @@ limitations under the License.
|
|||
.mx_AuthBody_spinner {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.mx_AuthBody_passwordScore {
|
||||
width: 100%;
|
||||
appearance: none;
|
||||
height: 4px;
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
|
||||
&::-moz-progress-bar {
|
||||
border-radius: 2px;
|
||||
background-color: $accent-color;
|
||||
}
|
||||
|
||||
&::-webkit-progress-bar,
|
||||
&::-webkit-progress-value {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: $accent-color;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ limitations under the License.
|
|||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1535053
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139
|
||||
display: inline-block;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mx_BaseAvatar_initial {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 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,8 +16,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
/* Using a textarea for this element, to circumvent autofill */
|
||||
.mx_ChatInviteDialog_input,
|
||||
.mx_ChatInviteDialog_input:focus
|
||||
.mx_AddressPickerDialog_input,
|
||||
.mx_AddressPickerDialog_input:focus
|
||||
{
|
||||
height: 26px;
|
||||
font-size: 14px;
|
||||
|
@ -34,11 +35,11 @@ limitations under the License.
|
|||
word-wrap: nowrap;
|
||||
}
|
||||
|
||||
.mx_ChatInviteDialog .mx_Dialog_content {
|
||||
.mx_AddressPickerDialog .mx_Dialog_content {
|
||||
min-height: 50px
|
||||
}
|
||||
|
||||
.mx_ChatInviteDialog_inputContainer {
|
||||
.mx_AddressPickerDialog_inputContainer {
|
||||
border-radius: 3px;
|
||||
border: solid 1px $input-border-color;
|
||||
line-height: 36px;
|
||||
|
@ -51,19 +52,19 @@ limitations under the License.
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mx_ChatInviteDialog_error {
|
||||
.mx_AddressPickerDialog_error {
|
||||
margin-top: 10px;
|
||||
color: $warning-color;
|
||||
}
|
||||
|
||||
.mx_ChatInviteDialog_cancel {
|
||||
.mx_AddressPickerDialog_cancel {
|
||||
position: absolute;
|
||||
right: 11px;
|
||||
top: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_ChatInviteDialog_cancel object {
|
||||
.mx_AddressPickerDialog_cancel object {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
@ -14,39 +14,12 @@ 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 .mx_Field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.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;
|
||||
// TODO: We should really apply this to all .mx_Field inputs.
|
||||
// See https://github.com/vector-im/riot-web/issues/9344.
|
||||
flex: 1;
|
||||
}
|
||||
|
|
|
@ -14,15 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_SettingsDialog {
|
||||
.mx_Dialog {
|
||||
// Not actually a component but things shared by settings components
|
||||
.mx_UserSettingsDialog, .mx_RoomSettingsDialog {
|
||||
width: 90vw;
|
||||
max-width: 1000px;
|
||||
width: 90%;
|
||||
height: 80%;
|
||||
border-radius: 4px;
|
||||
padding-top: 0;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
// set the height too since tabbed view scrolls itself.
|
||||
height: 80vh;
|
||||
|
||||
.mx_TabbedView {
|
||||
top: 65px;
|
||||
|
@ -31,7 +28,7 @@ limitations under the License.
|
|||
.mx_TabbedView .mx_SettingsTab {
|
||||
box-sizing: border-box;
|
||||
min-width: 580px;
|
||||
padding-right: 130px;
|
||||
padding-right: 100px;
|
||||
|
||||
// Put some padding on the bottom to avoid the settings tab from
|
||||
// colliding harshly with the dialog when scrolled down.
|
||||
|
@ -40,8 +37,6 @@ limitations under the License.
|
|||
|
||||
.mx_Dialog_title {
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
35
res/css/views/dialogs/_UploadConfirmDialog.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2019 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_UploadConfirmDialog_fileIcon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.mx_UploadConfirmDialog_previewOuter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mx_UploadConfirmDialog_previewInner {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mx_UploadConfirmDialog_imagePreview {
|
||||
max-height: 300px;
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid $dialog-close-fg-color;
|
||||
}
|
28
res/css/views/dialogs/_WidgetOpenIDPermissionsDialog.scss
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
Copyright 2019 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_WidgetOpenIDPermissionsDialog .mx_SettingsFlag {
|
||||
.mx_ToggleSwitch {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.mx_SettingsFlag_label {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
|
@ -37,6 +37,12 @@ limitations under the License.
|
|||
.mx_AccessibleButton_kind_primary {
|
||||
color: $button-primary-fg-color;
|
||||
background-color: $button-primary-bg-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_secondary {
|
||||
color: $accent-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_primary.mx_AccessibleButton_disabled {
|
||||
|
|
|
@ -168,6 +168,7 @@ limitations under the License.
|
|||
.mx_Field_tooltip {
|
||||
margin-top: -12px;
|
||||
margin-left: 4px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.mx_Field_tooltip.mx_Field_valid {
|
||||
|
|
|
@ -80,7 +80,24 @@ limitations under the License.
|
|||
// hack for mx_Dialog having a top padding of 40px
|
||||
top: 40px;
|
||||
right: 0px;
|
||||
padding: 35px;
|
||||
padding-top: 35px;
|
||||
padding-right: 35px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_ImageView_rotateClockwise {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 70px;
|
||||
padding-top: 35px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_ImageView_rotateCounterClockwise {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 105px;
|
||||
padding-top: 35px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,11 +50,10 @@ limitations under the License.
|
|||
|
||||
.mx_Tooltip {
|
||||
display: none;
|
||||
animation: mx_fadein 0.2s;
|
||||
position: fixed;
|
||||
border: 1px solid $menu-border-color;
|
||||
border-radius: 4px;
|
||||
box-shadow: 4px 4px 12px 0 rgba(118, 131, 156, 0.6);
|
||||
box-shadow: 4px 4px 12px 0 $menu-box-shadow-color;
|
||||
background-color: $menu-bg-color;
|
||||
z-index: 2000;
|
||||
padding: 10px;
|
||||
|
@ -66,4 +65,12 @@ limitations under the License.
|
|||
max-width: 200px;
|
||||
word-break: break-word;
|
||||
margin-right: 50px;
|
||||
|
||||
&.mx_Tooltip_visible {
|
||||
animation: mx_fadein 0.2s forwards;
|
||||
}
|
||||
|
||||
&.mx_Tooltip_invisible {
|
||||
animation: mx_fadeout 0.1s forwards;
|
||||
}
|
||||
}
|
||||
|
|
69
res/css/views/elements/_Validation.scss
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
Copyright 2019 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_Validation {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mx_Validation_details {
|
||||
padding-left: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mx_Validation_description + .mx_Validation_details {
|
||||
margin: 1em 0 0;
|
||||
}
|
||||
|
||||
.mx_Validation_detail {
|
||||
position: relative;
|
||||
font-weight: normal;
|
||||
list-style: none;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
top: 0;
|
||||
left: -18px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
}
|
||||
|
||||
&.mx_Validation_valid {
|
||||
color: $input-valid-border-color;
|
||||
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/feather-customised/check.svg');
|
||||
background-color: $input-valid-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_Validation_invalid {
|
||||
color: $input-invalid-border-color;
|
||||
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/feather-customised/x.svg');
|
||||
background-color: $input-invalid-border-color;
|
||||
}
|
||||
}
|
||||
}
|
74
res/css/views/messages/_MessageActionBar.scss
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
Copyright 2019 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_MessageActionBar {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
border-radius: 4px;
|
||||
background: $message-action-bar-bg-color;
|
||||
top: -13px;
|
||||
right: 8px;
|
||||
user-select: none;
|
||||
|
||||
> * {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 27px;
|
||||
border: 1px solid $message-action-bar-border-color;
|
||||
margin-left: -1px;
|
||||
|
||||
&:hover {
|
||||
border-color: $message-action-bar-hover-border-color;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
&:only-child {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_maskButton::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
background-color: $message-action-bar-fg-color;
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_replyButton::after {
|
||||
mask-image: url('$(res)/img/reply.svg');
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_optionsButton::after {
|
||||
mask-image: url('$(res)/img/icon_context.svg');
|
||||
}
|
25
res/css/views/messages/_ReactionDimension.scss
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Copyright 2019 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_ReactionDimension {
|
||||
width: 42px;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.mx_ReactionDimension_disabled {
|
||||
opacity: 0.4;
|
||||
}
|
19
res/css/views/messages/_ReactionsRow.scss
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
Copyright 2019 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_ReactionsRow {
|
||||
margin: 6px 0;
|
||||
}
|
36
res/css/views/messages/_ReactionsRowButton.scss
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
Copyright 2019 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_ReactionsRowButton {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
line-height: 21px;
|
||||
margin-right: 6px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid $reaction-row-button-border-color;
|
||||
border-radius: 10px;
|
||||
background-color: $reaction-row-button-bg-color;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: $reaction-row-button-hover-border-color;
|
||||
}
|
||||
|
||||
&.mx_ReactionsRowButton_selected {
|
||||
background-color: $reaction-row-button-selected-bg-color;
|
||||
border-color: $reaction-row-button-selected-border-color;
|
||||
}
|
||||
}
|
|
@ -18,36 +18,3 @@ limitations under the License.
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mx_SenderProfile_color1 {
|
||||
color: $username-variant1-color;
|
||||
}
|
||||
|
||||
.mx_SenderProfile_color2 {
|
||||
color: $username-variant2-color;
|
||||
}
|
||||
|
||||
.mx_SenderProfile_color3 {
|
||||
color: $username-variant3-color;
|
||||
}
|
||||
|
||||
.mx_SenderProfile_color4 {
|
||||
color: $username-variant4-color;
|
||||
}
|
||||
|
||||
.mx_SenderProfile_color5 {
|
||||
color: $username-variant5-color;
|
||||
}
|
||||
|
||||
.mx_SenderProfile_color6 {
|
||||
color: $username-variant6-color;
|
||||
}
|
||||
|
||||
.mx_SenderProfile_color7 {
|
||||
color: $username-variant7-color;
|
||||
}
|
||||
|
||||
.mx_SenderProfile_color8 {
|
||||
color: $username-variant8-color;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ limitations under the License.
|
|||
top: 14px;
|
||||
left: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar {
|
||||
|
@ -62,6 +63,7 @@ limitations under the License.
|
|||
vertical-align: top;
|
||||
height: 16px;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
|
||||
img {
|
||||
vertical-align: -2px;
|
||||
|
@ -80,6 +82,7 @@ limitations under the License.
|
|||
width: 46px; /* 8 + 30 (avatar) + 8 */
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
|
@ -118,7 +121,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_EventTile:hover .mx_EventTile_line,
|
||||
.mx_EventTile.menu .mx_EventTile_line
|
||||
.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line
|
||||
{
|
||||
background-color: $event-selected-color;
|
||||
}
|
||||
|
@ -203,7 +206,7 @@ limitations under the License.
|
|||
// 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 {
|
||||
.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
@ -216,24 +219,8 @@ limitations under the License.
|
|||
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 {
|
||||
.mx_EventTile:hover .mx_MessageActionBar,
|
||||
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
@ -243,6 +230,7 @@ limitations under the License.
|
|||
width: 14px;
|
||||
height: 14px;
|
||||
top: 29px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mx_EventTile_continuation .mx_EventTile_readAvatars,
|
||||
|
@ -550,10 +538,6 @@ limitations under the License.
|
|||
top: 3px;
|
||||
}
|
||||
|
||||
.mx_EventTile_editButton {
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.mx_EventTile_readAvatars {
|
||||
top: 27px;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ limitations under the License.
|
|||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
|
||||
.mx_Spinner {
|
||||
flex: 1 0 auto;
|
||||
|
@ -35,6 +36,10 @@ limitations under the License.
|
|||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mx_AutoHideScrollbar {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MemberList_chevron {
|
||||
|
|
|
@ -15,33 +15,47 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_RoomBreadcrumbs {
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
height: 32px;
|
||||
margin: 8px;
|
||||
margin-bottom: 0;
|
||||
|
||||
overflow-x: hidden;
|
||||
height: 42px;
|
||||
padding: 8px;
|
||||
padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
> * {
|
||||
margin-left: 4px;
|
||||
// Autohide the scrollbar
|
||||
overflow-x: hidden;
|
||||
&:hover {
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 15px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
.mx_AutoHideScrollbar_offset {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, $panel-gradient);
|
||||
}
|
||||
|
||||
.mx_RoomBreadcrumbs_crumb {
|
||||
margin-left: 4px;
|
||||
height: 32px;
|
||||
display: inline-block;
|
||||
transition: transform 0.3s, width 0.3s;
|
||||
position: relative;
|
||||
|
||||
.mx_RoomTile_badge {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: -4px;
|
||||
}
|
||||
|
||||
.mx_RoomBreadcrumbs_dmIndicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomBreadcrumbs_animate {
|
||||
margin-left: 0;
|
||||
transition: transform 0.3s, width 0.3s;
|
||||
width: 32px;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
@ -50,5 +64,37 @@ limitations under the License.
|
|||
width: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.mx_RoomBreadcrumbs_left {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
// Note: we have to manually control the gradient and stuff, but the IndicatorScrollbar
|
||||
// will deal with left/right positioning for us. Normally we'd use position:sticky on
|
||||
// a few key elements, however that doesn't work in horizontal scrolling scenarios.
|
||||
|
||||
.mx_IndicatorScrollbar_leftOverflowIndicator,
|
||||
.mx_IndicatorScrollbar_rightOverflowIndicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.mx_IndicatorScrollbar_leftOverflow .mx_IndicatorScrollbar_leftOverflowIndicator,
|
||||
&.mx_IndicatorScrollbar_rightOverflow .mx_IndicatorScrollbar_rightOverflowIndicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 15px;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.mx_IndicatorScrollbar_leftOverflowIndicator {
|
||||
background: linear-gradient(to left, $panel-gradient);
|
||||
}
|
||||
|
||||
.mx_IndicatorScrollbar_rightOverflowIndicator {
|
||||
background: linear-gradient(to right, $panel-gradient);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,48 +15,112 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_RoomPreviewBar {
|
||||
text-align: center;
|
||||
height: 176px;
|
||||
background-color: $event-selected-color;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
background-color: $preview-bar-bg-color;
|
||||
-webkit-align-items: center;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
|
||||
&.mx_RoomPreviewBar_spinnerTitle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Spinner {
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 10px 10px 10px 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_wrapper {
|
||||
.mx_RoomPreviewBar_dark {
|
||||
background-color: $tagpanel-bg-color;
|
||||
color: $accent-fg-color;
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_invite_text {
|
||||
color: $primary-fg-color;
|
||||
.mx_RoomPreviewBar_actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_join_text {
|
||||
color: $warning-color;
|
||||
.mx_RoomPreviewBar_message {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
p {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_preview_text {
|
||||
margin-top: 25px;
|
||||
color: $settings-grey-fg-color;
|
||||
.mx_RoomPreviewBar_panel {
|
||||
padding: 8px 8px 8px 20px;
|
||||
border-top: 1px solid $panel-divider-color;
|
||||
|
||||
flex-direction: row;
|
||||
|
||||
.mx_RoomPreviewBar_actions {
|
||||
flex: 0 0 auto;
|
||||
flex-direction: row;
|
||||
padding: 3px 8px;
|
||||
|
||||
&>* {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_message {
|
||||
flex: 1 0 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&>* {
|
||||
margin: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_join_text a {
|
||||
.mx_RoomPreviewBar_dialog {
|
||||
margin: auto;
|
||||
box-sizing: content;
|
||||
width: 400px;
|
||||
border-radius: 4px;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
|
||||
.mx_RoomPreviewBar_message {
|
||||
flex-direction: column;
|
||||
|
||||
&>* {
|
||||
margin: 5px 0 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_actions {
|
||||
flex-direction: column-reverse;
|
||||
.mx_AccessibleButton {
|
||||
padding: 7px 50px;//extra wide
|
||||
}
|
||||
|
||||
&>* {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_inviter {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
a.mx_RoomPreviewBar_inviter {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_warningIcon {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_spinnerIntro {
|
||||
margin-top: 50px;
|
||||
}
|
|
@ -144,11 +144,14 @@ limitations under the License.
|
|||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mx_RoomTile_unreadNotify .mx_RoomTile_badge {
|
||||
.mx_RoomTile_unreadNotify .mx_RoomTile_badge,
|
||||
.mx_RoomTile_badge.mx_RoomTile_badgeUnread {
|
||||
background-color: $roomtile-name-color;
|
||||
}
|
||||
|
||||
.mx_RoomTile_highlight .mx_RoomTile_badge {
|
||||
.mx_RoomTile_highlight .mx_RoomTile_badge,
|
||||
.mx_RoomTile_badge.mx_RoomTile_badgeRed
|
||||
{
|
||||
background-color: $warning-color;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
.mx_RoomUpgradeWarningBar {
|
||||
text-align: center;
|
||||
height: 176px;
|
||||
height: 235px;
|
||||
background-color: $event-selected-color;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_SettingsTab_warningText {
|
||||
color: $warning-color;
|
||||
}
|
||||
|
||||
.mx_SettingsTab_heading {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
|
@ -68,3 +72,7 @@ limitations under the License.
|
|||
// give them more visual distinction between the sections.
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.mx_SettingsTab a {
|
||||
color: $accent-color-alt;
|
||||
}
|
3
res/img/feather-customised/check.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg fill="none" height="24" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m20 6-11 11-5-5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 213 B |
4
res/img/feather-customised/x.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg fill="none" height="24" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m18 6-12 12"/>
|
||||
<path d="m6 6 12 12"/>
|
||||
</svg>
|
After Width: | Height: | Size: 236 B |
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="19px" height="19px" viewBox="0 0 19 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: sketchtool 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>ED5D3E59-2561-4AC1-9B43-82FBC51767FC</title>
|
||||
<desc>Created with sketchtool.</desc>
|
||||
<defs></defs>
|
||||
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="icon_context">
|
||||
<g>
|
||||
<path d="M9.5,19 C14.7467051,19 19,14.7467051 19,9.5 C19,4.25329488 14.7467051,0 9.5,0 C4.25329488,0 0,4.25329488 0,9.5 C0,14.7467051 4.25329488,19 9.5,19 Z" id="Oval-69" fill="#ECECEC"></path>
|
||||
<path d="M4.5,9.50063771 C4.5,9.13148623 4.59887838,8.85242947 4.7966381,8.66345907 C4.99439782,8.47448867 5.28224377,8.38000488 5.66018457,8.38000488 C6.0249414,8.38000488 6.3072941,8.47668596 6.50725115,8.67005103 C6.70720821,8.86341609 6.80718523,9.14027555 6.80718523,9.50063771 C6.80718523,9.84781589 6.70610956,10.1213794 6.50395517,10.3213365 C6.30180079,10.5212935 6.02054674,10.6212705 5.66018457,10.6212705 C5.29103309,10.6212705 5.00538444,10.5234908 4.80323006,10.3279284 C4.60107568,10.132366 4.5,9.85660521 4.5,9.50063771 L4.5,9.50063771 Z M8.3431114,9.50063771 C8.3431114,9.13148623 8.44198978,8.85242947 8.63974951,8.66345907 C8.83750923,8.47448867 9.12755247,8.38000488 9.50988794,8.38000488 C9.87464476,8.38000488 10.1569975,8.47668596 10.3569545,8.67005103 C10.5569116,8.86341609 10.6568886,9.14027555 10.6568886,9.50063771 C10.6568886,9.84781589 10.5558129,10.1213794 10.3536585,10.3213365 C10.1515042,10.5212935 9.8702501,10.6212705 9.50988794,10.6212705 C9.13634179,10.6212705 8.84849585,10.5234908 8.64634146,10.3279284 C8.44418708,10.132366 8.3431114,9.85660521 8.3431114,9.50063771 L8.3431114,9.50063771 Z M12.1928148,9.50063771 C12.1928148,9.13148623 12.2916931,8.85242947 12.4894529,8.66345907 C12.6872126,8.47448867 12.9750585,8.38000488 13.3529993,8.38000488 C13.7177562,8.38000488 14.0001089,8.47668596 14.2000659,8.67005103 C14.400023,8.86341609 14.5,9.14027555 14.5,9.50063771 C14.5,9.84781589 14.3989243,10.1213794 14.1967699,10.3213365 C13.9946156,10.5212935 13.7133615,10.6212705 13.3529993,10.6212705 C12.9838479,10.6212705 12.6981992,10.5234908 12.4960448,10.3279284 C12.2938904,10.132366 12.1928148,9.85660521 12.1928148,9.50063771 L12.1928148,9.50063771 Z" id="…" fill="#9B9B9B"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.5 KiB |
|
@ -1,27 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 25 25" style="enable-background:new 0 0 25 25;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st1{fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_1">
|
||||
<title>81230A28-D944-4572-B5DB-C03CAA2B1FCA</title>
|
||||
<desc>Created with sketchtool.</desc>
|
||||
<g id="Symbols">
|
||||
<g id="Left-nav-default" transform="translate(-50.000000, -725.000000)">
|
||||
<g id="Left-panel">
|
||||
<g>
|
||||
<g id="icons_people" transform="translate(50.000000, 725.000000)">
|
||||
<path id="Oval-1-Copy-7" fill="#76cfa6" d="M12.5,25C19.4,25,25,19.4,25,12.5S19.4,0,12.5,0S0,5.6,0,12.5S5.6,25,12.5,25z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Layer_2">
|
||||
<rect x="7.8" y="10.7" class="st1" stroke="#ffffff" width="9.4" height="7.4"/>
|
||||
<polygon class="st1" stroke="#ffffff" points="12.5,6 6.2,10.7 18.8,10.7 "/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: sketchtool 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>4D42A2A7-7430-4D4F-A0A2-E19278CF66E3</title>
|
||||
<desc>Created with sketchtool.</desc>
|
||||
<defs></defs>
|
||||
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Room-list-Copy-3" transform="translate(-165.000000, -726.000000)">
|
||||
<g id="icons_settings" transform="translate(165.000000, 726.000000)">
|
||||
<path d="M12.5,25 C19.4035594,25 25,19.4035594 25,12.5 C25,5.59644063 19.4035594,0 12.5,0 C5.59644063,0 0,5.59644063 0,12.5 C0,19.4035594 5.59644063,25 12.5,25 Z" id="Oval-1-Copy-7" fill="#76CFA6"></path>
|
||||
<path d="M15.625,12.5 C15.625,11.6373655 15.3198273,10.900882 14.7094727,10.2905273 C14.099118,9.68017273 13.3626345,9.375 12.5,9.375 C11.6373655,9.375 10.900882,9.68017273 10.2905273,10.2905273 C9.68017273,10.900882 9.375,11.6373655 9.375,12.5 C9.375,13.3626345 9.68017273,14.099118 10.2905273,14.7094727 C10.900882,15.3198273 11.6373655,15.625 12.5,15.625 C13.3626345,15.625 14.099118,15.3198273 14.7094727,14.7094727 C15.3198273,14.099118 15.625,13.3626345 15.625,12.5 L15.625,12.5 Z M19.7916667,11.465115 L19.7916667,13.5728624 C19.7916667,13.6488177 19.7663486,13.721607 19.7157118,13.7912326 C19.665075,13.8608583 19.6017799,13.9020001 19.5258247,13.9146593 L17.7693685,14.1805013 C17.649106,14.5222999 17.5256806,14.8102925 17.3990885,15.0444878 C17.6206247,15.360968 17.9592534,15.7977041 18.4149848,16.3547092 C18.4782808,16.4306644 18.5099284,16.5097833 18.5099284,16.5920681 C18.5099284,16.674353 18.4814456,16.7471423 18.4244792,16.8104384 C18.2535799,17.0446337 17.9402692,17.3864272 17.4845378,17.835829 C17.0288063,18.2852308 16.7313194,18.5099284 16.5920681,18.5099284 C16.5161129,18.5099284 16.4338293,18.4814456 16.3452148,18.4244792 L15.0349935,17.3990885 C14.7564909,17.5446694 14.4684983,17.6649301 14.1710069,17.7598741 C14.0697333,18.6207002 13.9779554,19.2093445 13.8956706,19.5258247 C13.8513633,19.7030535 13.7374322,19.7916667 13.5538737,19.7916667 L11.4461263,19.7916667 C11.3575119,19.7916667 11.2799754,19.7647663 11.2135145,19.7109646 C11.1470537,19.657163 11.110659,19.5891208 11.1043294,19.5068359 L10.8384874,17.7598741 C10.5283368,17.6586005 10.243509,17.5415046 9.98399523,17.4085829 L8.6452908,18.4244792 C8.58199476,18.4814456 8.50287591,18.5099284 8.40793186,18.5099284 C8.31931741,18.5099284 8.24019855,18.4751161 8.17057292,18.4054905 C7.37304289,17.6839157 6.85085844,17.152237 6.60400391,16.8104384 C6.55969668,16.7471423 6.5375434,16.674353 6.5375434,16.5920681 C6.5375434,16.5161129 6.56286144,16.4433236 6.61349826,16.3736979 C6.70844231,16.2407762 6.86984478,16.0303201 7.0977105,15.7423231 C7.32557623,15.4543262 7.49647295,15.231211 7.61040582,15.0729709 C7.43950652,14.7564907 7.3097516,14.4431801 7.22113715,14.1330295 L5.4836697,13.8766819 C5.40138486,13.8640227 5.33492502,13.8244632 5.28428819,13.7580024 C5.23365137,13.6915416 5.20833333,13.6171698 5.20833333,13.534885 L5.20833333,11.4271376 C5.20833333,11.3511823 5.23365137,11.278393 5.28428819,11.2087674 C5.33492502,11.1391417 5.39505535,11.0979999 5.46468099,11.0853407 L7.23063151,10.8194987 C7.31924596,10.5283369 7.44267137,10.2371796 7.60091146,9.9460178 C7.34772732,9.5852304 7.00909862,9.14849432 6.58501519,8.63579644 C6.52171916,8.5598412 6.49007161,8.4838871 6.49007161,8.40793186 C6.49007161,8.34463582 6.5185544,8.27184648 6.57552083,8.18956163 C6.74009052,7.96169591 7.05181881,7.62148483 7.51071506,7.16891819 C7.96961131,6.71635154 8.26868058,6.49007161 8.40793186,6.49007161 C8.4902167,6.49007161 8.57250031,6.52171916 8.65478516,6.58501519 L9.96500651,7.60091146 C10.2435091,7.45533058 10.5315017,7.33506992 10.8289931,7.24012587 C10.9302667,6.3792998 11.0220446,5.79065552 11.1043294,5.47417535 C11.1486367,5.29694645 11.2625678,5.20833333 11.4461263,5.20833333 L13.5538737,5.20833333 C13.6424881,5.20833333 13.7200246,5.23523374 13.7864855,5.28903537 C13.8529463,5.342837 13.889341,5.41087922 13.8956706,5.49316406 L14.1615126,7.24012587 C14.4716632,7.34139952 14.756491,7.45849543 15.0160048,7.5914171 L16.3642036,6.57552083 C16.42117,6.5185544 16.4971241,6.49007161 16.5920681,6.49007161 C16.674353,6.49007161 16.7534718,6.52171916 16.8294271,6.58501519 C17.6459459,7.338238 18.1681304,7.87624622 18.3959961,8.19905599 C18.4403033,8.24969282 18.4624566,8.31931741 18.4624566,8.40793186 C18.4624566,8.4838871 18.4371386,8.55667645 18.3865017,8.62630208 C18.2915577,8.75922375 18.1301552,8.96967991 17.9022895,9.25767687 C17.6744238,9.54567382 17.503527,9.76878899 17.3895942,9.92702908 C17.5541639,10.2435093 17.6839188,10.5536552 17.7788628,10.8574761 L19.5163303,11.1233181 C19.5986151,11.1359773 19.665075,11.1755368 19.7157118,11.2419976 C19.7663486,11.3084584 19.7916667,11.3828302 19.7916667,11.465115 L19.7916667,11.465115 Z" id="icons_settings-copy" stroke="#FFFFFF" opacity="0.8"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 5 KiB |
6
res/img/reply.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg">
|
||||
<g stroke="#2E2F32" stroke-width=".75" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8.75 4.75L12.5 8.5l-3.75 3.75"/>
|
||||
<path d="M.5.25V5.5a3 3 0 0 0 3 3h9"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 289 B |
1
res/img/rotate-ccw.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rotate-ccw"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>
|
After Width: | Height: | Size: 311 B |
1
res/img/rotate-cw.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rotate-cw"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
|
After Width: | Height: | Size: 315 B |
|
@ -40,7 +40,7 @@ $tagpanel-bg-color: $base-color;
|
|||
$selected-color: $room-highlight-color;
|
||||
|
||||
// selected for hoverover & selected event tiles
|
||||
$event-selected-color: #111316;
|
||||
$event-selected-color: $header-panel-bg-color;
|
||||
|
||||
// used for the hairline dividers in RoomView
|
||||
$primary-hairline-color: $header-panel-border-color;
|
||||
|
@ -72,7 +72,7 @@ $avatar-bg-color: $bg-color;
|
|||
|
||||
$h3-color: $primary-fg-color;
|
||||
|
||||
$dialog-title-fg-color: #454545;
|
||||
$dialog-title-fg-color: $base-text-color;
|
||||
$dialog-backdrop-color: #000;
|
||||
$dialog-shadow-color: rgba(0, 0, 0, 0.48);
|
||||
$dialog-close-fg-color: #9fa9ba;
|
||||
|
@ -146,6 +146,17 @@ $room-warning-bg-color: $header-panel-bg-color;
|
|||
$dark-panel-bg-color: $header-panel-bg-color;
|
||||
$panel-gradient: rgba(34, 38, 46, 0), rgba(34, 38, 46, 1);
|
||||
|
||||
$message-action-bar-bg-color: $header-panel-bg-color;
|
||||
$message-action-bar-fg-color: $header-panel-text-primary-color;
|
||||
$message-action-bar-border-color: #616b7f;
|
||||
$message-action-bar-hover-border-color: $header-panel-text-primary-color;
|
||||
|
||||
$reaction-row-button-bg-color: $header-panel-bg-color;
|
||||
$reaction-row-button-border-color: #616b7f;
|
||||
$reaction-row-button-hover-border-color: $header-panel-text-primary-color;
|
||||
$reaction-row-button-selected-bg-color: #1f6954;
|
||||
$reaction-row-button-selected-border-color: $accent-color;
|
||||
|
||||
// ***** Mixins! *****
|
||||
|
||||
@define-mixin mx_DialogButton {
|
||||
|
|
|
@ -11,7 +11,7 @@ $font-family: 'Nunito', Arial, Helvetica, Sans-Serif;
|
|||
$accent-color: #03b381;
|
||||
$notice-primary-color: #ff4b55;
|
||||
$notice-secondary-color: #61708b;
|
||||
$header-panel-bg-color: #f2f5f8;
|
||||
$header-panel-bg-color: #f3f8fd;
|
||||
|
||||
// typical text (dark-on-white in light skin)
|
||||
$primary-fg-color: #2e2f32;
|
||||
|
@ -66,14 +66,14 @@ $droptarget-bg-color: rgba(255,255,255,0.5);
|
|||
$selected-color: $secondary-accent-color;
|
||||
|
||||
// selected for hoverover & selected event tiles
|
||||
$event-selected-color: #f7f7f7;
|
||||
$event-selected-color: $header-panel-bg-color;
|
||||
|
||||
// used for the hairline dividers in RoomView
|
||||
$primary-hairline-color: #e5e5e5;
|
||||
|
||||
// used for the border of input text fields
|
||||
$input-border-color: #e7e7e7;
|
||||
$input-darker-bg-color: rgba(193, 201, 214, 0.29);
|
||||
$input-darker-bg-color: #e3e8f0;
|
||||
$input-darker-fg-color: #9fa9ba;
|
||||
$input-lighter-bg-color: #f2f5f8;
|
||||
$input-lighter-fg-color: $input-darker-fg-color;
|
||||
|
@ -106,10 +106,10 @@ $avatar-bg-color: #ffffff;
|
|||
|
||||
$h3-color: #3d3b39;
|
||||
|
||||
$dialog-title-fg-color: #2e2f32;
|
||||
$dialog-title-fg-color: #45474a;
|
||||
$dialog-backdrop-color: rgba(46, 48, 51, 0.38);
|
||||
$dialog-shadow-color: rgba(0, 0, 0, 0.48);
|
||||
$dialog-close-fg-color: #9fa9ba;
|
||||
$dialog-close-fg-color: #c1c1c1;
|
||||
|
||||
$dialog-background-bg-color: #e9e9e9;
|
||||
$lightbox-background-bg-color: #000;
|
||||
|
@ -153,7 +153,7 @@ $roomheader-button-color: #91A1C0;
|
|||
$groupheader-button-color: #91A1C0;
|
||||
$rightpanel-button-color: #91A1C0;
|
||||
$composer-button-color: #91A1C0;
|
||||
$roomtopic-color: #9fa9ba;
|
||||
$roomtopic-color: #9e9e9e;
|
||||
$eventtile-meta-color: $roomtopic-color;
|
||||
|
||||
$composer-e2e-icon-color: #c9ced6;
|
||||
|
@ -203,7 +203,6 @@ $event-redacted-border-color: #cccccc;
|
|||
// event timestamp
|
||||
$event-timestamp-color: #acacac;
|
||||
|
||||
$edit-button-url: "$(res)/img/icon_context_message.svg";
|
||||
$copy-button-url: "$(res)/img/icon_copy_message.svg";
|
||||
|
||||
// e2e
|
||||
|
@ -255,6 +254,17 @@ $authpage-secondary-color: #61708b;
|
|||
$dark-panel-bg-color: $secondary-accent-color;
|
||||
$panel-gradient: rgba(242, 245, 248, 0), rgba(242, 245, 248, 1);
|
||||
|
||||
$message-action-bar-bg-color: $primary-bg-color;
|
||||
$message-action-bar-fg-color: $primary-fg-color;
|
||||
$message-action-bar-border-color: #e9edf1;
|
||||
$message-action-bar-hover-border-color: #b8c1d2;
|
||||
|
||||
$reaction-row-button-bg-color: $header-panel-bg-color;
|
||||
$reaction-row-button-border-color: #e9edf1;
|
||||
$reaction-row-button-hover-border-color: #bebebe;
|
||||
$reaction-row-button-selected-bg-color: #e9fff9;
|
||||
$reaction-row-button-selected-border-color: $accent-color;
|
||||
|
||||
// ***** Mixins! *****
|
||||
|
||||
@define-mixin mx_DialogButton {
|
||||
|
|
9
scripts/ci/Dockerfile
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Update on docker hub with the following commands in the directory of this file:
|
||||
# docker build -t matrixdotorg/riotweb-ci-e2etests-env:latest .
|
||||
# docker log
|
||||
# docker push matrixdotorg/riotweb-ci-e2etests-env:latest
|
||||
FROM node:10
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime
|
||||
# dependencies for chrome (installed by puppeteer)
|
||||
RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
|
|
@ -6,16 +6,38 @@
|
|||
|
||||
set -ev
|
||||
|
||||
upload_logs() {
|
||||
echo "--- Uploading logs"
|
||||
buildkite-agent artifact upload "logs/**/*;synapse/installations/consent/homeserver.log"
|
||||
}
|
||||
|
||||
handle_error() {
|
||||
EXIT_CODE=$?
|
||||
if [ $TESTS_STARTED -eq 1 ]; then
|
||||
upload_logs
|
||||
fi
|
||||
exit $EXIT_CODE
|
||||
}
|
||||
|
||||
trap 'handle_error' ERR
|
||||
|
||||
RIOT_WEB_DIR=riot-web
|
||||
REACT_SDK_DIR=`pwd`
|
||||
|
||||
|
||||
echo "--- Building Riot"
|
||||
scripts/ci/build.sh
|
||||
# run end to end tests
|
||||
echo "--- Fetching end-to-end tests from master"
|
||||
scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests master
|
||||
pushd matrix-react-end-to-end-tests
|
||||
ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web
|
||||
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
|
||||
# CHROME_PATH=$(which google-chrome-stable) ./run.sh
|
||||
echo "--- Install synapse & other dependencies"
|
||||
./install.sh
|
||||
./run.sh --travis
|
||||
mkdir logs
|
||||
echo "+++ Running end-to-end tests"
|
||||
TESTS_STARTED=1
|
||||
./run.sh --no-sandbox --log-directory logs/
|
||||
popd
|
||||
|
|
|
@ -1,28 +1,40 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
org="$1"
|
||||
repo="$2"
|
||||
set -x
|
||||
|
||||
deforg="$1"
|
||||
defrepo="$2"
|
||||
defbranch="$3"
|
||||
|
||||
[ -z "$defbranch" ] && defbranch="develop"
|
||||
|
||||
rm -r "$repo" || true
|
||||
rm -r "$defrepo" || true
|
||||
|
||||
clone() {
|
||||
branch=$1
|
||||
org=$1
|
||||
repo=$2
|
||||
branch=$3
|
||||
if [ -n "$branch" ]
|
||||
then
|
||||
echo "Trying to use the branch $branch"
|
||||
git clone https://github.com/$org/$repo.git $repo --branch "$branch" && exit 0
|
||||
echo "Trying to use $org/$repo#$branch"
|
||||
git clone git://github.com/$org/$repo.git $repo --branch "$branch" && exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# Try the PR author's branch in case it exists on the deps as well.
|
||||
clone $BUILDKITE_BRANCH
|
||||
# If BUILDKITE_BRANCH is set, it will contain either:
|
||||
# * "branch" when the author's branch and target branch are in the same repo
|
||||
# * "author:branch" when the author's branch is in their fork
|
||||
# We can split on `:` into an array to check.
|
||||
BUILDKITE_BRANCH_ARRAY=(${BUILDKITE_BRANCH//:/ })
|
||||
if [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "1" ]]; then
|
||||
clone $deforg $defrepo $BUILDKITE_BRANCH
|
||||
elif [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then
|
||||
clone ${BUILDKITE_BRANCH_ARRAY[0]} $defrepo ${BUILDKITE_BRANCH_ARRAY[1]}
|
||||
fi
|
||||
# Try the target branch of the push or PR.
|
||||
clone $BUILDKITE_PULL_REQUEST_BASE_BRANCH
|
||||
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH
|
||||
# Try the current branch from Jenkins.
|
||||
clone `"echo $GIT_BRANCH" | sed -e 's/^origin\///'`
|
||||
clone $deforg $defrepo `"echo $GIT_BRANCH" | sed -e 's/^origin\///'`
|
||||
# Use the default branch as the last resort.
|
||||
clone $defbranch
|
||||
clone $deforg $defrepo $defbranch
|
||||
|
|
|
@ -84,6 +84,11 @@ const customVariables = {
|
|||
expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'),
|
||||
example: 'off',
|
||||
},
|
||||
'Breadcrumbs': {
|
||||
id: 9,
|
||||
expl: _td("Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)"),
|
||||
example: 'disabled',
|
||||
},
|
||||
'Homeserver URL': {
|
||||
id: 7,
|
||||
expl: _td('Your homeserver\'s URL'),
|
||||
|
@ -201,6 +206,7 @@ class Analytics {
|
|||
|
||||
trackEvent(category, action, name, value) {
|
||||
if (this.disabled) return;
|
||||
this._paq.push(['setCustomUrl', getRedactedUrl()]);
|
||||
this._paq.push(['trackEvent', category, action, name, value]);
|
||||
}
|
||||
|
||||
|
@ -233,6 +239,11 @@ class Analytics {
|
|||
this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off');
|
||||
}
|
||||
|
||||
setBreadcrumbs(state) {
|
||||
if (this.disabled) return;
|
||||
this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled');
|
||||
}
|
||||
|
||||
showDetailsModal() {
|
||||
let rows = [];
|
||||
if (window.Piwik) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 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,21 +18,27 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import Promise from 'bluebird';
|
||||
const extend = require('./extend');
|
||||
const dis = require('./dispatcher');
|
||||
const MatrixClientPeg = require('./MatrixClientPeg');
|
||||
const sdk = require('./index');
|
||||
import extend from './extend';
|
||||
import dis from './dispatcher';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
const Modal = require('./Modal');
|
||||
|
||||
const encrypt = require("browser-encrypt-attachment");
|
||||
import Modal from './Modal';
|
||||
import RoomViewStore from './stores/RoomViewStore';
|
||||
import encrypt from "browser-encrypt-attachment";
|
||||
import extractPngChunks from "png-chunks-extract";
|
||||
|
||||
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
|
||||
require("blueimp-canvas-to-blob");
|
||||
import "blueimp-canvas-to-blob";
|
||||
|
||||
const MAX_WIDTH = 800;
|
||||
const MAX_HEIGHT = 600;
|
||||
|
||||
// scraped out of a macOS hidpi (5660ppm) screenshot png
|
||||
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
||||
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
|
||||
|
||||
export class UploadCanceledError extends Error {}
|
||||
|
||||
/**
|
||||
* Create a thumbnail for a image DOM element.
|
||||
|
@ -91,27 +98,51 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
|||
/**
|
||||
* Load a file into a newly created image element.
|
||||
*
|
||||
* @param {File} file The file to load in an image element.
|
||||
* @param {File} imageFile The file to load in an image element.
|
||||
* @return {Promise} A promise that resolves with the html image element.
|
||||
*/
|
||||
function loadImageElement(imageFile) {
|
||||
const deferred = Promise.defer();
|
||||
|
||||
async function loadImageElement(imageFile) {
|
||||
// Load the file into an html element
|
||||
const img = document.createElement("img");
|
||||
const objectUrl = URL.createObjectURL(imageFile);
|
||||
img.src = objectUrl;
|
||||
|
||||
// Once ready, create a thumbnail
|
||||
const imgPromise = new Promise((resolve, reject) => {
|
||||
img.onload = function() {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
deferred.resolve(img);
|
||||
resolve(img);
|
||||
};
|
||||
img.onerror = function(e) {
|
||||
deferred.reject(e);
|
||||
reject(e);
|
||||
};
|
||||
});
|
||||
img.src = objectUrl;
|
||||
|
||||
return deferred.promise;
|
||||
// check for hi-dpi PNGs and fudge display resolution as needed.
|
||||
// this is mainly needed for macOS screencaps
|
||||
let parsePromise;
|
||||
if (imageFile.type === "image/png") {
|
||||
// in practice macOS happens to order the chunks so they fall in
|
||||
// the first 0x1000 bytes (thanks to a massive ICC header).
|
||||
// Thus we could slice the file down to only sniff the first 0x1000
|
||||
// bytes (but this makes extractPngChunks choke on the corrupt file)
|
||||
const headers = imageFile; //.slice(0, 0x1000);
|
||||
parsePromise = readFileAsArrayBuffer(headers).then(arrayBuffer => {
|
||||
const buffer = new Uint8Array(arrayBuffer);
|
||||
const chunks = extractPngChunks(buffer);
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.name === 'pHYs') {
|
||||
if (chunk.data.byteLength !== PHYS_HIDPI.length) return;
|
||||
const hidpi = chunk.data.every((val, i) => val === PHYS_HIDPI[i]);
|
||||
return hidpi;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
const [hidpi] = await Promise.all([parsePromise, imgPromise]);
|
||||
const width = hidpi ? (img.width >> 1) : img.width;
|
||||
const height = hidpi ? (img.height >> 1) : img.height;
|
||||
return {width, height, img};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -119,7 +150,7 @@ function loadImageElement(imageFile) {
|
|||
*
|
||||
* @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with.
|
||||
* @param {String} roomId The ID of the room the image will be uploaded in.
|
||||
* @param {File} The image to read and thumbnail.
|
||||
* @param {File} imageFile The image to read and thumbnail.
|
||||
* @return {Promise} A promise that resolves with the attachment info.
|
||||
*/
|
||||
function infoForImageFile(matrixClient, roomId, imageFile) {
|
||||
|
@ -129,8 +160,8 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
|
|||
}
|
||||
|
||||
let imageInfo;
|
||||
return loadImageElement(imageFile).then(function(img) {
|
||||
return createThumbnail(img, img.width, img.height, thumbnailType);
|
||||
return loadImageElement(imageFile).then(function(r) {
|
||||
return createThumbnail(r.img, r.width, r.height, thumbnailType);
|
||||
}).then(function(result) {
|
||||
imageInfo = result.info;
|
||||
return uploadFile(matrixClient, roomId, result.thumbnail);
|
||||
|
@ -144,7 +175,7 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
|
|||
/**
|
||||
* Load a file into a newly created video element.
|
||||
*
|
||||
* @param {File} file The file to load in an video element.
|
||||
* @param {File} videoFile The file to load in an video element.
|
||||
* @return {Promise} A promise that resolves with the video image element.
|
||||
*/
|
||||
function loadVideoElement(videoFile) {
|
||||
|
@ -179,7 +210,7 @@ function loadVideoElement(videoFile) {
|
|||
*
|
||||
* @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with.
|
||||
* @param {String} roomId The ID of the room the video will be uploaded to.
|
||||
* @param {File} The video to read and thumbnail.
|
||||
* @param {File} videoFile The video to read and thumbnail.
|
||||
* @return {Promise} A promise that resolves with the attachment info.
|
||||
*/
|
||||
function infoForVideoFile(matrixClient, roomId, videoFile) {
|
||||
|
@ -200,6 +231,7 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
|
|||
|
||||
/**
|
||||
* Read the file as an ArrayBuffer.
|
||||
* @param {File} file The file to read
|
||||
* @return {Promise} A promise that resolves with an ArrayBuffer when the file
|
||||
* is read.
|
||||
*/
|
||||
|
@ -233,17 +265,25 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
|
|||
if (matrixClient.isRoomEncrypted(roomId)) {
|
||||
// If the room is encrypted then encrypt the file before uploading it.
|
||||
// First read the file into memory.
|
||||
return readFileAsArrayBuffer(file).then(function(data) {
|
||||
let canceled = false;
|
||||
let uploadPromise;
|
||||
let encryptInfo;
|
||||
const prom = readFileAsArrayBuffer(file).then(function(data) {
|
||||
if (canceled) throw new UploadCanceledError();
|
||||
// Then encrypt the file.
|
||||
return encrypt.encryptAttachment(data);
|
||||
}).then(function(encryptResult) {
|
||||
if (canceled) throw new UploadCanceledError();
|
||||
// Record the information needed to decrypt the attachment.
|
||||
const encryptInfo = encryptResult.info;
|
||||
encryptInfo = encryptResult.info;
|
||||
// Pass the encrypted data as a Blob to the uploader.
|
||||
const blob = new Blob([encryptResult.data]);
|
||||
return matrixClient.uploadContent(blob, {
|
||||
uploadPromise = matrixClient.uploadContent(blob, {
|
||||
progressHandler: progressHandler,
|
||||
includeFilename: false,
|
||||
});
|
||||
|
||||
return uploadPromise;
|
||||
}).then(function(url) {
|
||||
// If the attachment is encrypted then bundle the URL along
|
||||
// with the information needed to decrypt the attachment and
|
||||
|
@ -254,7 +294,11 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
|
|||
}
|
||||
return {"file": encryptInfo};
|
||||
});
|
||||
});
|
||||
prom.abort = () => {
|
||||
canceled = true;
|
||||
if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise);
|
||||
};
|
||||
return prom;
|
||||
} else {
|
||||
const basePromise = matrixClient.uploadContent(file, {
|
||||
progressHandler: progressHandler,
|
||||
|
@ -269,11 +313,43 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
class ContentMessages {
|
||||
export default class ContentMessages {
|
||||
constructor() {
|
||||
this.inprogress = [];
|
||||
this.nextId = 0;
|
||||
this._mediaConfig = null;
|
||||
}
|
||||
|
||||
static sharedInstance() {
|
||||
if (global.mx_ContentMessages === undefined) {
|
||||
global.mx_ContentMessages = new ContentMessages();
|
||||
}
|
||||
return global.mx_ContentMessages;
|
||||
}
|
||||
|
||||
_isFileSizeAcceptable(file) {
|
||||
if (this._mediaConfig !== null &&
|
||||
this._mediaConfig["m.upload.size"] !== undefined &&
|
||||
file.size > this._mediaConfig["m.upload.size"]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_ensureMediaConfigFetched() {
|
||||
if (this._mediaConfig !== null) return;
|
||||
|
||||
console.log("[Media Config] Fetching");
|
||||
return MatrixClientPeg.get().getMediaConfig().then((config) => {
|
||||
console.log("[Media Config] Fetched config:", config);
|
||||
return config;
|
||||
}).catch(() => {
|
||||
// Media repo can't or won't report limits, so provide an empty object (no limits).
|
||||
console.log("[Media Config] Could not fetch config, so not limiting uploads.");
|
||||
return {};
|
||||
}).then((config) => {
|
||||
this._mediaConfig = config;
|
||||
});
|
||||
}
|
||||
|
||||
sendStickerContentToRoom(url, roomId, info, text, matrixClient) {
|
||||
|
@ -283,7 +359,90 @@ class ContentMessages {
|
|||
});
|
||||
}
|
||||
|
||||
sendContentToRoom(file, roomId, matrixClient) {
|
||||
getUploadLimit() {
|
||||
if (this._mediaConfig !== null && this._mediaConfig["m.upload.size"] !== undefined) {
|
||||
return this._mediaConfig["m.upload.size"];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async sendContentListToRoom(files, roomId, matrixClient) {
|
||||
if (matrixClient.isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
|
||||
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
|
||||
if (isQuoting) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const shouldUpload = await new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, {
|
||||
title: _t('Replying With Files'),
|
||||
description: (
|
||||
<div>{_t(
|
||||
'At this time it is not possible to reply with a file. ' +
|
||||
'Would you like to upload this file without replying?',
|
||||
)}</div>
|
||||
),
|
||||
hasCancelButton: true,
|
||||
button: _t("Continue"),
|
||||
onFinished: (shouldUpload) => {
|
||||
resolve(shouldUpload);
|
||||
},
|
||||
});
|
||||
});
|
||||
if (!shouldUpload) return;
|
||||
}
|
||||
|
||||
await this._ensureMediaConfigFetched();
|
||||
|
||||
const tooBigFiles = [];
|
||||
const okFiles = [];
|
||||
|
||||
for (let i = 0; i < files.length; ++i) {
|
||||
if (this._isFileSizeAcceptable(files[i])) {
|
||||
okFiles.push(files[i]);
|
||||
} else {
|
||||
tooBigFiles.push(files[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (tooBigFiles.length > 0) {
|
||||
const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog");
|
||||
const uploadFailureDialogPromise = new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, {
|
||||
badFiles: tooBigFiles,
|
||||
totalFiles: files.length,
|
||||
contentMessages: this,
|
||||
onFinished: (shouldContinue) => {
|
||||
resolve(shouldContinue);
|
||||
},
|
||||
});
|
||||
});
|
||||
const shouldContinue = await uploadFailureDialogPromise;
|
||||
if (!shouldContinue) return;
|
||||
}
|
||||
|
||||
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
|
||||
for (let i = 0; i < okFiles.length; ++i) {
|
||||
const file = okFiles[i];
|
||||
const shouldContinue = await new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
|
||||
file,
|
||||
currentIndex: i,
|
||||
totalFiles: okFiles.length,
|
||||
onFinished: (shouldContinue) => {
|
||||
resolve(shouldContinue);
|
||||
},
|
||||
});
|
||||
});
|
||||
if (!shouldContinue) break;
|
||||
this._sendContentToRoom(file, roomId, matrixClient);
|
||||
}
|
||||
}
|
||||
|
||||
_sendContentToRoom(file, roomId, matrixClient) {
|
||||
const content = {
|
||||
body: file.name || 'Attachment',
|
||||
info: {
|
||||
|
@ -333,6 +492,9 @@ class ContentMessages {
|
|||
this.inprogress.push(upload);
|
||||
dis.dispatch({action: 'upload_started'});
|
||||
|
||||
// Focus the composer view
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
|
||||
let error;
|
||||
|
||||
function onProgress(ev) {
|
||||
|
@ -357,9 +519,12 @@ class ContentMessages {
|
|||
}, function(err) {
|
||||
error = err;
|
||||
if (!upload.canceled) {
|
||||
let desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.';
|
||||
let desc = _t("The file '%(fileName)s' failed to upload.", {fileName: upload.fileName});
|
||||
if (err.http_status == 413) {
|
||||
desc = _t('The file \'%(fileName)s\' exceeds this homeserver\'s size limit for uploads', {fileName: upload.fileName});
|
||||
desc = _t(
|
||||
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
|
||||
{fileName: upload.fileName},
|
||||
);
|
||||
}
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
||||
|
@ -377,15 +542,22 @@ class ContentMessages {
|
|||
}
|
||||
}
|
||||
if (error) {
|
||||
// 413: File was too big or upset the server in some way:
|
||||
// clear the media size limit so we fetch it again next time
|
||||
// we try to upload
|
||||
if (error && error.http_status === 413) {
|
||||
this._mediaConfig = null;
|
||||
}
|
||||
dis.dispatch({action: 'upload_failed', upload, error});
|
||||
} else {
|
||||
dis.dispatch({action: 'upload_finished', upload});
|
||||
dis.dispatch({action: 'message_sent'});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentUploads() {
|
||||
return this.inprogress;
|
||||
return this.inprogress.filter(u => !u.canceled);
|
||||
}
|
||||
|
||||
cancelUpload(promise) {
|
||||
|
@ -401,12 +573,7 @@ class ContentMessages {
|
|||
if (upload) {
|
||||
upload.canceled = true;
|
||||
MatrixClientPeg.get().cancelUpload(upload.promise);
|
||||
dis.dispatch({action: 'upload_canceled', upload});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (global.mx_ContentMessage === undefined) {
|
||||
global.mx_ContentMessage = new ContentMessages();
|
||||
}
|
||||
|
||||
module.exports = global.mx_ContentMessage;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Travis Ralston
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,17 +21,19 @@ import IntegrationManager from './IntegrationManager';
|
|||
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
|
||||
import ActiveWidgetStore from './stores/ActiveWidgetStore';
|
||||
|
||||
const WIDGET_API_VERSION = '0.0.1'; // Current API version
|
||||
const WIDGET_API_VERSION = '0.0.2'; // Current API version
|
||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
||||
'0.0.1',
|
||||
'0.0.2',
|
||||
];
|
||||
const INBOUND_API_NAME = 'fromWidget';
|
||||
|
||||
// Listen for and handle incomming requests using the 'fromWidget' postMessage
|
||||
// Listen for and handle incoming requests using the 'fromWidget' postMessage
|
||||
// API and initiate responses
|
||||
export default class FromWidgetPostMessageApi {
|
||||
constructor() {
|
||||
this.widgetMessagingEndpoints = [];
|
||||
this.widgetListeners = {}; // {action: func[]}
|
||||
|
||||
this.start = this.start.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
|
@ -45,6 +48,32 @@ export default class FromWidgetPostMessageApi {
|
|||
window.removeEventListener('message', this.onPostMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for a given action
|
||||
* @param {string} action The action to listen for.
|
||||
* @param {Function} callbackFn A callback function to be called when the action is
|
||||
* encountered. Called with two parameters: the interesting request information and
|
||||
* the raw event received from the postMessage API. The raw event is meant to be used
|
||||
* for sendResponse and similar functions.
|
||||
*/
|
||||
addListener(action, callbackFn) {
|
||||
if (!this.widgetListeners[action]) this.widgetListeners[action] = [];
|
||||
this.widgetListeners[action].push(callbackFn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener for a given action.
|
||||
* @param {string} action The action that was subscribed to.
|
||||
* @param {Function} callbackFn The original callback function that was used to subscribe
|
||||
* to updates.
|
||||
*/
|
||||
removeListener(action, callbackFn) {
|
||||
if (!this.widgetListeners[action]) return;
|
||||
|
||||
const idx = this.widgetListeners[action].indexOf(callbackFn);
|
||||
if (idx !== -1) this.widgetListeners[action].splice(idx, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a widget endpoint for trusted postMessage communication
|
||||
* @param {string} widgetId Unique widget identifier
|
||||
|
@ -87,10 +116,8 @@ export default class FromWidgetPostMessageApi {
|
|||
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);
|
||||
});
|
||||
this.widgetMessagingEndpoints = this.widgetMessagingEndpoints
|
||||
.filter((endpoint) => endpoint.widgetId !== widgetId || endpoint.endpointUrl !== origin);
|
||||
return (length > this.widgetMessagingEndpoints.length);
|
||||
}
|
||||
return false;
|
||||
|
@ -117,6 +144,13 @@ export default class FromWidgetPostMessageApi {
|
|||
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
||||
}
|
||||
|
||||
// Call any listeners we have registered
|
||||
if (this.widgetListeners[event.data.action]) {
|
||||
for (const fn of this.widgetListeners[event.data.action]) {
|
||||
fn(event.data, event);
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
@ -164,6 +198,8 @@ export default class FromWidgetPostMessageApi {
|
|||
if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) {
|
||||
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
|
||||
}
|
||||
} else if (action === 'get_openid') {
|
||||
// Handled by caller
|
||||
} else {
|
||||
console.warn('Widget postMessage event unhandled');
|
||||
this.sendError(event, {message: 'The postMessage was unhandled'});
|
||||
|
|
|
@ -516,7 +516,11 @@ export function bodyToHtml(content, highlights, opts={}) {
|
|||
contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, '');
|
||||
|
||||
const match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
||||
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length
|
||||
// Prevent user pills expanding for users with only emoji in
|
||||
// their username
|
||||
&& (content.formatted_body == undefined
|
||||
|| !content.formatted_body.includes("https://matrix.to/"));
|
||||
}
|
||||
|
||||
const className = classNames({
|
||||
|
|
|
@ -31,7 +31,8 @@ import Modal from './Modal';
|
|||
import sdk from './index';
|
||||
import ActiveWidgetStore from './stores/ActiveWidgetStore';
|
||||
import PlatformPeg from "./PlatformPeg";
|
||||
import {sendLoginRequest} from "./Login";
|
||||
import { sendLoginRequest } from "./Login";
|
||||
import * as StorageManager from './utils/StorageManager';
|
||||
|
||||
/**
|
||||
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
||||
|
@ -102,9 +103,14 @@ export async function loadSession(opts) {
|
|||
return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName);
|
||||
}
|
||||
|
||||
// fall back to login screen
|
||||
// fall back to welcome screen
|
||||
return false;
|
||||
} catch (e) {
|
||||
if (e instanceof AbortLoginAndRebuildStorage) {
|
||||
// If we're aborting login because of a storage inconsistency, we don't
|
||||
// need to show the general failure dialog. Instead, just go back to welcome.
|
||||
return false;
|
||||
}
|
||||
return _handleLoadSessionFailure(e);
|
||||
}
|
||||
}
|
||||
|
@ -197,9 +203,6 @@ export function handleInvalidStoreError(e) {
|
|||
function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||
console.log(`Doing guest login on ${hsUrl}`);
|
||||
|
||||
// TODO: we should probably de-duplicate this and Login.loginAsGuest.
|
||||
// Not really sure where the right home for it is.
|
||||
|
||||
// create a temporary MatrixClient to do the login
|
||||
const client = Matrix.createClient({
|
||||
baseUrl: hsUrl,
|
||||
|
@ -278,7 +281,7 @@ async function _restoreFromLocalStorage() {
|
|||
}
|
||||
|
||||
function _handleLoadSessionFailure(e) {
|
||||
console.log("Unable to load session", e);
|
||||
console.error("Unable to load session", e);
|
||||
|
||||
const def = Promise.defer();
|
||||
const SessionRestoreErrorDialog =
|
||||
|
@ -353,6 +356,22 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
|||
await _clearStorage();
|
||||
}
|
||||
|
||||
const results = await StorageManager.checkConsistency();
|
||||
// If there's an inconsistency between account data in local storage and the
|
||||
// crypto store, we'll be generally confused when handling encrypted data.
|
||||
// Show a modal recommending a full reset of storage.
|
||||
if (results.dataInLocalStorage && !results.dataInCryptoStore) {
|
||||
const signOut = await _showStorageEvictedDialog();
|
||||
if (signOut) {
|
||||
await _clearStorage();
|
||||
// This error feels a bit clunky, but we want to make sure we don't go any
|
||||
// further and instead head back to sign in.
|
||||
throw new AbortLoginAndRebuildStorage(
|
||||
"Aborting login in progress because of storage inconsistency",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl, credentials.identityServerUrl);
|
||||
|
||||
if (localStorage) {
|
||||
|
@ -383,6 +402,19 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
|||
return MatrixClientPeg.get();
|
||||
}
|
||||
|
||||
function _showStorageEvictedDialog() {
|
||||
const StorageEvictedDialog = sdk.getComponent('views.dialogs.StorageEvictedDialog');
|
||||
return new Promise(resolve => {
|
||||
Modal.createTrackedDialog('Storage evicted', '', StorageEvictedDialog, {
|
||||
onFinished: resolve,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Note: Babel 6 requires the `transform-builtin-extend` plugin for this to satisfy
|
||||
// `instanceof`. Babel 7 supports this natively in their class handling.
|
||||
class AbortLoginAndRebuildStorage extends Error { }
|
||||
|
||||
function _persistCredentialsToLocalStorage(credentials) {
|
||||
localStorage.setItem("mx_hs_url", credentials.homeserverUrl);
|
||||
localStorage.setItem("mx_is_url", credentials.identityServerUrl);
|
||||
|
|
20
src/Login.js
|
@ -81,26 +81,6 @@ export default class Login {
|
|||
return flowStep ? flowStep.type : null;
|
||||
}
|
||||
|
||||
loginAsGuest() {
|
||||
const client = this._createTemporaryClient();
|
||||
return client.registerGuest({
|
||||
body: {
|
||||
initial_device_display_name: this._defaultDeviceDisplayName,
|
||||
},
|
||||
}).then((creds) => {
|
||||
return {
|
||||
userId: creds.user_id,
|
||||
deviceId: creds.device_id,
|
||||
accessToken: creds.access_token,
|
||||
homeserverUrl: this._hsUrl,
|
||||
identityServerUrl: this._isUrl,
|
||||
guest: true,
|
||||
};
|
||||
}, (error) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
loginViaPassword(username, phoneCountry, phoneNumber, pass) {
|
||||
const self = this;
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import {phasedRollOutExpiredForUser} from "./PhasedRollOut";
|
|||
import Modal from './Modal';
|
||||
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
|
||||
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
|
||||
import * as StorageManager from './utils/StorageManager';
|
||||
|
||||
interface MatrixClientCreds {
|
||||
homeserverUrl: string,
|
||||
|
@ -103,7 +104,7 @@ class MatrixClientPeg {
|
|||
} catch (err) {
|
||||
if (dbType === 'indexeddb') {
|
||||
console.error('Error starting matrixclient store - falling back to memory store', err);
|
||||
this.matrixClient.store = new Matrix.MatrixInMemoryStore({
|
||||
this.matrixClient.store = new Matrix.MemoryStore({
|
||||
localStorage: global.localStorage,
|
||||
});
|
||||
} else {
|
||||
|
@ -113,6 +114,8 @@ class MatrixClientPeg {
|
|||
}
|
||||
}
|
||||
|
||||
StorageManager.trackStores(this.matrixClient);
|
||||
|
||||
// try to initialise e2e on the new client
|
||||
try {
|
||||
// check that we have a version of the js-sdk which includes initCrypto
|
||||
|
@ -120,7 +123,7 @@ class MatrixClientPeg {
|
|||
await this.matrixClient.initCrypto();
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name === 'InvalidCryptoStoreError') {
|
||||
if (e && e.name === 'InvalidCryptoStoreError') {
|
||||
// The js-sdk found a crypto DB too new for it to use
|
||||
const CryptoStoreTooNewDialog =
|
||||
sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog");
|
||||
|
@ -130,7 +133,7 @@ class MatrixClientPeg {
|
|||
}
|
||||
// this can happen for a number of reasons, the most likely being
|
||||
// that the olm library was missing. It's not fatal.
|
||||
console.warn("Unable to initialise e2e: " + e);
|
||||
console.warn("Unable to initialise e2e", e);
|
||||
}
|
||||
|
||||
const opts = utils.deepCopy(this.opts);
|
||||
|
@ -171,7 +174,7 @@ class MatrixClientPeg {
|
|||
return matches[1];
|
||||
}
|
||||
|
||||
_createClient(creds: MatrixClientCreds, useIndexedDb) {
|
||||
_createClient(creds: MatrixClientCreds) {
|
||||
const opts = {
|
||||
baseUrl: creds.homeserverUrl,
|
||||
idBaseUrl: creds.identityServerUrl,
|
||||
|
@ -183,7 +186,7 @@ class MatrixClientPeg {
|
|||
verificationMethods: [verificationMethods.SAS]
|
||||
};
|
||||
|
||||
this.matrixClient = createMatrixClient(opts, useIndexedDb);
|
||||
this.matrixClient = createMatrixClient(opts);
|
||||
|
||||
// we're going to add eventlisteners for each matrix event tile, so the
|
||||
// potential number of event listeners is quite high.
|
||||
|
|
34
src/Modal.js
|
@ -75,10 +75,9 @@ const AsyncWrapper = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const {loader, ...otherProps} = this.props;
|
||||
if (this.state.component) {
|
||||
const Component = this.state.component;
|
||||
return <Component {...otherProps} />;
|
||||
return <Component {...this.props} />;
|
||||
} else if (this.state.error) {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
@ -124,6 +123,10 @@ class ModalManager {
|
|||
this.closeAll = this.closeAll.bind(this);
|
||||
}
|
||||
|
||||
hasDialogs() {
|
||||
return this._priorityModal || this._staticModal || this._modals.length > 0;
|
||||
}
|
||||
|
||||
getOrCreateContainer() {
|
||||
let container = document.getElementById(DIALOG_CONTAINER_ID);
|
||||
|
||||
|
@ -154,7 +157,7 @@ class ModalManager {
|
|||
}
|
||||
|
||||
createDialog(Element, ...rest) {
|
||||
return this.createDialogAsync(new Promise(resolve => resolve(Element)), ...rest);
|
||||
return this.createDialogAsync(Promise.resolve(Element), ...rest);
|
||||
}
|
||||
|
||||
createTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) {
|
||||
|
@ -189,36 +192,35 @@ class ModalManager {
|
|||
* also be removed from the stack. This is not compatible
|
||||
* with being a priority modal. Only one modal can be
|
||||
* static at a time.
|
||||
* @returns {object} Object with 'close' parameter being a function that will close the dialog
|
||||
*/
|
||||
createDialogAsync(prom, props, className, isPriorityModal, isStaticModal) {
|
||||
const self = this;
|
||||
const modal = {};
|
||||
|
||||
// never call this from onFinished() otherwise it will loop
|
||||
//
|
||||
// nb explicit function() rather than arrow function, to get `arguments`
|
||||
const closeDialog = function() {
|
||||
if (props && props.onFinished) props.onFinished.apply(null, arguments);
|
||||
const i = self._modals.indexOf(modal);
|
||||
const closeDialog = (...args) => {
|
||||
if (props && props.onFinished) props.onFinished.apply(null, args);
|
||||
const i = this._modals.indexOf(modal);
|
||||
if (i >= 0) {
|
||||
self._modals.splice(i, 1);
|
||||
this._modals.splice(i, 1);
|
||||
}
|
||||
|
||||
if (self._priorityModal === modal) {
|
||||
self._priorityModal = null;
|
||||
if (this._priorityModal === modal) {
|
||||
this._priorityModal = null;
|
||||
|
||||
// XXX: This is destructive
|
||||
self._modals = [];
|
||||
this._modals = [];
|
||||
}
|
||||
|
||||
if (self._staticModal === modal) {
|
||||
self._staticModal = null;
|
||||
if (this._staticModal === modal) {
|
||||
this._staticModal = null;
|
||||
|
||||
// XXX: This is destructive
|
||||
self._modals = [];
|
||||
this._modals = [];
|
||||
}
|
||||
|
||||
self._reRender();
|
||||
this._reRender();
|
||||
};
|
||||
|
||||
// don't attempt to reuse the same AsyncWrapper for different dialogs,
|
||||
|
|
|
@ -220,7 +220,17 @@ const Notifier = {
|
|||
}
|
||||
},
|
||||
|
||||
isToolbarHidden: function() {
|
||||
shouldShowToolbar: function() {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
return false;
|
||||
}
|
||||
const isGuest = client.isGuest();
|
||||
return !isGuest && this.supportsDesktopNotifications() &&
|
||||
!this.isEnabled() && !this._isToolbarHidden();
|
||||
},
|
||||
|
||||
_isToolbarHidden: function() {
|
||||
// Check localStorage for any such meta data
|
||||
if (global.localStorage) {
|
||||
return global.localStorage.getItem("notifications_hidden") === "true";
|
||||
|
|
|
@ -65,6 +65,24 @@ export function showRoomInviteDialog(roomId) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given MatrixEvent is a valid 3rd party user invite.
|
||||
* @param {MatrixEvent} event The event to check
|
||||
* @returns {boolean} True if valid, false otherwise
|
||||
*/
|
||||
export function isValid3pidInvite(event) {
|
||||
if (!event || event.getType() !== "m.room.third_party_invite") return false;
|
||||
|
||||
// any events without these keys are not valid 3pid invites, so we ignore them
|
||||
const requiredKeys = ['key_validity_url', 'public_key', 'display_name'];
|
||||
for (let i = 0; i < requiredKeys.length; ++i) {
|
||||
if (!event.getContent()[requiredKeys[i]]) return false;
|
||||
}
|
||||
|
||||
// Valid enough by our standards
|
||||
return true;
|
||||
}
|
||||
|
||||
function _onStartChatFinished(shouldInvite, addrs) {
|
||||
if (!shouldInvite) return;
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ export const ALL_MESSAGES = 'all_messages';
|
|||
export const MENTIONS_ONLY = 'mentions_only';
|
||||
export const MUTE = 'mute';
|
||||
|
||||
export const BADGE_STATES = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
|
||||
export const MENTION_BADGE_STATES = [...BADGE_STATES, MENTIONS_ONLY];
|
||||
|
||||
function _shouldShowNotifBadge(roomNotifState) {
|
||||
const showBadgeInStates = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
|
||||
|
@ -107,6 +109,28 @@ export function setRoomNotifsState(roomId, newState) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getUnreadNotificationCount(room, type=null) {
|
||||
let notificationCount = room.getUnreadNotificationCount(type);
|
||||
|
||||
// Check notification counts in the old room just in case there's some lost
|
||||
// there. We only go one level down to avoid performance issues, and theory
|
||||
// is that 1st generation rooms will have already been read by the 3rd generation.
|
||||
const createEvent = room.currentState.getStateEvents("m.room.create", "");
|
||||
if (createEvent && createEvent.getContent()['predecessor']) {
|
||||
const oldRoomId = createEvent.getContent()['predecessor']['room_id'];
|
||||
const oldRoom = MatrixClientPeg.get().getRoom(oldRoomId);
|
||||
if (oldRoom) {
|
||||
// We only ever care if there's highlights in the old room. No point in
|
||||
// notifying the user for unread messages because they would have extreme
|
||||
// difficulty changing their notification preferences away from "All Messages"
|
||||
// and "Noisy".
|
||||
notificationCount += oldRoom.getUnreadNotificationCount("highlight");
|
||||
}
|
||||
}
|
||||
|
||||
return notificationCount;
|
||||
}
|
||||
|
||||
function setRoomNotifsStateMuted(roomId) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const promises = [];
|
||||
|
@ -204,4 +228,3 @@ function isRuleForRoom(roomId, rule) {
|
|||
function isMuteRule(rule) {
|
||||
return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify');
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,12 @@ class SdkConfig {
|
|||
static unset() {
|
||||
global.mxReactSdkConfig = undefined;
|
||||
}
|
||||
|
||||
static add(cfg) {
|
||||
const liveConfig = SdkConfig.get();
|
||||
const newConfig = Object.assign({}, liveConfig, cfg);
|
||||
SdkConfig.put(newConfig);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SdkConfig;
|
||||
|
|
|
@ -28,6 +28,8 @@ import {MATRIXTO_URL_PATTERN} from "./linkify-matrix";
|
|||
import * as querystring from "querystring";
|
||||
import MultiInviter from './utils/MultiInviter';
|
||||
import { linkifyAndSanitizeHtml } from './HtmlUtils';
|
||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||
import WidgetUtils from "./utils/WidgetUtils";
|
||||
|
||||
class Command {
|
||||
constructor({name, args='', description, runFn, hideCompletionAfterSpace=false}) {
|
||||
|
@ -105,7 +107,72 @@ export const CommandMap = {
|
|||
description: _td('Upgrades a room to a new version'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
return success(MatrixClientPeg.get().upgradeRoom(roomId, args));
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation',
|
||||
QuestionDialog, {
|
||||
title: _t('Room upgrade confirmation'),
|
||||
description: (
|
||||
<div>
|
||||
<p>{_t("Upgrading a room can be destructive and isn't always necessary.")}</p>
|
||||
<p>
|
||||
{_t(
|
||||
"Room upgrades are usually recommended when a room version is considered " +
|
||||
"<i>unstable</i>. Unstable room versions might have bugs, missing features, or " +
|
||||
"security vulnerabilities.",
|
||||
{}, {
|
||||
"i": (sub) => <i>{sub}</i>,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
"Room upgrades usually only affect <i>server-side</i> processing of the " +
|
||||
"room. If you're having problems with your Riot client, please file an issue " +
|
||||
"with <issueLink />.",
|
||||
{}, {
|
||||
"i": (sub) => <i>{sub}</i>,
|
||||
"issueLink": () => {
|
||||
return <a href="https://github.com/vector-im/riot-web/issues/new/choose"
|
||||
target="_blank" rel="noopener">
|
||||
https://github.com/vector-im/riot-web/issues/new/choose
|
||||
</a>;
|
||||
},
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room " +
|
||||
"members to the new version of the room.</i> We'll post a link to the new room " +
|
||||
"in the old version of the room - room members will have to click this link to " +
|
||||
"join the new room.",
|
||||
{}, {
|
||||
"b": (sub) => <b>{sub}</b>,
|
||||
"i": (sub) => <i>{sub}</i>,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
"Please confirm that you'd like to go forward with upgrading this room " +
|
||||
"from <oldVersion /> to <newVersion />.",
|
||||
{},
|
||||
{
|
||||
oldVersion: () => <code>{room ? room.getVersion() : "1"}</code>,
|
||||
newVersion: () => <code>{args}</code>,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
button: _t("Upgrade"),
|
||||
onFinished: (confirm) => {
|
||||
if (!confirm) return;
|
||||
|
||||
MatrixClientPeg.get().upgradeRoom(roomId, args);
|
||||
},
|
||||
});
|
||||
return success();
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
|
@ -365,7 +432,7 @@ export const CommandMap = {
|
|||
|
||||
if (!targetRoomId) targetRoomId = roomId;
|
||||
return success(
|
||||
cli.leave(targetRoomId).then(function() {
|
||||
cli.leaveRoomChain(targetRoomId).then(function() {
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
}),
|
||||
);
|
||||
|
@ -540,6 +607,26 @@ export const CommandMap = {
|
|||
},
|
||||
}),
|
||||
|
||||
addwidget: new Command({
|
||||
name: 'addwidget',
|
||||
args: '<url>',
|
||||
description: _td('Adds a custom widget by URL to the room'),
|
||||
runFn: function(roomId, args) {
|
||||
if (!args || (!args.startsWith("https://") && !args.startsWith("http://"))) {
|
||||
return reject(_t("Please supply a https:// or http:// widget URL"));
|
||||
}
|
||||
if (WidgetUtils.canUserModifyWidgets(roomId)) {
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const nowMs = (new Date()).getTime();
|
||||
const widgetId = encodeURIComponent(`${roomId}_${userId}_${nowMs}`);
|
||||
return success(WidgetUtils.setRoomWidget(
|
||||
roomId, widgetId, "m.custom", args, "Custom Widget", {}));
|
||||
} else {
|
||||
return reject(_t("You cannot modify widgets in this room."));
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
// Verify a user, device, and pubkey tuple
|
||||
verify: new Command({
|
||||
name: 'verify',
|
||||
|
|
|
@ -17,6 +17,7 @@ import MatrixClientPeg from './MatrixClientPeg';
|
|||
import CallHandler from './CallHandler';
|
||||
import { _t } from './languageHandler';
|
||||
import * as Roles from './Roles';
|
||||
import {isValid3pidInvite} from "./RoomInvite";
|
||||
|
||||
function textForMemberEvent(ev) {
|
||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||
|
@ -366,6 +367,15 @@ function textForCallInviteEvent(event) {
|
|||
|
||||
function textForThreePidInviteEvent(event) {
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
|
||||
if (!isValid3pidInvite(event)) {
|
||||
const targetDisplayName = event.getPrevContent().display_name || _t("Someone");
|
||||
return _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
|
||||
senderName,
|
||||
targetDisplayName,
|
||||
});
|
||||
}
|
||||
|
||||
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
|
||||
senderName,
|
||||
targetDisplayName: event.getContent().display_name,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const React = require('react');
|
||||
const ReactDom = require('react-dom');
|
||||
import PropTypes from 'prop-types';
|
||||
const Velocity = require('velocity-vector');
|
||||
const Velocity = require('velocity-animate');
|
||||
|
||||
/**
|
||||
* The Velociraptor contains components and animates transitions with velocity.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const Velocity = require('velocity-vector');
|
||||
const Velocity = require('velocity-animate');
|
||||
|
||||
// courtesy of https://github.com/julianshapiro/velocity/issues/283
|
||||
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 Travis Ralston
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -21,6 +22,11 @@ limitations under the License.
|
|||
|
||||
import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
|
||||
import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
|
||||
import Modal from "./Modal";
|
||||
import MatrixClientPeg from "./MatrixClientPeg";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
||||
import WidgetUtils from "./utils/WidgetUtils";
|
||||
|
||||
if (!global.mxFromWidgetMessaging) {
|
||||
global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
|
||||
|
@ -34,12 +40,14 @@ if (!global.mxToWidgetMessaging) {
|
|||
const OUTBOUND_API_NAME = 'toWidget';
|
||||
|
||||
export default class WidgetMessaging {
|
||||
constructor(widgetId, widgetUrl, target) {
|
||||
constructor(widgetId, widgetUrl, isUserWidget, target) {
|
||||
this.widgetId = widgetId;
|
||||
this.widgetUrl = widgetUrl;
|
||||
this.isUserWidget = isUserWidget;
|
||||
this.target = target;
|
||||
this.fromWidget = global.mxFromWidgetMessaging;
|
||||
this.toWidget = global.mxToWidgetMessaging;
|
||||
this._onOpenIdRequest = this._onOpenIdRequest.bind(this);
|
||||
this.start();
|
||||
}
|
||||
|
||||
|
@ -109,9 +117,57 @@ export default class WidgetMessaging {
|
|||
|
||||
start() {
|
||||
this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl);
|
||||
this.fromWidget.addListener("get_openid", this._onOpenIdRequest);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl);
|
||||
this.fromWidget.removeListener("get_openid", this._onOpenIdRequest);
|
||||
}
|
||||
|
||||
async _onOpenIdRequest(ev, rawEv) {
|
||||
if (ev.widgetId !== this.widgetId) return; // not interesting
|
||||
|
||||
const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.widgetUrl, this.isUserWidget);
|
||||
|
||||
const settings = SettingsStore.getValue("widgetOpenIDPermissions");
|
||||
if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
|
||||
this.fromWidget.sendResponse(rawEv, {state: "blocked"});
|
||||
return;
|
||||
}
|
||||
if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
|
||||
const responseBody = {state: "allowed"};
|
||||
const credentials = await MatrixClientPeg.get().getOpenIdToken();
|
||||
Object.assign(responseBody, credentials);
|
||||
this.fromWidget.sendResponse(rawEv, responseBody);
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm that we received the request
|
||||
this.fromWidget.sendResponse(rawEv, {state: "request"});
|
||||
|
||||
// Actually ask for permission to send the user's data
|
||||
Modal.createTrackedDialog("OpenID widget permissions", '',
|
||||
WidgetOpenIDPermissionsDialog, {
|
||||
widgetUrl: this.widgetUrl,
|
||||
widgetId: this.widgetId,
|
||||
isUserWidget: this.isUserWidget,
|
||||
|
||||
onFinished: async (confirm) => {
|
||||
const responseBody = {success: confirm};
|
||||
if (confirm) {
|
||||
const credentials = await MatrixClientPeg.get().getOpenIdToken();
|
||||
Object.assign(responseBody, credentials);
|
||||
}
|
||||
this.messageToWidget({
|
||||
api: OUTBOUND_API_NAME,
|
||||
action: "openid_credentials",
|
||||
data: responseBody,
|
||||
}).catch((error) => {
|
||||
console.error("Failed to send OpenID credentials: ", error);
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
|||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||
if (command) {
|
||||
// the only reason we need to do this is because Fuse only matches on properties
|
||||
this.matcher.setObjects(client.getRooms().filter(
|
||||
let matcherObjects = client.getRooms().filter(
|
||||
(room) => !!room && !!getDisplayAliasForRoom(room),
|
||||
).map((room) => {
|
||||
return {
|
||||
|
@ -64,7 +64,21 @@ export default class RoomProvider extends AutocompleteProvider {
|
|||
name: room.name,
|
||||
displayedAlias: getDisplayAliasForRoom(room),
|
||||
};
|
||||
}));
|
||||
});
|
||||
|
||||
// Filter out any matches where the user will have also autocompleted new rooms
|
||||
matcherObjects = matcherObjects.filter((r) => {
|
||||
const tombstone = r.room.currentState.getStateEvents("m.room.tombstone", "");
|
||||
if (tombstone && tombstone.getContent() && tombstone.getContent()['replacement_room']) {
|
||||
const hasReplacementRoom = matcherObjects.some(
|
||||
(r2) => r2.room.roomId === tombstone.getContent()['replacement_room'],
|
||||
);
|
||||
return !hasReplacementRoom;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
this.matcher.setObjects(matcherObjects);
|
||||
const matchedString = command[0];
|
||||
completions = this.matcher.match(matchedString);
|
||||
completions = _sortBy(completions, [
|
||||
|
|
|
@ -121,8 +121,10 @@ export default class AutoHideScrollbar extends React.Component {
|
|||
render() {
|
||||
return (<div
|
||||
ref={this._collectContainerRef}
|
||||
style={this.props.style}
|
||||
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
|
||||
onScroll={this.props.onScroll}
|
||||
onWheel={this.props.onWheel}
|
||||
>
|
||||
<div className="mx_AutoHideScrollbar_offset">
|
||||
{ this.props.children }
|
||||
|
|
|
@ -1,197 +0,0 @@
|
|||
/*
|
||||
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) {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
return <Tooltip className="mx_BottomLeftMenu_tooltip" label={label} />;
|
||||
}
|
||||
},
|
||||
|
||||
_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.enableTagPanel") ?
|
||||
<GroupsButton tooltip={true} /> : null;
|
||||
|
||||
return (
|
||||
<div className="mx_BottomLeftMenu">
|
||||
<div className="mx_BottomLeftMenu_options">
|
||||
<HomeButton tooltip={true} />
|
||||
<div ref={this._collectPeopleButton}>
|
||||
<StartChatButton tooltip={true} />
|
||||
</div>
|
||||
<div ref={this._collectDirectoryButton}>
|
||||
<RoomDirectoryButton tooltip={true} />
|
||||
</div>
|
||||
<div ref={this._collectCreateRoomButton}>
|
||||
<CreateRoomButton tooltip={true} />
|
||||
</div>
|
||||
{ groupsButton }
|
||||
<span className="mx_BottomLeftMenu_settings">
|
||||
<SettingsButton tooltip={true} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -56,6 +56,7 @@ export default class ContextualMenu extends React.Component {
|
|||
menuPaddingRight: PropTypes.number,
|
||||
menuPaddingBottom: PropTypes.number,
|
||||
menuPaddingLeft: PropTypes.number,
|
||||
zIndex: PropTypes.number,
|
||||
|
||||
// If true, insert an invisible screen-sized element behind the
|
||||
// menu that when clicked will close it.
|
||||
|
@ -215,16 +216,22 @@ export default class ContextualMenu extends React.Component {
|
|||
menuStyle["paddingRight"] = props.menuPaddingRight;
|
||||
}
|
||||
|
||||
const wrapperStyle = {};
|
||||
if (!isNaN(Number(props.zIndex))) {
|
||||
menuStyle["zIndex"] = props.zIndex + 1;
|
||||
wrapperStyle["zIndex"] = props.zIndex;
|
||||
}
|
||||
|
||||
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!
|
||||
return <div className={className} style={position}>
|
||||
return <div className={className} style={{...position, ...wrapperStyle}}>
|
||||
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect}>
|
||||
{ chevron }
|
||||
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
|
||||
</div>
|
||||
{ props.hasBackground && <div className="mx_ContextualMenu_background"
|
||||
{ props.hasBackground && <div className="mx_ContextualMenu_background" style={wrapperStyle}
|
||||
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
|
||||
<style>{ chevronCSS }</style>
|
||||
</div>;
|
||||
|
|
|
@ -123,6 +123,7 @@ const FilePanel = React.createClass({
|
|||
timelineSet={this.state.timelineSet}
|
||||
showUrlPreview = {false}
|
||||
tileShape="file_grid"
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
empty={_t('There are no visible files in this room')}
|
||||
/>
|
||||
);
|
||||
|
|
38
src/components/structures/GenericErrorPage.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
Copyright 2019 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";
|
||||
|
||||
export default class GenericErrorPage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
message: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
return <div className='mx_GenericErrorPage'>
|
||||
<div className='mx_GenericErrorPage_box'>
|
||||
<h1>{_t("Error loading Riot")}</h1>
|
||||
<p>{this.props.message}</p>
|
||||
<p>{_t(
|
||||
"If this is unexpected, please contact your system administrator " +
|
||||
"or technical support representative.",
|
||||
)}</p>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -15,9 +15,22 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
export default class IndicatorScrollbar extends React.Component {
|
||||
static PropTypes = {
|
||||
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
|
||||
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
|
||||
// by the parent element.
|
||||
trackHorizontalOverflow: PropTypes.bool,
|
||||
|
||||
// If true, when the user tries to use their mouse wheel in the component it will
|
||||
// scroll horizontally rather than vertically. This should only be used on components
|
||||
// with no vertical scroll opportunity.
|
||||
verticalScrollsHorizontally: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._collectScroller = this._collectScroller.bind(this);
|
||||
|
@ -25,6 +38,18 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
this.checkOverflow = this.checkOverflow.bind(this);
|
||||
this._scrollElement = null;
|
||||
this._autoHideScrollbar = null;
|
||||
|
||||
this.state = {
|
||||
leftIndicatorOffset: 0,
|
||||
rightIndicatorOffset: 0,
|
||||
};
|
||||
}
|
||||
|
||||
moveToOrigin() {
|
||||
if (!this._scrollElement) return;
|
||||
|
||||
this._scrollElement.scrollLeft = 0;
|
||||
this._scrollElement.scrollTop = 0;
|
||||
}
|
||||
|
||||
_collectScroller(scroller) {
|
||||
|
@ -43,6 +68,10 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
const hasTopOverflow = this._scrollElement.scrollTop > 0;
|
||||
const hasBottomOverflow = this._scrollElement.scrollHeight >
|
||||
(this._scrollElement.scrollTop + this._scrollElement.clientHeight);
|
||||
const hasLeftOverflow = this._scrollElement.scrollLeft > 0;
|
||||
const hasRightOverflow = this._scrollElement.scrollWidth >
|
||||
(this._scrollElement.scrollLeft + this._scrollElement.clientWidth);
|
||||
|
||||
if (hasTopOverflow) {
|
||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
|
||||
} else {
|
||||
|
@ -53,10 +82,30 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
} else {
|
||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
|
||||
}
|
||||
if (hasLeftOverflow) {
|
||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_leftOverflow");
|
||||
} else {
|
||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_leftOverflow");
|
||||
}
|
||||
if (hasRightOverflow) {
|
||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_rightOverflow");
|
||||
} else {
|
||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
|
||||
}
|
||||
|
||||
if (this._autoHideScrollbar) {
|
||||
this._autoHideScrollbar.checkOverflow();
|
||||
}
|
||||
|
||||
if (this.props.trackHorizontalOverflow) {
|
||||
this.setState({
|
||||
// Offset from absolute position of the container
|
||||
leftIndicatorOffset: hasLeftOverflow ? `${this._scrollElement.scrollLeft}px` : '0',
|
||||
|
||||
// Negative because we're coming from the right
|
||||
rightIndicatorOffset: hasRightOverflow ? `-${this._scrollElement.scrollLeft}px` : '0',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getScrollTop() {
|
||||
|
@ -69,13 +118,41 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onMouseWheel = (e) => {
|
||||
if (this.props.verticalScrollsHorizontally && this._scrollElement) {
|
||||
// xyThreshold is the amount of horizontal motion required for the component to
|
||||
// ignore the vertical delta in a scroll. Used to stop trackpads from acting in
|
||||
// strange ways. Should be positive.
|
||||
const xyThreshold = 0;
|
||||
|
||||
// yRetention is the factor multiplied by the vertical delta to try and reduce
|
||||
// the harshness of the scroll behaviour. Should be a value between 0 and 1.
|
||||
const yRetention = 1.0;
|
||||
|
||||
if (Math.abs(e.deltaX) < xyThreshold) {
|
||||
// noinspection JSSuspiciousNameCombination
|
||||
this._scrollElement.scrollLeft += e.deltaY * yRetention;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const leftIndicatorStyle = {left: this.state.leftIndicatorOffset};
|
||||
const rightIndicatorStyle = {right: this.state.rightIndicatorOffset};
|
||||
const leftOverflowIndicator = this.props.trackHorizontalOverflow
|
||||
? <div className="mx_IndicatorScrollbar_leftOverflowIndicator" style={leftIndicatorStyle} /> : null;
|
||||
const rightOverflowIndicator = this.props.trackHorizontalOverflow
|
||||
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
||||
|
||||
return (<AutoHideScrollbar
|
||||
ref={this._collectScrollerComponent}
|
||||
wrappedRef={this._collectScroller}
|
||||
onWheel={this.onMouseWheel}
|
||||
{... this.props}
|
||||
>
|
||||
{ leftOverflowIndicator }
|
||||
{ this.props.children }
|
||||
{ rightOverflowIndicator }
|
||||
</AutoHideScrollbar>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import VectorConferenceHandler from '../../VectorConferenceHandler';
|
|||
import TagPanelButtons from './TagPanelButtons';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import {_t} from "../../languageHandler";
|
||||
import Analytics from "../../Analytics";
|
||||
|
||||
|
||||
const LeftPanel = React.createClass({
|
||||
|
@ -45,11 +46,23 @@ const LeftPanel = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
searchFilter: '',
|
||||
breadcrumbs: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.focusedElement = null;
|
||||
|
||||
this._settingWatchRef = SettingsStore.watchSetting(
|
||||
"feature_room_breadcrumbs", null, this._onBreadcrumbsChanged);
|
||||
|
||||
const useBreadcrumbs = SettingsStore.isFeatureEnabled("feature_room_breadcrumbs");
|
||||
Analytics.setBreadcrumbs(useBreadcrumbs);
|
||||
this.setState({breadcrumbs: useBreadcrumbs});
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
SettingsStore.unwatchSetting(this._settingWatchRef);
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
|
@ -73,6 +86,22 @@ const LeftPanel = React.createClass({
|
|||
return false;
|
||||
},
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.breadcrumbs !== this.state.breadcrumbs) {
|
||||
Analytics.setBreadcrumbs(this.state.breadcrumbs);
|
||||
}
|
||||
},
|
||||
|
||||
_onBreadcrumbsChanged: function(settingName, roomId, level, valueAtLevel, value) {
|
||||
// Features are only possible at a single level, so we can get away with using valueAtLevel.
|
||||
// The SettingsStore runs on the same tick as the update, so `value` will be wrong.
|
||||
this.setState({breadcrumbs: valueAtLevel});
|
||||
|
||||
// For some reason the setState doesn't trigger a render of the component, so force one.
|
||||
// Probably has to do with the change happening outside of a change detector cycle.
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
||||
_onFocus: function(ev) {
|
||||
this.focusedElement = ev.target;
|
||||
},
|
||||
|
@ -220,7 +249,7 @@ const LeftPanel = React.createClass({
|
|||
collapsed={this.props.collapsed} />);
|
||||
|
||||
let breadcrumbs;
|
||||
if (SettingsStore.isFeatureEnabled("feature_room_breadcrumbs")) {
|
||||
if (this.state.breadcrumbs) {
|
||||
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
|
||||
}
|
||||
|
||||
|
@ -234,14 +263,13 @@ const LeftPanel = React.createClass({
|
|||
<CallPreview ConferenceHandler={VectorConferenceHandler} />
|
||||
<RoomList
|
||||
ref={this.collectRoomList}
|
||||
toolbarShown={this.props.toolbarShown}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ConferenceHandler={VectorConferenceHandler} />
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
// <BottomLeftMenu collapsed={this.props.collapsed}/>
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ import PropTypes from 'prop-types';
|
|||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
import Notifier from '../../Notifier';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
import sdk from '../../index';
|
||||
|
@ -121,6 +120,18 @@ const LoggedInView = React.createClass({
|
|||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// attempt to guess when a banner was opened or closed
|
||||
if (
|
||||
(prevProps.showCookieBar !== this.props.showCookieBar) ||
|
||||
(prevProps.hasNewVersion !== this.props.hasNewVersion) ||
|
||||
(prevProps.userHasGeneratedPassword !== this.props.userHasGeneratedPassword) ||
|
||||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
|
||||
) {
|
||||
this.props.resizeNotifier.notifyBannersChanged();
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
|
@ -173,6 +184,7 @@ const LoggedInView = React.createClass({
|
|||
},
|
||||
onResized: (size) => {
|
||||
window.localStorage.setItem("mx_lhs_size", '' + size);
|
||||
this.props.resizeNotifier.notifyLeftHandleResized();
|
||||
},
|
||||
};
|
||||
const resizer = new Resizer(
|
||||
|
@ -448,6 +460,7 @@ const LoggedInView = React.createClass({
|
|||
disabled={this.props.middleDisabled}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
ConferenceHandler={this.props.ConferenceHandler}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>;
|
||||
break;
|
||||
|
||||
|
@ -489,7 +502,6 @@ const LoggedInView = React.createClass({
|
|||
});
|
||||
|
||||
let topBar;
|
||||
const isGuest = this.props.matrixClient.isGuest();
|
||||
if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
topBar = <ServerLimitBar kind='hard'
|
||||
adminContact={this.state.syncErrorData.error.data.admin_contact}
|
||||
|
@ -513,10 +525,7 @@ const LoggedInView = React.createClass({
|
|||
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
|
||||
} else if (this.state.userHasGeneratedPassword) {
|
||||
topBar = <PasswordNagBar />;
|
||||
} else if (
|
||||
!isGuest && Notifier.supportsDesktopNotifications() &&
|
||||
!Notifier.isEnabled() && !Notifier.isToolbarHidden()
|
||||
) {
|
||||
} else if (this.props.showNotifierToolbar) {
|
||||
topBar = <MatrixToolbar />;
|
||||
}
|
||||
|
||||
|
@ -534,7 +543,7 @@ const LoggedInView = React.createClass({
|
|||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div ref={this._setResizeContainerRef} className={bodyClasses}>
|
||||
<LeftPanel
|
||||
toolbarShown={!!topBar}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
|
|
|
@ -27,6 +27,9 @@ export default class MainSplit extends React.Component {
|
|||
|
||||
_onResized(size) {
|
||||
window.localStorage.setItem("mx_rhs_size", size);
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.notifyRightHandleResized();
|
||||
}
|
||||
}
|
||||
|
||||
_createResizer() {
|
||||
|
|
|
@ -29,6 +29,7 @@ import PlatformPeg from "../../PlatformPeg";
|
|||
import SdkConfig from "../../SdkConfig";
|
||||
import * as RoomListSorter from "../../RoomListSorter";
|
||||
import dis from "../../dispatcher";
|
||||
import Notifier from '../../Notifier';
|
||||
|
||||
import Modal from "../../Modal";
|
||||
import Tinter from "../../Tinter";
|
||||
|
@ -48,6 +49,7 @@ import { _t, getCurrentLanguage } from '../../languageHandler';
|
|||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
||||
import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
|
||||
const AutoDiscovery = Matrix.AutoDiscovery;
|
||||
|
||||
|
@ -194,6 +196,8 @@ export default React.createClass({
|
|||
hideToSRUsers: false,
|
||||
|
||||
syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
|
||||
resizeNotifier: new ResizeNotifier(),
|
||||
showNotifierToolbar: false,
|
||||
};
|
||||
return s;
|
||||
},
|
||||
|
@ -245,17 +249,6 @@ export default React.createClass({
|
|||
return this.state.defaultIsUrl || "https://vector.im";
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether to skip the server details phase of registration and start at the
|
||||
* actual form.
|
||||
* @return {boolean}
|
||||
* If there was a configured default HS or default server name, skip the
|
||||
* the server details.
|
||||
*/
|
||||
skipServerDetailsForRegistration() {
|
||||
return !!this.state.defaultHsUrl;
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
SdkConfig.put(this.props.config);
|
||||
|
||||
|
@ -316,6 +309,9 @@ export default React.createClass({
|
|||
// N.B. we don't call the whole of setTheme() here as we may be
|
||||
// racing with the theme CSS download finishing from index.js
|
||||
Tinter.tint();
|
||||
|
||||
// For PersistentElement
|
||||
this.state.resizeNotifier.on("middlePanelResized", this._dispatchTimelineResize);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -398,6 +394,7 @@ export default React.createClass({
|
|||
dis.unregister(this.dispatcherRef);
|
||||
window.removeEventListener("focus", this.onFocus);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize);
|
||||
},
|
||||
|
||||
componentWillUpdate: function(props, state) {
|
||||
|
@ -556,19 +553,8 @@ export default React.createClass({
|
|||
},
|
||||
});
|
||||
break;
|
||||
case 'view_user':
|
||||
// FIXME: ugly hack to expand the RightPanel and then re-dispatch.
|
||||
if (this.state.collapsedRhs) {
|
||||
setTimeout(()=>{
|
||||
dis.dispatch({
|
||||
action: 'show_right_panel',
|
||||
});
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
member: payload.member,
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
case 'view_user_info':
|
||||
this._viewUser(payload.userId, payload.subAction);
|
||||
break;
|
||||
case 'view_room':
|
||||
// Takes either a room ID or room alias: if switching to a room the client is already
|
||||
|
@ -588,8 +574,8 @@ export default React.createClass({
|
|||
break;
|
||||
case 'view_user_settings': {
|
||||
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
|
||||
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, 'mx_SettingsDialog',
|
||||
/*isPriority=*/false, /*isStatic=*/true);
|
||||
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {},
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||
|
||||
// View the welcome or home page if we need something to look at
|
||||
this._viewSomethingBehindModal();
|
||||
|
@ -638,8 +624,9 @@ export default React.createClass({
|
|||
case 'view_invite':
|
||||
showRoomInviteDialog(payload.roomId);
|
||||
break;
|
||||
case 'notifier_enabled':
|
||||
this.forceUpdate();
|
||||
case 'notifier_enabled': {
|
||||
this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()});
|
||||
}
|
||||
break;
|
||||
case 'hide_left_panel':
|
||||
this.setState({
|
||||
|
@ -923,6 +910,22 @@ export default React.createClass({
|
|||
this.notifyNewScreen('home');
|
||||
},
|
||||
|
||||
_viewUser: function(userId, subAction) {
|
||||
// Wait for the first sync so that `getRoom` gives us a room object if it's
|
||||
// in the sync response
|
||||
const waitForSync = this.firstSyncPromise ?
|
||||
this.firstSyncPromise.promise : Promise.resolve();
|
||||
waitForSync.then(() => {
|
||||
if (subAction === 'chat') {
|
||||
this._chatCreateOrReuse(userId);
|
||||
return;
|
||||
}
|
||||
this.notifyNewScreen('user/' + userId);
|
||||
this.setState({currentUserId: userId});
|
||||
this._setPage(PageTypes.UserView);
|
||||
});
|
||||
},
|
||||
|
||||
_setMxId: function(payload) {
|
||||
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
||||
const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
|
||||
|
@ -1058,23 +1061,23 @@ export default React.createClass({
|
|||
button: _t("Leave"),
|
||||
onFinished: (shouldLeave) => {
|
||||
if (shouldLeave) {
|
||||
const d = MatrixClientPeg.get().leave(roomId);
|
||||
const d = MatrixClientPeg.get().leaveRoomChain(roomId);
|
||||
|
||||
// FIXME: controller shouldn't be loading a view :(
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
|
||||
d.then(() => {
|
||||
d.then((errors) => {
|
||||
modal.close();
|
||||
if (this.state.currentRoomId === roomId) {
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
}
|
||||
}, (err) => {
|
||||
modal.close();
|
||||
console.error("Failed to leave room " + roomId + " " + err);
|
||||
|
||||
for (const leftRoomId of Object.keys(errors)) {
|
||||
const err = errors[leftRoomId];
|
||||
if (!err) continue;
|
||||
|
||||
console.error("Failed to leave room " + leftRoomId + " " + err);
|
||||
let title = _t("Failed to leave room");
|
||||
let message = _t("Server may be unavailable, overloaded, or you hit a bug.");
|
||||
if (err.errcode == 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') {
|
||||
if (err.errcode === 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') {
|
||||
title = _t("Can't leave Server Notices room");
|
||||
message = _t(
|
||||
"This room is used for important messages from the Homeserver, " +
|
||||
|
@ -1087,6 +1090,20 @@ export default React.createClass({
|
|||
title: title,
|
||||
description: message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.currentRoomId === roomId) {
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
}
|
||||
}, (err) => {
|
||||
// This should only happen if something went seriously wrong with leaving the chain.
|
||||
modal.close();
|
||||
console.error("Failed to leave room " + roomId + " " + err);
|
||||
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
|
||||
title: _t("Failed to leave room"),
|
||||
description: _t("Unknown error"),
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -1173,7 +1190,7 @@ export default React.createClass({
|
|||
* Called when a new logged in session has started
|
||||
*/
|
||||
_onLoggedIn: async function() {
|
||||
this.setStateForNewView({view: VIEWS.LOGGED_IN});
|
||||
this.setStateForNewView({ view: VIEWS.LOGGED_IN });
|
||||
if (this._is_registered) {
|
||||
this._is_registered = false;
|
||||
|
||||
|
@ -1306,7 +1323,10 @@ export default React.createClass({
|
|||
self.firstSyncPromise.resolve();
|
||||
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
self.setState({ready: true});
|
||||
self.setState({
|
||||
ready: true,
|
||||
showNotifierToolbar: Notifier.shouldShowToolbar(),
|
||||
});
|
||||
});
|
||||
cli.on('Call.incoming', function(call) {
|
||||
// we dispatch this synchronously to make sure that the event
|
||||
|
@ -1535,7 +1555,16 @@ export default React.createClass({
|
|||
} else if (screen.indexOf('room/') == 0) {
|
||||
const segments = screen.substring(5).split('/');
|
||||
const roomString = segments[0];
|
||||
const eventId = segments[1]; // undefined if no event id given
|
||||
let eventId = segments.splice(1).join("/"); // empty string if no event id given
|
||||
|
||||
// Previously we pulled the eventID from the segments in such a way
|
||||
// where if there was no eventId then we'd get undefined. However, we
|
||||
// now do a splice and join to handle v3 event IDs which results in
|
||||
// an empty string. To maintain our potential contract with the rest
|
||||
// of the app, we coerce the eventId to be undefined where applicable.
|
||||
if (!eventId) eventId = undefined;
|
||||
|
||||
// TODO: Handle encoded room/event IDs: https://github.com/vector-im/riot-web/issues/9149
|
||||
|
||||
// FIXME: sort_out caseConsistency
|
||||
const thirdPartyInvite = {
|
||||
|
@ -1579,19 +1608,10 @@ export default React.createClass({
|
|||
dis.dispatch(payload);
|
||||
} else if (screen.indexOf('user/') == 0) {
|
||||
const userId = screen.substring(5);
|
||||
|
||||
// Wait for the first sync so that `getRoom` gives us a room object if it's
|
||||
// in the sync response
|
||||
const waitFor = this.firstSyncPromise ?
|
||||
this.firstSyncPromise.promise : Promise.resolve();
|
||||
waitFor.then(() => {
|
||||
if (params.action === 'chat') {
|
||||
this._chatCreateOrReuse(userId);
|
||||
return;
|
||||
}
|
||||
this.notifyNewScreen('user/' + userId);
|
||||
this.setState({currentUserId: userId});
|
||||
this._setPage(PageTypes.UserView);
|
||||
dis.dispatch({
|
||||
action: 'view_user_info',
|
||||
userId: userId,
|
||||
subAction: params.action,
|
||||
});
|
||||
} else if (screen.indexOf('group/') == 0) {
|
||||
const groupId = screen.substring(6);
|
||||
|
@ -1661,9 +1681,14 @@ export default React.createClass({
|
|||
dis.dispatch({ action: 'show_right_panel' });
|
||||
}
|
||||
|
||||
this.state.resizeNotifier.notifyWindowResized();
|
||||
this._windowWidth = window.innerWidth;
|
||||
},
|
||||
|
||||
_dispatchTimelineResize() {
|
||||
dis.dispatch({ action: 'timeline_resize' });
|
||||
},
|
||||
|
||||
onRoomCreated: function(roomId) {
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
|
@ -1720,7 +1745,7 @@ export default React.createClass({
|
|||
hasCancelButton: false,
|
||||
});
|
||||
|
||||
return;
|
||||
return MatrixClientPeg.get();
|
||||
}
|
||||
}
|
||||
return Lifecycle.setLoggedIn(credentials);
|
||||
|
@ -1759,7 +1784,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_setPageSubtitle: function(subtitle='') {
|
||||
document.title = `Riot ${subtitle}`;
|
||||
document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle}`;
|
||||
},
|
||||
|
||||
updateStatusIndicator: function(state, prevState) {
|
||||
|
@ -1937,7 +1962,6 @@ export default React.createClass({
|
|||
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
skipServerDetails={this.skipServerDetailsForRegistration()}
|
||||
brand={this.props.config.brand}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
|
@ -1980,7 +2004,6 @@ export default React.createClass({
|
|||
fallbackHsUrl={this.getFallbackHsUrl()}
|
||||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||
enableGuest={this.props.enableGuest}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -21,7 +21,6 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import {wantsDateSeparator} from '../../DateUtils';
|
||||
import dis from "../../dispatcher";
|
||||
import sdk from '../../index';
|
||||
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
|
@ -628,16 +627,29 @@ module.exports = React.createClass({
|
|||
_onHeightChanged: function() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.forceUpdate();
|
||||
scrollPanel.checkScroll();
|
||||
}
|
||||
},
|
||||
|
||||
_onTypingVisible: function() {
|
||||
_onTypingShown: function() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
|
||||
// scroll down if at bottom
|
||||
// this will make the timeline grow, so checkScroll
|
||||
scrollPanel.checkScroll();
|
||||
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
|
||||
scrollPanel.preventShrinking();
|
||||
}
|
||||
},
|
||||
|
||||
_onTypingHidden: function() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
if (scrollPanel) {
|
||||
// as hiding the typing notifications doesn't
|
||||
// update the scrollPanel, we tell it to apply
|
||||
// the shrinking prevention once the typing notifs are hidden
|
||||
scrollPanel.updatePreventShrinking();
|
||||
// order is important here as checkScroll will scroll down to
|
||||
// reveal added padding to balance the notifs disappearing.
|
||||
scrollPanel.checkScroll();
|
||||
scrollPanel.blockShrinking();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -653,22 +665,18 @@ module.exports = React.createClass({
|
|||
// update the min-height, so once the last
|
||||
// person stops typing, no jumping occurs
|
||||
if (isAtBottom && isTypingVisible) {
|
||||
scrollPanel.blockShrinking();
|
||||
scrollPanel.preventShrinking();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
clearTimelineHeight: function() {
|
||||
onTimelineReset: function() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.clearBlockShrinking();
|
||||
scrollPanel.clearPreventShrinking();
|
||||
}
|
||||
},
|
||||
|
||||
onResize: function() {
|
||||
dis.dispatch({ action: 'timeline_resize' }, true);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile");
|
||||
|
@ -693,7 +701,12 @@ module.exports = React.createClass({
|
|||
|
||||
let whoIsTyping;
|
||||
if (this.props.room) {
|
||||
whoIsTyping = (<WhoIsTypingTile room={this.props.room} onVisible={this._onTypingVisible} ref="whoIsTyping" />);
|
||||
whoIsTyping = (<WhoIsTypingTile
|
||||
room={this.props.room}
|
||||
onShown={this._onTypingShown}
|
||||
onHidden={this._onTypingHidden}
|
||||
ref="whoIsTyping" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -703,7 +716,8 @@ module.exports = React.createClass({
|
|||
onFillRequest={this.props.onFillRequest}
|
||||
onUnfillRequest={this.props.onUnfillRequest}
|
||||
style={style}
|
||||
stickyBottom={this.props.stickyBottom}>
|
||||
stickyBottom={this.props.stickyBottom}
|
||||
resizeNotifier={this.props.resizeNotifier}>
|
||||
{ topSpinner }
|
||||
{ this._getEventTiles() }
|
||||
{ whoIsTyping }
|
||||
|
|
|
@ -50,6 +50,7 @@ export default class RightPanel extends React.Component {
|
|||
FilePanel: 'FilePanel',
|
||||
NotificationPanel: 'NotificationPanel',
|
||||
RoomMemberInfo: 'RoomMemberInfo',
|
||||
Room3pidMemberInfo: 'Room3pidMemberInfo',
|
||||
GroupMemberInfo: 'GroupMemberInfo',
|
||||
});
|
||||
|
||||
|
@ -155,6 +156,7 @@ export default class RightPanel extends React.Component {
|
|||
groupRoomId: payload.groupRoomId,
|
||||
groupId: payload.groupId,
|
||||
member: payload.member,
|
||||
event: payload.event,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -162,6 +164,7 @@ export default class RightPanel extends React.Component {
|
|||
render() {
|
||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
|
||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
||||
|
||||
|
@ -180,6 +183,8 @@ export default class RightPanel extends React.Component {
|
|||
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) {
|
||||
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.Room3pidMemberInfo) {
|
||||
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) {
|
||||
panel = <GroupMemberInfo
|
||||
groupMember={this.state.member}
|
||||
|
@ -193,7 +198,7 @@ export default class RightPanel extends React.Component {
|
|||
} else if (this.state.phase === RightPanel.Phase.NotificationPanel) {
|
||||
panel = <NotificationPanel />;
|
||||
} else if (this.state.phase === RightPanel.Phase.FilePanel) {
|
||||
panel = <FilePanel roomId={this.props.roomId} />;
|
||||
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_RightPanel", "mx_fadable", {
|
||||
|
|
|
@ -26,14 +26,17 @@ const dis = require('../../dispatcher');
|
|||
|
||||
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import {instanceForInstanceId, protocolNameForInstanceId} from '../../utils/DirectoryUtils';
|
||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||
import Analytics from '../../Analytics';
|
||||
|
||||
const MAX_NAME_LENGTH = 80;
|
||||
const MAX_TOPIC_LENGTH = 160;
|
||||
|
||||
function track(action) {
|
||||
Analytics.trackEvent('RoomDirectory', action);
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomDirectory',
|
||||
|
||||
|
@ -53,6 +56,7 @@ module.exports = React.createClass({
|
|||
publicRooms: [],
|
||||
loading: true,
|
||||
protocolsLoading: true,
|
||||
error: null,
|
||||
instanceId: null,
|
||||
includeAll: false,
|
||||
roomServer: null,
|
||||
|
@ -95,10 +99,12 @@ module.exports = React.createClass({
|
|||
// thing you see when loading the client!
|
||||
return;
|
||||
}
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to get protocol list from homeserver', '', ErrorDialog, {
|
||||
title: _t('Failed to get protocol list from homeserver'),
|
||||
description: _t('The homeserver may be too old to support third party networks'),
|
||||
track('Failed to get protocol list from homeserver');
|
||||
this.setState({
|
||||
error: _t(
|
||||
'Riot failed to get the protocol list from the homeserver. ' +
|
||||
'The homeserver may be too old to support third party networks.',
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -187,12 +193,14 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
this.setState({ loading: false });
|
||||
console.error("Failed to get publicRooms: %s", JSON.stringify(err));
|
||||
const 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')),
|
||||
track('Failed to get public room list');
|
||||
this.setState({
|
||||
loading: false,
|
||||
error:
|
||||
`${_t('Riot failed to get the public room list.')} ` +
|
||||
`${(err && err.message) ? err.message : _t('The homeserver may be unavailable or overloaded.')}`
|
||||
,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -511,25 +519,15 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
// TODO: clean this up
|
||||
if (this.state.protocolsLoading) {
|
||||
return (
|
||||
<div className="mx_RoomDirectory">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let content;
|
||||
if (this.state.loading) {
|
||||
content = <div className="mx_RoomDirectory">
|
||||
<Loader />
|
||||
</div>;
|
||||
if (this.state.error) {
|
||||
content = this.state.error;
|
||||
} else if (this.state.protocolsLoading || this.state.loading) {
|
||||
content = <Loader />;
|
||||
} else {
|
||||
const rows = this.getRows();
|
||||
// we still show the scrollpanel, at least for now, because
|
||||
|
@ -551,12 +549,16 @@ module.exports = React.createClass({
|
|||
onFillRequest={ this.onFillRequest }
|
||||
stickyBottom={false}
|
||||
startAtBottom={false}
|
||||
onResize={function() {}}
|
||||
>
|
||||
{ scrollpanel_content }
|
||||
</ScrollPanel>;
|
||||
}
|
||||
|
||||
let listHeader;
|
||||
if (!this.state.protocolsLoading) {
|
||||
const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
|
||||
const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
|
||||
|
||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||
let instance_expected_field_type;
|
||||
if (
|
||||
|
@ -586,13 +588,21 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
listHeader = <div className="mx_RoomDirectory_listheader">
|
||||
<DirectorySearchBox
|
||||
className="mx_RoomDirectory_searchbox"
|
||||
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinClick}
|
||||
placeholder={placeholder} showJoinButton={showJoinButton}
|
||||
/>
|
||||
<NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
const createRoomButton = (<AccessibleButton
|
||||
onClick={this.onCreateRoomClicked}
|
||||
className="mx_RoomDirectory_createRoom"
|
||||
>{_t("Create new room")}</AccessibleButton>);
|
||||
|
||||
const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
|
||||
const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
|
||||
return (
|
||||
<BaseDialog
|
||||
className={'mx_RoomDirectory_dialog'}
|
||||
|
@ -603,14 +613,7 @@ module.exports = React.createClass({
|
|||
>
|
||||
<div className="mx_RoomDirectory">
|
||||
<div className="mx_RoomDirectory_list">
|
||||
<div className="mx_RoomDirectory_listheader">
|
||||
<DirectorySearchBox
|
||||
className="mx_RoomDirectory_searchbox"
|
||||
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinClick}
|
||||
placeholder={placeholder} showJoinButton={showJoinButton}
|
||||
/>
|
||||
<NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} />
|
||||
</div>
|
||||
{listHeader}
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018, 2019 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.
|
||||
|
@ -146,8 +146,8 @@ const RoomSubList = React.createClass({
|
|||
key={room.roomId}
|
||||
collapsed={this.props.collapsed || false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite}
|
||||
notificationCount={room.getUnreadNotificationCount()}
|
||||
highlight={this.props.isInvite || RoomNotifs.getUnreadNotificationCount(room, 'highlight') > 0}
|
||||
notificationCount={RoomNotifs.getUnreadNotificationCount(room)}
|
||||
isInvite={this.props.isInvite}
|
||||
refreshSubList={this._updateSubListCount}
|
||||
incomingCall={null}
|
||||
|
|
|
@ -19,29 +19,28 @@ limitations under the License.
|
|||
// TODO: This component is enormous! There's several things which could stand-alone:
|
||||
// - Search results component
|
||||
// - Drag and drop
|
||||
// - File uploading - uploadFile()
|
||||
|
||||
import shouldHideEvent from "../../shouldHideEvent";
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
import filesize from 'filesize';
|
||||
const classNames = require("classnames");
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../languageHandler';
|
||||
import {RoomPermalinkCreator} from "../../matrix-to";
|
||||
import {RoomPermalinkCreator} from '../../matrix-to';
|
||||
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const ContentMessages = require("../../ContentMessages");
|
||||
const Modal = require("../../Modal");
|
||||
const sdk = require('../../index');
|
||||
const CallHandler = require('../../CallHandler');
|
||||
const dis = require("../../dispatcher");
|
||||
const Tinter = require("../../Tinter");
|
||||
const rate_limited_func = require('../../ratelimitedfunc');
|
||||
const ObjectUtils = require('../../ObjectUtils');
|
||||
const Rooms = require('../../Rooms');
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
import Modal from '../../Modal';
|
||||
import sdk from '../../index';
|
||||
import CallHandler from '../../CallHandler';
|
||||
import dis from '../../dispatcher';
|
||||
import Tinter from '../../Tinter';
|
||||
import rate_limited_func from '../../ratelimitedfunc';
|
||||
import ObjectUtils from '../../ObjectUtils';
|
||||
import Rooms from '../../Rooms';
|
||||
|
||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
|
||||
|
@ -52,6 +51,7 @@ import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
|||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
import WidgetUtils from '../../utils/WidgetUtils';
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function() {};
|
||||
|
@ -144,6 +144,7 @@ module.exports = React.createClass({
|
|||
// the end of the live timeline. It has the effect of hiding the
|
||||
// 'scroll to bottom' knob, among a couple of other things.
|
||||
atEndOfLiveTimeline: true,
|
||||
atEndOfLiveTimelineInit: false, // used by componentDidUpdate to avoid unnecessary checks
|
||||
|
||||
showTopUnreadMessagesBar: false,
|
||||
|
||||
|
@ -168,7 +169,6 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this._fetchMediaConfig();
|
||||
// Start listening for RoomViewStore updates
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
|
@ -176,27 +176,6 @@ module.exports = React.createClass({
|
|||
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
|
||||
},
|
||||
|
||||
_fetchMediaConfig: function(invalidateCache: boolean = false) {
|
||||
/// NOTE: Using global here so we don't make repeated requests for the
|
||||
/// config every time we swap room.
|
||||
if(global.mediaConfig !== undefined && !invalidateCache) {
|
||||
this.setState({mediaConfig: global.mediaConfig});
|
||||
return;
|
||||
}
|
||||
console.log("[Media Config] Fetching");
|
||||
MatrixClientPeg.get().getMediaConfig().then((config) => {
|
||||
console.log("[Media Config] Fetched config:", config);
|
||||
return config;
|
||||
}).catch(() => {
|
||||
// Media repo can't or won't report limits, so provide an empty object (no limits).
|
||||
console.log("[Media Config] Could not fetch config, so not limiting uploads.");
|
||||
return {};
|
||||
}).then((config) => {
|
||||
global.mediaConfig = config;
|
||||
this.setState({mediaConfig: config});
|
||||
});
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdate: function(initial) {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
|
@ -293,6 +272,28 @@ module.exports = React.createClass({
|
|||
return this.state.room ? this.state.room.roomId : this.state.roomId;
|
||||
},
|
||||
|
||||
_getPermalinkCreatorForRoom: function(room) {
|
||||
if (!this._permalinkCreators) this._permalinkCreators = {};
|
||||
if (this._permalinkCreators[room.roomId]) return this._permalinkCreators[room.roomId];
|
||||
|
||||
this._permalinkCreators[room.roomId] = new RoomPermalinkCreator(room);
|
||||
if (this.state.room && room.roomId === this.state.room.roomId) {
|
||||
// We want to watch for changes in the creator for the primary room in the view, but
|
||||
// don't need to do so for search results.
|
||||
this._permalinkCreators[room.roomId].start();
|
||||
} else {
|
||||
this._permalinkCreators[room.roomId].load();
|
||||
}
|
||||
return this._permalinkCreators[room.roomId];
|
||||
},
|
||||
|
||||
_stopAllPermalinkCreators: function() {
|
||||
if (!this._permalinkCreators) return;
|
||||
for (const roomId of Object.keys(this._permalinkCreators)) {
|
||||
this._permalinkCreators[roomId].stop();
|
||||
}
|
||||
},
|
||||
|
||||
_onWidgetEchoStoreUpdate: function() {
|
||||
this.setState({
|
||||
showApps: this._shouldShowApps(this.state.room),
|
||||
|
@ -392,7 +393,9 @@ module.exports = React.createClass({
|
|||
this._updateConfCallNotification();
|
||||
|
||||
window.addEventListener('beforeunload', this.onPageUnload);
|
||||
window.addEventListener('resize', this.onResize);
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
|
||||
}
|
||||
this.onResize();
|
||||
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
|
@ -428,6 +431,18 @@ module.exports = React.createClass({
|
|||
roomView.addEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: We check the ref here with a flag because componentDidMount, despite
|
||||
// documentation, does not define our messagePanel ref. It looks like our spinner
|
||||
// in render() prevents the ref from being set on first mount, so we try and
|
||||
// catch the messagePanel when it does mount. Because we only want the ref once,
|
||||
// we use a boolean flag to avoid duplicate work.
|
||||
if (this.refs.messagePanel && !this.state.atEndOfLiveTimelineInit) {
|
||||
this.setState({
|
||||
atEndOfLiveTimelineInit: true,
|
||||
atEndOfLiveTimeline: this.refs.messagePanel.isAtEndOfLiveTimeline(),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -443,9 +458,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
// stop tracking room changes to format permalinks
|
||||
if (this.state.permalinkCreator) {
|
||||
this.state.permalinkCreator.stop();
|
||||
}
|
||||
this._stopAllPermalinkCreators();
|
||||
|
||||
if (this.refs.roomView) {
|
||||
// disconnect the D&D event listeners from the room view. This
|
||||
|
@ -472,7 +485,9 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
|
||||
}
|
||||
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
|
||||
|
@ -492,7 +507,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onPageUnload(event) {
|
||||
if (ContentMessages.getCurrentUploads().length > 0) {
|
||||
if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) {
|
||||
return event.returnValue =
|
||||
_t("You seem to be uploading files, are you sure you want to quit?");
|
||||
} else if (this._getCallForRoom() && this.state.callState !== 'ended') {
|
||||
|
@ -541,16 +556,14 @@ module.exports = React.createClass({
|
|||
payload.data.description || payload.data.name);
|
||||
break;
|
||||
case 'picture_snapshot':
|
||||
this.uploadFile(payload.file);
|
||||
return ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
[payload.file], this.state.room.roomId, MatrixClientPeg.get(),
|
||||
);
|
||||
break;
|
||||
case 'upload_failed':
|
||||
// 413: File was too big or upset the server in some way.
|
||||
if (payload.error && payload.error.http_status === 413) {
|
||||
this._fetchMediaConfig(true);
|
||||
}
|
||||
case 'notifier_enabled':
|
||||
case 'upload_started':
|
||||
case 'upload_finished':
|
||||
case 'upload_canceled':
|
||||
this.forceUpdate();
|
||||
break;
|
||||
case 'call_state':
|
||||
|
@ -658,11 +671,6 @@ module.exports = React.createClass({
|
|||
this._loadMembersIfJoined(room);
|
||||
this._calculateRecommendedVersion(room);
|
||||
this._updateE2EStatus(room);
|
||||
if (!this.state.permalinkCreator) {
|
||||
const permalinkCreator = new RoomPermalinkCreator(room);
|
||||
permalinkCreator.start();
|
||||
this.setState({permalinkCreator});
|
||||
}
|
||||
},
|
||||
|
||||
_calculateRecommendedVersion: async function(room) {
|
||||
|
@ -738,8 +746,19 @@ module.exports = React.createClass({
|
|||
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) {
|
||||
return;
|
||||
}
|
||||
if (!MatrixClientPeg.get().isCryptoEnabled()) {
|
||||
// If crypto is not currently enabled, we aren't tracking devices at all,
|
||||
// so we don't know what the answer is. Let's error on the safe side and show
|
||||
// a warning for this case.
|
||||
this.setState({
|
||||
e2eStatus: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
|
||||
this.setState({e2eStatus: hasUnverifiedDevices ? "warning" : "verified"});
|
||||
this.setState({
|
||||
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -865,10 +884,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onSearchResultsResize: function() {
|
||||
dis.dispatch({ action: 'timeline_resize' }, true);
|
||||
},
|
||||
|
||||
onSearchResultsFillRequest: function(backwards) {
|
||||
if (!backwards) {
|
||||
return Promise.resolve(false);
|
||||
|
@ -1001,9 +1016,11 @@ module.exports = React.createClass({
|
|||
onDrop: function(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
ev.dataTransfer.files, this.state.room.roomId, MatrixClientPeg.get(),
|
||||
);
|
||||
this.setState({ draggingFile: false });
|
||||
const files = [...ev.dataTransfer.files];
|
||||
files.forEach(this.uploadFile);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
onDragLeaveOrEnd: function(ev) {
|
||||
|
@ -1012,55 +1029,13 @@ module.exports = React.createClass({
|
|||
this.setState({ draggingFile: false });
|
||||
},
|
||||
|
||||
isFileUploadAllowed(file) {
|
||||
if (this.state.mediaConfig !== undefined &&
|
||||
this.state.mediaConfig["m.upload.size"] !== undefined &&
|
||||
file.size > this.state.mediaConfig["m.upload.size"]) {
|
||||
return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(this.state.mediaConfig["m.upload.size"])});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
uploadFile: async function(file) {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ContentMessages.sendContentToRoom(file, this.state.room.roomId, MatrixClientPeg.get());
|
||||
} catch (error) {
|
||||
if (error.name === "UnknownDeviceError") {
|
||||
// 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")),
|
||||
});
|
||||
|
||||
// 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: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
|
||||
ContentMessages.sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
|
||||
ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
|
||||
.done(undefined, (error) => {
|
||||
if (error.name === "UnknownDeviceError") {
|
||||
// Let the staus bar handle this
|
||||
|
@ -1209,6 +1184,7 @@ module.exports = React.createClass({
|
|||
|
||||
const mxEv = result.context.getEvent();
|
||||
const roomId = mxEv.getRoomId();
|
||||
const room = cli.getRoom(roomId);
|
||||
|
||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||
// XXX: can this ever happen? It will make the result count
|
||||
|
@ -1218,7 +1194,6 @@ module.exports = React.createClass({
|
|||
|
||||
if (this.state.searchScope === 'All') {
|
||||
if (roomId != lastRoomId) {
|
||||
const room = cli.getRoom(roomId);
|
||||
|
||||
// XXX: if we've left the room, we might not know about
|
||||
// it. We should tell the js sdk to go and find out about
|
||||
|
@ -1239,7 +1214,7 @@ module.exports = React.createClass({
|
|||
searchResult={result}
|
||||
searchHighlights={this.state.searchHighlights}
|
||||
resultLink={resultLink}
|
||||
permalinkCreator={this.state.permalinkCreator}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(room)}
|
||||
onHeightChanged={onHeightChanged} />);
|
||||
}
|
||||
return ret;
|
||||
|
@ -1364,8 +1339,7 @@ module.exports = React.createClass({
|
|||
|
||||
const showBar = this.refs.messagePanel.canJumpToReadMarker();
|
||||
if (this.state.showTopUnreadMessagesBar != showBar) {
|
||||
this.setState({showTopUnreadMessagesBar: showBar},
|
||||
this.onChildResize);
|
||||
this.setState({showTopUnreadMessagesBar: showBar});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1408,7 +1382,7 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
onResize: function(e) {
|
||||
onResize: function() {
|
||||
// It seems flexbox doesn't give us a way to constrain the auxPanel height to have
|
||||
// a minimum of the height of the video element, whilst also capping it from pushing out the page
|
||||
// so we have to do it via JS instead. In this implementation we cap the height by putting
|
||||
|
@ -1426,9 +1400,6 @@ module.exports = React.createClass({
|
|||
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
|
||||
|
||||
this.setState({auxPanelMaxHeight: auxPanelMaxHeight});
|
||||
|
||||
// changing the maxHeight on the auxpanel will trigger a callback go
|
||||
// onChildResize, so no need to worry about that here.
|
||||
},
|
||||
|
||||
onFullscreenClick: function() {
|
||||
|
@ -1458,10 +1429,6 @@ module.exports = React.createClass({
|
|||
this.forceUpdate(); // TODO: just update the voip buttons
|
||||
},
|
||||
|
||||
onChildResize: function() {
|
||||
// no longer anything to do here
|
||||
},
|
||||
|
||||
onStatusBarVisible: function() {
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
|
@ -1515,6 +1482,25 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_getOldRoom: function() {
|
||||
const createEvent = this.state.room.currentState.getStateEvents("m.room.create", "");
|
||||
if (!createEvent || !createEvent.getContent()['predecessor']) return null;
|
||||
|
||||
return MatrixClientPeg.get().getRoom(createEvent.getContent()['predecessor']['room_id']);
|
||||
},
|
||||
|
||||
_getHiddenHighlightCount: function() {
|
||||
const oldRoom = this._getOldRoom();
|
||||
if (!oldRoom) return 0;
|
||||
return oldRoom.getUnreadNotificationCount('highlight');
|
||||
},
|
||||
|
||||
_onHiddenHighlightsClick: function() {
|
||||
const oldRoom = this._getOldRoom();
|
||||
if (!oldRoom) return;
|
||||
dis.dispatch({action: "view_room", room_id: oldRoom.roomId});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||
|
@ -1525,16 +1511,21 @@ module.exports = React.createClass({
|
|||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar");
|
||||
const RoomRecoveryReminder = sdk.getComponent("rooms.RoomRecoveryReminder");
|
||||
|
||||
if (!this.state.room) {
|
||||
if (this.state.roomLoading || this.state.peekLoading) {
|
||||
const loading = this.state.roomLoading || this.state.peekLoading;
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<Loader />
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
error={this.state.roomLoadError}
|
||||
loading={loading}
|
||||
joining={this.state.joining}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
@ -1552,29 +1543,17 @@ module.exports = React.createClass({
|
|||
const roomAlias = this.state.roomAlias;
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomHeader ref="header"
|
||||
room={this.state.room}
|
||||
oobData={this.props.oobData}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
/>
|
||||
<div className="mx_RoomView_body">
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
canPreview={false} error={this.state.roomLoadError}
|
||||
roomAlias={roomAlias}
|
||||
spinner={this.state.joining}
|
||||
spinnerState="joining"
|
||||
joining={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
room={this.state.room}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomView_messagePanel"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1583,9 +1562,12 @@ module.exports = React.createClass({
|
|||
if (myMembership == 'invite') {
|
||||
if (this.state.joining || this.state.rejecting) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<Loader />
|
||||
</div>
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
error={this.state.roomLoadError}
|
||||
joining={this.state.joining}
|
||||
rejecting={this.state.rejecting}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
@ -1600,27 +1582,15 @@ module.exports = React.createClass({
|
|||
// We have a regular invite for this room.
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomHeader
|
||||
ref="header"
|
||||
room={this.state.room}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
/>
|
||||
<div className="mx_RoomView_body">
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectButtonClicked}
|
||||
inviterName={inviterName}
|
||||
canPreview={false}
|
||||
spinner={this.state.joining}
|
||||
spinnerState="joining"
|
||||
joining={this.state.joining}
|
||||
room={this.state.room}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomView_messagePanel"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1641,7 +1611,7 @@ module.exports = React.createClass({
|
|||
let statusBar;
|
||||
let isStatusAreaExpanded = true;
|
||||
|
||||
if (ContentMessages.getCurrentUploads().length > 0) {
|
||||
if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) {
|
||||
const UploadBar = sdk.getComponent('structures.UploadBar');
|
||||
statusBar = <UploadBar room={this.state.room} />;
|
||||
} else if (!this.state.searchResults) {
|
||||
|
@ -1654,7 +1624,6 @@ module.exports = React.createClass({
|
|||
isPeeking={myMembership !== "join"}
|
||||
onInviteClick={this.onInviteButtonClick}
|
||||
onStopWarningClick={this.onStopAloneWarningClick}
|
||||
onResize={this.onChildResize}
|
||||
onVisible={this.onStatusBarVisible}
|
||||
onHidden={this.onStatusBarHidden}
|
||||
/>;
|
||||
|
@ -1673,8 +1642,12 @@ module.exports = React.createClass({
|
|||
!MatrixClientPeg.get().getKeyBackupEnabled()
|
||||
);
|
||||
|
||||
const hiddenHighlightCount = this._getHiddenHighlightCount();
|
||||
|
||||
let aux = null;
|
||||
let previewBar;
|
||||
let hideCancel = false;
|
||||
let hideRightPanel = false;
|
||||
if (this.state.forwardingEvent !== null) {
|
||||
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
|
||||
} else if (this.state.searching) {
|
||||
|
@ -1701,18 +1674,36 @@ module.exports = React.createClass({
|
|||
invitedEmail = this.props.thirdPartyInvite.invitedEmail;
|
||||
}
|
||||
hideCancel = true;
|
||||
aux = (
|
||||
previewBar = (
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
spinner={this.state.joining}
|
||||
spinnerState="joining"
|
||||
joining={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
canPreview={this.state.canPeek}
|
||||
room={this.state.room}
|
||||
/>
|
||||
);
|
||||
if (!this.state.canPeek) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
{ previewBar }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
hideRightPanel = true;
|
||||
}
|
||||
} else if (hiddenHighlightCount > 0) {
|
||||
aux = (
|
||||
<AccessibleButton element="div" className="mx_RoomView_auxPanel_hiddenHighlights"
|
||||
onClick={this._onHiddenHighlightsClick}>
|
||||
{_t(
|
||||
"You have %(count)s unread notifications in a prior version of this room.",
|
||||
{count: hiddenHighlightCount},
|
||||
)}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
const auxPanel = (
|
||||
|
@ -1723,7 +1714,6 @@ module.exports = React.createClass({
|
|||
draggingFile={this.state.draggingFile}
|
||||
displayConfCallNotification={this.state.displayConfCallNotification}
|
||||
maxHeight={this.state.auxPanelMaxHeight}
|
||||
onResize={this.onChildResize}
|
||||
showApps={this.state.showApps}
|
||||
hideAppsDrawer={false} >
|
||||
{ aux }
|
||||
|
@ -1739,22 +1729,14 @@ module.exports = React.createClass({
|
|||
messageComposer =
|
||||
<MessageComposer
|
||||
room={this.state.room}
|
||||
onResize={this.onChildResize}
|
||||
uploadFile={this.uploadFile}
|
||||
callState={this.state.callState}
|
||||
disabled={this.props.disabled}
|
||||
showApps={this.state.showApps}
|
||||
uploadAllowed={this.isFileUploadAllowed}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
permalinkCreator={this.state.permalinkCreator}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
const AuthButtons = sdk.getComponent('views.auth.AuthButtons');
|
||||
messageComposer = <AuthButtons />;
|
||||
}
|
||||
|
||||
// TODO: Why aren't we storing the term/scope/count in this format
|
||||
// in this.state if this is what RoomHeader desires?
|
||||
if (this.state.searchResults) {
|
||||
|
@ -1814,7 +1796,7 @@ module.exports = React.createClass({
|
|||
<ScrollPanel ref="searchResultsPanel"
|
||||
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
|
||||
onFillRequest={this.onSearchResultsFillRequest}
|
||||
onResize={this.onSearchResultsResize}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
<li className={scrollheader_classes}></li>
|
||||
{ this.getSearchResultTiles() }
|
||||
|
@ -1848,7 +1830,8 @@ module.exports = React.createClass({
|
|||
showUrlPreview = {this.state.showUrlPreview}
|
||||
className="mx_RoomView_messagePanel"
|
||||
membersLoaded={this.state.membersLoaded}
|
||||
permalinkCreator={this.state.permalinkCreator}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
@ -1881,14 +1864,16 @@ module.exports = React.createClass({
|
|||
},
|
||||
);
|
||||
|
||||
const rightPanel = this.state.room ? <RightPanel roomId={this.state.room.roomId} /> : undefined;
|
||||
const rightPanel = !hideRightPanel && this.state.room &&
|
||||
<RightPanel roomId={this.state.room.roomId} resizeNotifier={this.props.resizeNotifier} />;
|
||||
const collapsedRhs = hideRightPanel || this.props.collapsedRhs;
|
||||
|
||||
return (
|
||||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
|
||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||
oobData={this.props.oobData}
|
||||
inRoom={myMembership === 'join'}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
collapsedRhs={collapsedRhs}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onPinnedClick={this.onPinnedClick}
|
||||
|
@ -1897,7 +1882,11 @@ module.exports = React.createClass({
|
|||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
/>
|
||||
<MainSplit panel={rightPanel} collapsedRhs={this.props.collapsedRhs}>
|
||||
<MainSplit
|
||||
panel={rightPanel}
|
||||
collapsedRhs={collapsedRhs}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
<div className={fadableSectionClasses}>
|
||||
{ auxPanel }
|
||||
<div className="mx_RoomView_timeline">
|
||||
|
@ -1912,6 +1901,7 @@ module.exports = React.createClass({
|
|||
{ statusBar }
|
||||
</div>
|
||||
</div>
|
||||
{ previewBar }
|
||||
{ messageComposer }
|
||||
</div>
|
||||
</MainSplit>
|
||||
|
|
|
@ -15,14 +15,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import sdk from '../../index.js';
|
||||
import Timer from '../../utils/Timer';
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
const DEBUG_SCROLL = false;
|
||||
// var DEBUG_SCROLL = true;
|
||||
|
||||
// The amount of extra scroll distance to allow prior to unfilling.
|
||||
// See _getExcessHeight.
|
||||
|
@ -30,12 +29,18 @@ const UNPAGINATION_PADDING = 6000;
|
|||
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
||||
// many scroll events causing many unfilling requests.
|
||||
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
|
||||
// _updateHeight makes the height a ceiled multiple of this so we
|
||||
// don't have to update the height too often. It also allows the user
|
||||
// to scroll past the pagination spinner a bit so they don't feel blocked so
|
||||
// much while the content loads.
|
||||
const PAGE_SIZE = 400;
|
||||
|
||||
let debuglog;
|
||||
if (DEBUG_SCROLL) {
|
||||
// using bind means that we get to keep useful line numbers in the console
|
||||
var debuglog = console.log.bind(console);
|
||||
debuglog = console.log.bind(console, "ScrollPanel debuglog:");
|
||||
} else {
|
||||
var debuglog = function() {};
|
||||
debuglog = function() {};
|
||||
}
|
||||
|
||||
/* This component implements an intelligent scrolling list.
|
||||
|
@ -129,11 +134,6 @@ module.exports = React.createClass({
|
|||
*/
|
||||
onScroll: PropTypes.func,
|
||||
|
||||
/* onResize: a callback which is called whenever the Gemini scroll
|
||||
* panel is resized
|
||||
*/
|
||||
onResize: PropTypes.func,
|
||||
|
||||
/* className: classnames to add to the top-level div
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
|
@ -141,6 +141,9 @@ module.exports = React.createClass({
|
|||
/* style: styles to add to the top-level div
|
||||
*/
|
||||
style: PropTypes.object,
|
||||
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
|
||||
*/
|
||||
resizeNotifier: PropTypes.object,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -150,12 +153,18 @@ module.exports = React.createClass({
|
|||
onFillRequest: function(backwards) { return Promise.resolve(false); },
|
||||
onUnfillRequest: function(backwards, scrollToken) {},
|
||||
onScroll: function() {},
|
||||
onResize: function() {},
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._fillRequestWhileRunning = false;
|
||||
this._isFilling = false;
|
||||
this._pendingFillRequests = {b: null, f: null};
|
||||
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
|
||||
}
|
||||
|
||||
this.resetScrollState();
|
||||
},
|
||||
|
||||
|
@ -170,6 +179,7 @@ module.exports = React.createClass({
|
|||
//
|
||||
// This will also re-check the fill state, in case the paginate was inadequate
|
||||
this.checkScroll();
|
||||
this.updatePreventShrinking();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -178,81 +188,49 @@ module.exports = React.createClass({
|
|||
//
|
||||
// (We could use isMounted(), but facebook have deprecated that.)
|
||||
this.unmounted = true;
|
||||
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
|
||||
}
|
||||
},
|
||||
|
||||
onScroll: function(ev) {
|
||||
const sn = this._getScrollNode();
|
||||
debuglog("Scroll event: offset now:", sn.scrollTop,
|
||||
"_lastSetScroll:", this._lastSetScroll);
|
||||
|
||||
// Sometimes we see attempts to write to scrollTop essentially being
|
||||
// ignored. (Or rather, it is successfully written, but on the next
|
||||
// scroll event, it's been reset again).
|
||||
//
|
||||
// This was observed on Chrome 47, when scrolling using the trackpad in OS
|
||||
// X Yosemite. Can't reproduce on El Capitan. Our theory is that this is
|
||||
// due to Chrome not being able to cope with the scroll offset being reset
|
||||
// while a two-finger drag is in progress.
|
||||
//
|
||||
// By way of a workaround, we detect this situation and just keep
|
||||
// resetting scrollTop until we see the scroll node have the right
|
||||
// value.
|
||||
if (this._lastSetScroll !== undefined && sn.scrollTop < this._lastSetScroll-200) {
|
||||
console.log("Working around vector-im/vector-web#528");
|
||||
this._restoreSavedScrollState();
|
||||
return;
|
||||
}
|
||||
|
||||
// If there weren't enough children to fill the viewport, the scroll we
|
||||
// got might be different to the scroll we wanted; we don't want to
|
||||
// forget what we wanted, so don't overwrite the saved state unless
|
||||
// this appears to be a user-initiated scroll.
|
||||
if (sn.scrollTop != this._lastSetScroll) {
|
||||
debuglog("onScroll", this._getScrollNode().scrollTop);
|
||||
this._scrollTimeout.restart();
|
||||
this._saveScrollState();
|
||||
} else {
|
||||
debuglog("Ignoring scroll echo");
|
||||
// only ignore the echo once, otherwise we'll get confused when the
|
||||
// user scrolls away from, and back to, the autoscroll point.
|
||||
this._lastSetScroll = undefined;
|
||||
}
|
||||
|
||||
this._checkBlockShrinking();
|
||||
|
||||
this.updatePreventShrinking();
|
||||
this.props.onScroll(ev);
|
||||
|
||||
this.checkFillState();
|
||||
},
|
||||
|
||||
onResize: function() {
|
||||
this.clearBlockShrinking();
|
||||
this.props.onResize();
|
||||
this.checkScroll();
|
||||
if (this._gemScroll) this._gemScroll.forceUpdate();
|
||||
// update preventShrinkingState if present
|
||||
if (this.preventShrinkingState) {
|
||||
this.preventShrinking();
|
||||
}
|
||||
},
|
||||
|
||||
// after an update to the contents of the panel, check that the scroll is
|
||||
// where it ought to be, and set off pagination requests if necessary.
|
||||
checkScroll: function() {
|
||||
this._restoreSavedScrollState();
|
||||
this._checkBlockShrinking();
|
||||
this.checkFillState();
|
||||
},
|
||||
|
||||
// return true if the content is fully scrolled down right now; else false.
|
||||
//
|
||||
// note that this is independent of the 'stuckAtBottom' state - it is simply
|
||||
// about whether the the content is scrolled down right now, irrespective of
|
||||
// about whether the content is scrolled down right now, irrespective of
|
||||
// whether it will stay that way when the children update.
|
||||
isAtBottom: function() {
|
||||
const sn = this._getScrollNode();
|
||||
// fractional values (both too big and too small)
|
||||
// for scrollTop happen on certain browsers/platforms
|
||||
// when scrolled all the way down. E.g. Chrome 72 on debian.
|
||||
// so check difference <= 1;
|
||||
return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1;
|
||||
|
||||
// there seems to be some bug with flexbox/gemini/chrome/richvdh's
|
||||
// understanding of the box model, wherein the scrollNode ends up 2
|
||||
// pixels higher than the available space, even when there are less
|
||||
// than a screenful of messages. + 3 is a fudge factor to pretend
|
||||
// that we're at the bottom when we're still a few pixels off.
|
||||
|
||||
return sn.scrollHeight - Math.ceil(sn.scrollTop) <= sn.clientHeight + 3;
|
||||
},
|
||||
|
||||
// returns the vertical height in the given direction that can be removed from
|
||||
|
@ -288,19 +266,25 @@ module.exports = React.createClass({
|
|||
// `---------' -
|
||||
_getExcessHeight: function(backwards) {
|
||||
const sn = this._getScrollNode();
|
||||
const contentHeight = this._getMessagesHeight();
|
||||
const listHeight = this._getListHeight();
|
||||
const clippedHeight = contentHeight - listHeight;
|
||||
const unclippedScrollTop = sn.scrollTop + clippedHeight;
|
||||
|
||||
if (backwards) {
|
||||
return sn.scrollTop - sn.clientHeight - UNPAGINATION_PADDING;
|
||||
return unclippedScrollTop - sn.clientHeight - UNPAGINATION_PADDING;
|
||||
} else {
|
||||
return sn.scrollHeight - (sn.scrollTop + 2*sn.clientHeight) - UNPAGINATION_PADDING;
|
||||
return contentHeight - (unclippedScrollTop + 2*sn.clientHeight) - UNPAGINATION_PADDING;
|
||||
}
|
||||
},
|
||||
|
||||
// check the scroll state and send out backfill requests if necessary.
|
||||
checkFillState: function() {
|
||||
checkFillState: async function(depth=0) {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFirstCall = depth === 0;
|
||||
const sn = this._getScrollNode();
|
||||
|
||||
// if there is less than a screenful of messages above or below the
|
||||
|
@ -327,13 +311,53 @@ module.exports = React.createClass({
|
|||
// `---------' -
|
||||
//
|
||||
|
||||
if (sn.scrollTop < sn.clientHeight) {
|
||||
// need to back-fill
|
||||
this._maybeFill(true);
|
||||
// as filling is async and recursive,
|
||||
// don't allow more than 1 chain of calls concurrently
|
||||
// do make a note when a new request comes in while already running one,
|
||||
// so we can trigger a new chain of calls once done.
|
||||
if (isFirstCall) {
|
||||
if (this._isFilling) {
|
||||
debuglog("_isFilling: not entering while request is ongoing, marking for a subsequent request");
|
||||
this._fillRequestWhileRunning = true;
|
||||
return;
|
||||
}
|
||||
if (sn.scrollTop > sn.scrollHeight - sn.clientHeight * 2) {
|
||||
debuglog("_isFilling: setting");
|
||||
this._isFilling = true;
|
||||
}
|
||||
|
||||
const itemlist = this.refs.itemlist;
|
||||
const firstTile = itemlist && itemlist.firstElementChild;
|
||||
const contentTop = firstTile && firstTile.offsetTop;
|
||||
const fillPromises = [];
|
||||
|
||||
// if scrollTop gets to 1 screen from the top of the first tile,
|
||||
// try backward filling
|
||||
if (!firstTile || (sn.scrollTop - contentTop) < sn.clientHeight) {
|
||||
// need to back-fill
|
||||
fillPromises.push(this._maybeFill(depth, true));
|
||||
}
|
||||
// if scrollTop gets to 2 screens from the end (so 1 screen below viewport),
|
||||
// try forward filling
|
||||
if ((sn.scrollHeight - sn.scrollTop) < sn.clientHeight * 2) {
|
||||
// need to forward-fill
|
||||
this._maybeFill(false);
|
||||
fillPromises.push(this._maybeFill(depth, false));
|
||||
}
|
||||
|
||||
if (fillPromises.length) {
|
||||
try {
|
||||
await Promise.all(fillPromises);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
if (isFirstCall) {
|
||||
debuglog("_isFilling: clearing");
|
||||
this._isFilling = false;
|
||||
}
|
||||
|
||||
if (this._fillRequestWhileRunning) {
|
||||
this._fillRequestWhileRunning = false;
|
||||
this.checkFillState();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -343,6 +367,9 @@ module.exports = React.createClass({
|
|||
if (excessHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const origExcessHeight = excessHeight;
|
||||
|
||||
const tiles = this.refs.itemlist.children;
|
||||
|
||||
// The scroll token of the first/last tile to be unpaginated
|
||||
|
@ -354,8 +381,9 @@ module.exports = React.createClass({
|
|||
// pagination.
|
||||
//
|
||||
// If backwards is true, we unpaginate (remove) tiles from the back (top).
|
||||
let tile;
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const tile = tiles[backwards ? i : tiles.length - 1 - i];
|
||||
tile = tiles[backwards ? i : tiles.length - 1 - i];
|
||||
// Subtract height of tile as if it were unpaginated
|
||||
excessHeight -= tile.clientHeight;
|
||||
//If removing the tile would lead to future pagination, break before setting scroll token
|
||||
|
@ -376,26 +404,31 @@ module.exports = React.createClass({
|
|||
}
|
||||
this._unfillDebouncer = setTimeout(() => {
|
||||
this._unfillDebouncer = null;
|
||||
debuglog("unfilling now", backwards, origExcessHeight);
|
||||
this.props.onUnfillRequest(backwards, markerScrollToken);
|
||||
}, UNFILL_REQUEST_DEBOUNCE_MS);
|
||||
}
|
||||
},
|
||||
|
||||
// check if there is already a pending fill request. If not, set one off.
|
||||
_maybeFill: function(backwards) {
|
||||
_maybeFill: function(depth, backwards) {
|
||||
const dir = backwards ? 'b' : 'f';
|
||||
if (this._pendingFillRequests[dir]) {
|
||||
debuglog("ScrollPanel: Already a "+dir+" fill in progress - not starting another");
|
||||
debuglog("Already a "+dir+" fill in progress - not starting another");
|
||||
return;
|
||||
}
|
||||
|
||||
debuglog("ScrollPanel: starting "+dir+" fill");
|
||||
debuglog("starting "+dir+" fill");
|
||||
|
||||
// onFillRequest can end up calling us recursively (via onScroll
|
||||
// events) so make sure we set this before firing off the call.
|
||||
this._pendingFillRequests[dir] = true;
|
||||
|
||||
Promise.try(() => {
|
||||
// wait 1ms before paginating, because otherwise
|
||||
// this will block the scroll event handler for +700ms
|
||||
// if messages are already cached in memory,
|
||||
// This would cause jumping to happen on Chrome/macOS.
|
||||
return new Promise(resolve => setTimeout(resolve, 1)).then(() => {
|
||||
return this.props.onFillRequest(backwards);
|
||||
}).finally(() => {
|
||||
this._pendingFillRequests[dir] = false;
|
||||
|
@ -406,14 +439,14 @@ module.exports = React.createClass({
|
|||
// Unpaginate once filling is complete
|
||||
this._checkUnfillState(!backwards);
|
||||
|
||||
debuglog("ScrollPanel: "+dir+" fill complete; hasMoreResults:"+hasMoreResults);
|
||||
debuglog(""+dir+" fill complete; hasMoreResults:"+hasMoreResults);
|
||||
if (hasMoreResults) {
|
||||
// further pagination requests have been disabled until now, so
|
||||
// it's time to check the fill state again in case the pagination
|
||||
// was insufficient.
|
||||
this.checkFillState();
|
||||
return this.checkFillState(depth + 1);
|
||||
}
|
||||
}).done();
|
||||
});
|
||||
},
|
||||
|
||||
/* get the current scroll state. This returns an object with the following
|
||||
|
@ -426,7 +459,7 @@ module.exports = React.createClass({
|
|||
* false, the first token in data-scroll-tokens of the child which we are
|
||||
* tracking.
|
||||
*
|
||||
* number pixelOffset: undefined if stuckAtBottom is true; if it is false,
|
||||
* number bottomOffset: undefined if stuckAtBottom is true; if it is false,
|
||||
* the number of pixels the bottom of the tracked child is above the
|
||||
* bottom of the scroll panel.
|
||||
*/
|
||||
|
@ -447,14 +480,20 @@ module.exports = React.createClass({
|
|||
* child list.)
|
||||
*/
|
||||
resetScrollState: function() {
|
||||
this.scrollState = {stuckAtBottom: this.props.startAtBottom};
|
||||
this.scrollState = {
|
||||
stuckAtBottom: this.props.startAtBottom,
|
||||
};
|
||||
this._bottomGrowth = 0;
|
||||
this._pages = 0;
|
||||
this._scrollTimeout = new Timer(100);
|
||||
this._heightUpdateInProgress = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* jump to the top of the content.
|
||||
*/
|
||||
scrollToTop: function() {
|
||||
this._setScrollTop(0);
|
||||
this._getScrollNode().scrollTop = 0;
|
||||
this._saveScrollState();
|
||||
},
|
||||
|
||||
|
@ -466,24 +505,26 @@ module.exports = React.createClass({
|
|||
// saved is to do the scroll, then save the updated state. (Calculating
|
||||
// it ourselves is hard, and we can't rely on an onScroll callback
|
||||
// happening, since there may be no user-visible change here).
|
||||
this._setScrollTop(Number.MAX_VALUE);
|
||||
const sn = this._getScrollNode();
|
||||
sn.scrollTop = sn.scrollHeight;
|
||||
this._saveScrollState();
|
||||
},
|
||||
|
||||
/**
|
||||
* Page up/down.
|
||||
*
|
||||
* mult: -1 to page up, +1 to page down
|
||||
* @param {number} mult: -1 to page up, +1 to page down
|
||||
*/
|
||||
scrollRelative: function(mult) {
|
||||
const scrollNode = this._getScrollNode();
|
||||
const delta = mult * scrollNode.clientHeight * 0.5;
|
||||
this._setScrollTop(scrollNode.scrollTop + delta);
|
||||
scrollNode.scrollTop = scrollNode.scrollTop + delta;
|
||||
this._saveScrollState();
|
||||
},
|
||||
|
||||
/**
|
||||
* Scroll up/down in response to a scroll key
|
||||
* @param {object} ev the keyboard event
|
||||
*/
|
||||
handleScrollKey: function(ev) {
|
||||
switch (ev.keyCode) {
|
||||
|
@ -528,32 +569,156 @@ module.exports = React.createClass({
|
|||
pixelOffset = pixelOffset || 0;
|
||||
offsetBase = offsetBase || 0;
|
||||
|
||||
// convert pixelOffset so that it is based on the bottom of the
|
||||
// container.
|
||||
pixelOffset += this._getScrollNode().clientHeight * (1-offsetBase);
|
||||
|
||||
// save the desired scroll state. It's important we do this here rather
|
||||
// than as a result of the scroll event, because (a) we might not *get*
|
||||
// a scroll event, and (b) it might not currently be possible to set
|
||||
// the requested scroll state (eg, because we hit the end of the
|
||||
// timeline and need to do more pagination); we want to save the
|
||||
// *desired* scroll state rather than what we end up achieving.
|
||||
// set the trackedScrollToken so we can get the node through _getTrackedNode
|
||||
this.scrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedScrollToken: scrollToken,
|
||||
pixelOffset: pixelOffset,
|
||||
};
|
||||
|
||||
// ... then make it so.
|
||||
this._restoreSavedScrollState();
|
||||
const trackedNode = this._getTrackedNode();
|
||||
const scrollNode = this._getScrollNode();
|
||||
if (trackedNode) {
|
||||
// set the scrollTop to the position we want.
|
||||
// note though, that this might not succeed if the combination of offsetBase and pixelOffset
|
||||
// would position the trackedNode towards the top of the viewport.
|
||||
// This because when setting the scrollTop only 10 or so events might be loaded,
|
||||
// not giving enough content below the trackedNode to scroll downwards
|
||||
// enough so it ends up in the top of the viewport.
|
||||
debuglog("scrollToken: setting scrollTop", {offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop});
|
||||
scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset;
|
||||
this._saveScrollState();
|
||||
}
|
||||
},
|
||||
|
||||
// set the scrollTop attribute appropriately to position the given child at the
|
||||
// given offset in the window. A helper for _restoreSavedScrollState.
|
||||
_scrollToToken: function(scrollToken, pixelOffset) {
|
||||
/* find the dom node with the right scrolltoken */
|
||||
_saveScrollState: function() {
|
||||
if (this.props.stickyBottom && this.isAtBottom()) {
|
||||
this.scrollState = { stuckAtBottom: true };
|
||||
debuglog("saved stuckAtBottom state");
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollNode = this._getScrollNode();
|
||||
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
|
||||
|
||||
const itemlist = this.refs.itemlist;
|
||||
const messages = itemlist.children;
|
||||
let node = null;
|
||||
|
||||
// TODO: do a binary search here, as items are sorted by offsetTop
|
||||
// loop backwards, from bottom-most message (as that is the most common case)
|
||||
for (let i = messages.length-1; i >= 0; --i) {
|
||||
if (!messages[i].dataset.scrollTokens) {
|
||||
continue;
|
||||
}
|
||||
node = messages[i];
|
||||
// break at the first message (coming from the bottom)
|
||||
// that has it's offsetTop above the bottom of the viewport.
|
||||
if (this._topFromBottom(node) > viewportBottom) {
|
||||
// Use this node as the scrollToken
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
debuglog("unable to save scroll state: found no children in the viewport");
|
||||
return;
|
||||
}
|
||||
const scrollToken = node.dataset.scrollTokens.split(',')[0];
|
||||
debuglog("saving anchored scroll state to message", node && node.innerText, scrollToken);
|
||||
const bottomOffset = this._topFromBottom(node);
|
||||
this.scrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedNode: node,
|
||||
trackedScrollToken: scrollToken,
|
||||
bottomOffset: bottomOffset,
|
||||
pixelOffset: bottomOffset - viewportBottom, //needed for restoring the scroll position when coming back to the room
|
||||
};
|
||||
},
|
||||
|
||||
_restoreSavedScrollState: async function() {
|
||||
const scrollState = this.scrollState;
|
||||
|
||||
if (scrollState.stuckAtBottom) {
|
||||
const sn = this._getScrollNode();
|
||||
sn.scrollTop = sn.scrollHeight;
|
||||
} else if (scrollState.trackedScrollToken) {
|
||||
const itemlist = this.refs.itemlist;
|
||||
const trackedNode = this._getTrackedNode();
|
||||
if (trackedNode) {
|
||||
const newBottomOffset = this._topFromBottom(trackedNode);
|
||||
const bottomDiff = newBottomOffset - scrollState.bottomOffset;
|
||||
this._bottomGrowth += bottomDiff;
|
||||
scrollState.bottomOffset = newBottomOffset;
|
||||
itemlist.style.height = `${this._getListHeight()}px`;
|
||||
debuglog("balancing height because messages below viewport grew by", bottomDiff);
|
||||
}
|
||||
}
|
||||
if (!this._heightUpdateInProgress) {
|
||||
this._heightUpdateInProgress = true;
|
||||
try {
|
||||
await this._updateHeight();
|
||||
} finally {
|
||||
this._heightUpdateInProgress = false;
|
||||
}
|
||||
} else {
|
||||
debuglog("not updating height because request already in progress");
|
||||
}
|
||||
},
|
||||
// need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content?
|
||||
async _updateHeight() {
|
||||
// wait until user has stopped scrolling
|
||||
if (this._scrollTimeout.isRunning()) {
|
||||
debuglog("updateHeight waiting for scrolling to end ... ");
|
||||
await this._scrollTimeout.finished();
|
||||
} else {
|
||||
debuglog("updateHeight getting straight to business, no scrolling going on.");
|
||||
}
|
||||
|
||||
const sn = this._getScrollNode();
|
||||
const itemlist = this.refs.itemlist;
|
||||
const contentHeight = this._getMessagesHeight();
|
||||
const minHeight = sn.clientHeight;
|
||||
const height = Math.max(minHeight, contentHeight);
|
||||
this._pages = Math.ceil(height / PAGE_SIZE);
|
||||
this._bottomGrowth = 0;
|
||||
const newHeight = this._getListHeight();
|
||||
|
||||
const scrollState = this.scrollState;
|
||||
if (scrollState.stuckAtBottom) {
|
||||
itemlist.style.height = `${newHeight}px`;
|
||||
sn.scrollTop = sn.scrollHeight;
|
||||
debuglog("updateHeight to", newHeight);
|
||||
} else if (scrollState.trackedScrollToken) {
|
||||
const trackedNode = this._getTrackedNode();
|
||||
// if the timeline has been reloaded
|
||||
// this can be called before scrollToBottom or whatever has been called
|
||||
// so don't do anything if the node has disappeared from
|
||||
// the currently filled piece of the timeline
|
||||
if (trackedNode) {
|
||||
const oldTop = trackedNode.offsetTop;
|
||||
// changing the height might change the scrollTop
|
||||
// if the new height is smaller than the scrollTop.
|
||||
// We calculate the diff that needs to be applied
|
||||
// ourselves, so be sure to measure the
|
||||
// scrollTop before changing the height.
|
||||
const preexistingScrollTop = sn.scrollTop;
|
||||
itemlist.style.height = `${newHeight}px`;
|
||||
const newTop = trackedNode.offsetTop;
|
||||
const topDiff = newTop - oldTop;
|
||||
sn.scrollTop = preexistingScrollTop + topDiff;
|
||||
debuglog("updateHeight to", {newHeight, topDiff, preexistingScrollTop});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getTrackedNode() {
|
||||
const scrollState = this.scrollState;
|
||||
const trackedNode = scrollState.trackedNode;
|
||||
|
||||
if (!trackedNode || !trackedNode.parentElement) {
|
||||
let node;
|
||||
const messages = this.refs.itemlist.children;
|
||||
const scrollToken = scrollState.trackedScrollToken;
|
||||
|
||||
for (let i = messages.length-1; i >= 0; --i) {
|
||||
const m = messages[i];
|
||||
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
|
||||
|
@ -564,102 +729,33 @@ module.exports = React.createClass({
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (node) {
|
||||
debuglog("had to find tracked node again for " + scrollState.trackedScrollToken);
|
||||
}
|
||||
scrollState.trackedNode = node;
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
debuglog("ScrollPanel: No node with scrollToken '"+scrollToken+"'");
|
||||
if (!scrollState.trackedNode) {
|
||||
debuglog("No node with ; '"+scrollState.trackedScrollToken+"'");
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollNode = this._getScrollNode();
|
||||
const scrollTop = scrollNode.scrollTop;
|
||||
const viewportBottom = scrollTop + scrollNode.clientHeight;
|
||||
const nodeBottom = node.offsetTop + node.clientHeight;
|
||||
const intendedViewportBottom = nodeBottom + pixelOffset;
|
||||
const scrollDelta = intendedViewportBottom - viewportBottom;
|
||||
|
||||
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
|
||||
pixelOffset + " (delta: "+scrollDelta+")");
|
||||
|
||||
if (scrollDelta !== 0) {
|
||||
this._setScrollTop(scrollTop + scrollDelta);
|
||||
}
|
||||
return scrollState.trackedNode;
|
||||
},
|
||||
|
||||
_saveScrollState: function() {
|
||||
if (this.props.stickyBottom && this.isAtBottom()) {
|
||||
this.scrollState = { stuckAtBottom: true };
|
||||
debuglog("ScrollPanel: Saved scroll state", this.scrollState);
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollNode = this._getScrollNode();
|
||||
const viewportBottom = scrollNode.scrollTop + scrollNode.clientHeight;
|
||||
_getListHeight() {
|
||||
return this._bottomGrowth + (this._pages * PAGE_SIZE);
|
||||
},
|
||||
|
||||
_getMessagesHeight() {
|
||||
const itemlist = this.refs.itemlist;
|
||||
const messages = itemlist.children;
|
||||
let node = null;
|
||||
|
||||
// loop backwards, from bottom-most message (as that is the most common case)
|
||||
for (let i = messages.length-1; i >= 0; --i) {
|
||||
if (!messages[i].dataset.scrollTokens) {
|
||||
continue;
|
||||
}
|
||||
node = messages[i];
|
||||
// break at the first message (coming from the bottom)
|
||||
// that has it's offsetTop above the bottom of the viewport.
|
||||
if (node.offsetTop < viewportBottom) {
|
||||
// Use this node as the scrollToken
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeBottom = node.offsetTop + node.clientHeight;
|
||||
debuglog("ScrollPanel: saved scroll state", this.scrollState);
|
||||
this.scrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
|
||||
pixelOffset: viewportBottom - nodeBottom,
|
||||
};
|
||||
const lastNode = itemlist.lastElementChild;
|
||||
// 18 is itemlist padding
|
||||
return (lastNode.offsetTop + lastNode.clientHeight) - itemlist.firstElementChild.offsetTop + (18 * 2);
|
||||
},
|
||||
|
||||
_restoreSavedScrollState: function() {
|
||||
const scrollState = this.scrollState;
|
||||
|
||||
if (scrollState.stuckAtBottom) {
|
||||
this._setScrollTop(Number.MAX_VALUE);
|
||||
} else if (scrollState.trackedScrollToken) {
|
||||
this._scrollToToken(scrollState.trackedScrollToken,
|
||||
scrollState.pixelOffset);
|
||||
}
|
||||
},
|
||||
|
||||
_setScrollTop: function(scrollTop) {
|
||||
const scrollNode = this._getScrollNode();
|
||||
|
||||
const prevScroll = scrollNode.scrollTop;
|
||||
|
||||
// FF ignores attempts to set scrollTop to very large numbers
|
||||
scrollNode.scrollTop = Math.min(scrollTop, scrollNode.scrollHeight);
|
||||
|
||||
// If this change generates a scroll event, we should not update the
|
||||
// saved scroll state on it. See the comments in onScroll.
|
||||
//
|
||||
// If we *don't* expect a scroll event, we need to leave _lastSetScroll
|
||||
// alone, otherwise we'll end up ignoring a future scroll event which is
|
||||
// nothing to do with this change.
|
||||
|
||||
if (scrollNode.scrollTop != prevScroll) {
|
||||
this._lastSetScroll = scrollNode.scrollTop;
|
||||
}
|
||||
|
||||
debuglog("ScrollPanel: set scrollTop:", scrollNode.scrollTop,
|
||||
"requested:", scrollTop,
|
||||
"_lastSetScroll:", this._lastSetScroll);
|
||||
_topFromBottom(node) {
|
||||
return this.refs.itemlist.clientHeight - node.offsetTop;
|
||||
},
|
||||
|
||||
/* get the DOM node which has the scrollTop property we care about for our
|
||||
|
@ -672,71 +768,112 @@ module.exports = React.createClass({
|
|||
throw new Error("ScrollPanel._getScrollNode called when unmounted");
|
||||
}
|
||||
|
||||
if (!this._gemScroll) {
|
||||
if (!this._divScroll) {
|
||||
// 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();
|
||||
return this._divScroll;
|
||||
},
|
||||
|
||||
_collectGeminiScroll: function(gemScroll) {
|
||||
this._gemScroll = gemScroll;
|
||||
_collectScroll: function(divScroll) {
|
||||
this._divScroll = divScroll;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the current height as the min height for the message list
|
||||
* so the timeline cannot shrink. This is used to avoid
|
||||
* jumping when the typing indicator gets replaced by a smaller message.
|
||||
Mark the bottom offset of the last tile so we can balance it out when
|
||||
anything below it changes, by calling updatePreventShrinking, to keep
|
||||
the same minimum bottom offset, effectively preventing the timeline to shrink.
|
||||
*/
|
||||
blockShrinking: function() {
|
||||
preventShrinking: function() {
|
||||
const messageList = this.refs.itemlist;
|
||||
if (messageList) {
|
||||
const currentHeight = messageList.clientHeight;
|
||||
messageList.style.minHeight = `${currentHeight}px`;
|
||||
const tiles = messageList && messageList.children;
|
||||
if (!messageList) {
|
||||
return;
|
||||
}
|
||||
let lastTileNode;
|
||||
for (let i = tiles.length - 1; i >= 0; i--) {
|
||||
const node = tiles[i];
|
||||
if (node.dataset.scrollTokens) {
|
||||
lastTileNode = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!lastTileNode) {
|
||||
return;
|
||||
}
|
||||
this.clearPreventShrinking();
|
||||
const offsetFromBottom = messageList.clientHeight - (lastTileNode.offsetTop + lastTileNode.clientHeight);
|
||||
this.preventShrinkingState = {
|
||||
offsetFromBottom: offsetFromBottom,
|
||||
offsetNode: lastTileNode,
|
||||
};
|
||||
debuglog("prevent shrinking, last tile ", offsetFromBottom, "px from bottom");
|
||||
},
|
||||
|
||||
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
|
||||
clearPreventShrinking: function() {
|
||||
const messageList = this.refs.itemlist;
|
||||
const balanceElement = messageList && messageList.parentElement;
|
||||
if (balanceElement) balanceElement.style.paddingBottom = null;
|
||||
this.preventShrinkingState = null;
|
||||
debuglog("prevent shrinking cleared");
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the previously set min height
|
||||
update the container padding to balance
|
||||
the bottom offset of the last tile since
|
||||
preventShrinking was called.
|
||||
Clears the prevent-shrinking state ones the offset
|
||||
from the bottom of the marked tile grows larger than
|
||||
what it was when marking.
|
||||
*/
|
||||
clearBlockShrinking: function() {
|
||||
const messageList = this.refs.itemlist;
|
||||
if (messageList) {
|
||||
messageList.style.minHeight = null;
|
||||
}
|
||||
},
|
||||
|
||||
_checkBlockShrinking: function() {
|
||||
updatePreventShrinking: function() {
|
||||
if (this.preventShrinkingState) {
|
||||
const sn = this._getScrollNode();
|
||||
const scrollState = this.scrollState;
|
||||
if (!scrollState.stuckAtBottom) {
|
||||
const messageList = this.refs.itemlist;
|
||||
const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
|
||||
// element used to set paddingBottom to balance the typing notifs disappearing
|
||||
const balanceElement = messageList.parentElement;
|
||||
// if the offsetNode got unmounted, clear
|
||||
let shouldClear = !offsetNode.parentElement;
|
||||
// also if 200px from bottom
|
||||
if (!shouldClear && !scrollState.stuckAtBottom) {
|
||||
const spaceBelowViewport = sn.scrollHeight - (sn.scrollTop + sn.clientHeight);
|
||||
// only if we've scrolled up 200px from the bottom
|
||||
// should we clear the min-height used by the typing notifications,
|
||||
// otherwise we might still see it jump as the whitespace disappears
|
||||
// when scrolling up from the bottom
|
||||
if (spaceBelowViewport >= 200) {
|
||||
this.clearBlockShrinking();
|
||||
shouldClear = spaceBelowViewport >= 200;
|
||||
}
|
||||
// try updating if not clearing
|
||||
if (!shouldClear) {
|
||||
const currentOffset = messageList.clientHeight - (offsetNode.offsetTop + offsetNode.clientHeight);
|
||||
const offsetDiff = offsetFromBottom - currentOffset;
|
||||
if (offsetDiff > 0) {
|
||||
balanceElement.style.paddingBottom = `${offsetDiff}px`;
|
||||
debuglog("update prevent shrinking ", offsetDiff, "px from bottom");
|
||||
} else if (offsetDiff < 0) {
|
||||
shouldClear = true;
|
||||
}
|
||||
}
|
||||
if (shouldClear) {
|
||||
this.clearPreventShrinking();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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 (<GeminiScrollbarWrapper autoshow={true} wrappedRef={this._collectGeminiScroll}
|
||||
onScroll={this.onScroll} onResize={this.onResize}
|
||||
className={this.props.className} style={this.props.style}>
|
||||
return (<AutoHideScrollbar wrappedRef={this._collectScroll}
|
||||
onScroll={this.onScroll}
|
||||
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
|
||||
{ this.props.children }
|
||||
</ol>
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
</AutoHideScrollbar>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -39,7 +39,7 @@ const TagPanelButtons = React.createClass({
|
|||
if (payload.action === "show_redesign_feedback_dialog") {
|
||||
const RedesignFeedbackDialog =
|
||||
sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
|
||||
Modal.createDialog(RedesignFeedbackDialog);
|
||||
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
|
||||
}
|
||||
},
|
||||
|
||||
|
|