Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into travis/blurhash
Conflicts: package.json src/ContentMessages.tsx yarn.lock
This commit is contained in:
commit
660a849cdd
235 changed files with 10534 additions and 4471 deletions
20
.eslintrc.js
20
.eslintrc.js
|
@ -18,7 +18,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
overrides: [{
|
overrides: [{
|
||||||
"files": ["src/**/*.{ts,tsx}"],
|
"files": ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
|
||||||
"extends": ["matrix-org/ts"],
|
"extends": ["matrix-org/ts"],
|
||||||
"rules": {
|
"rules": {
|
||||||
// We're okay being explicit at the moment
|
// We're okay being explicit at the moment
|
||||||
|
@ -30,6 +30,24 @@ module.exports = {
|
||||||
|
|
||||||
"quotes": "off",
|
"quotes": "off",
|
||||||
"no-extra-boolean-cast": "off",
|
"no-extra-boolean-cast": "off",
|
||||||
|
"no-restricted-properties": [
|
||||||
|
"error",
|
||||||
|
...buildRestrictedPropertiesOptions(
|
||||||
|
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
|
||||||
|
"Use UIStore to access window dimensions instead",
|
||||||
|
),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function buildRestrictedPropertiesOptions(properties, message) {
|
||||||
|
return properties.map(prop => {
|
||||||
|
const [object, property] = prop.split(".");
|
||||||
|
return {
|
||||||
|
object,
|
||||||
|
property,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
27
.github/workflows/develop.yml
vendored
Normal file
27
.github/workflows/develop.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
name: Develop jobs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [develop]
|
||||||
|
jobs:
|
||||||
|
end-to-end:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: vectorim/element-web-ci-e2etests-env:latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: End-to-End tests
|
||||||
|
run: ./scripts/ci/end-to-end-tests.sh
|
||||||
|
- name: Archive logs
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
test/end-to-end-tests/logs/**/*
|
||||||
|
test/end-to-end-tests/synapse/installations/consent/homeserver.log
|
||||||
|
retention-days: 14
|
||||||
|
- name: Archive performance benchmark
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: performance-entries.json
|
||||||
|
path: test/end-to-end-tests/performance-entries.json
|
219
CHANGELOG.md
219
CHANGELOG.md
|
@ -1,3 +1,222 @@
|
||||||
|
Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0)
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 11.2.0
|
||||||
|
* [Release] Fix notif panel timestamp padding
|
||||||
|
[\#6158](https://github.com/matrix-org/matrix-react-sdk/pull/6158)
|
||||||
|
|
||||||
|
Changes in [3.23.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0-rc.1) (2021-06-01)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0...v3.23.0-rc.1)
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 11.2.0-rc.1
|
||||||
|
* Translations update from Weblate
|
||||||
|
[\#6128](https://github.com/matrix-org/matrix-react-sdk/pull/6128)
|
||||||
|
* Fix all DMs wrongly appearing in room list when `m.direct` is changed
|
||||||
|
[\#6122](https://github.com/matrix-org/matrix-react-sdk/pull/6122)
|
||||||
|
* Update way of checking for registration disabled
|
||||||
|
[\#6123](https://github.com/matrix-org/matrix-react-sdk/pull/6123)
|
||||||
|
* Fix the ability to remove avatar from a space via settings
|
||||||
|
[\#6126](https://github.com/matrix-org/matrix-react-sdk/pull/6126)
|
||||||
|
* Switch to stable endpoint/fields for MSC2858
|
||||||
|
[\#6125](https://github.com/matrix-org/matrix-react-sdk/pull/6125)
|
||||||
|
* Clear stored editor state when canceling editing using a shortcut
|
||||||
|
[\#6117](https://github.com/matrix-org/matrix-react-sdk/pull/6117)
|
||||||
|
* Respect newlines in space topics
|
||||||
|
[\#6124](https://github.com/matrix-org/matrix-react-sdk/pull/6124)
|
||||||
|
* Add url param `defaultUsername` to prefill the login username field
|
||||||
|
[\#5674](https://github.com/matrix-org/matrix-react-sdk/pull/5674)
|
||||||
|
* Bump ws from 7.4.2 to 7.4.6
|
||||||
|
[\#6115](https://github.com/matrix-org/matrix-react-sdk/pull/6115)
|
||||||
|
* Sticky headers repositioning without layout trashing
|
||||||
|
[\#6110](https://github.com/matrix-org/matrix-react-sdk/pull/6110)
|
||||||
|
* Handle user_busy in voip calls
|
||||||
|
[\#6112](https://github.com/matrix-org/matrix-react-sdk/pull/6112)
|
||||||
|
* Avoid showing warning modals from the invite dialog after it unmounts
|
||||||
|
[\#6105](https://github.com/matrix-org/matrix-react-sdk/pull/6105)
|
||||||
|
* Fix misleading child counts in spaces
|
||||||
|
[\#6109](https://github.com/matrix-org/matrix-react-sdk/pull/6109)
|
||||||
|
* Close creation menu when expanding space panel via expand hierarchy
|
||||||
|
[\#6090](https://github.com/matrix-org/matrix-react-sdk/pull/6090)
|
||||||
|
* Prevent having duplicates in pending room state
|
||||||
|
[\#6108](https://github.com/matrix-org/matrix-react-sdk/pull/6108)
|
||||||
|
* Update reactions row on event decryption
|
||||||
|
[\#6106](https://github.com/matrix-org/matrix-react-sdk/pull/6106)
|
||||||
|
* Destroy playback instance on voice message unmount
|
||||||
|
[\#6101](https://github.com/matrix-org/matrix-react-sdk/pull/6101)
|
||||||
|
* Fix message preview not up to date
|
||||||
|
[\#6102](https://github.com/matrix-org/matrix-react-sdk/pull/6102)
|
||||||
|
* Convert some Flow typed files to TS (round 2)
|
||||||
|
[\#6076](https://github.com/matrix-org/matrix-react-sdk/pull/6076)
|
||||||
|
* Remove unused middlePanelResized event listener
|
||||||
|
[\#6086](https://github.com/matrix-org/matrix-react-sdk/pull/6086)
|
||||||
|
* Fix accessing currentState on an invalid joinedRoom
|
||||||
|
[\#6100](https://github.com/matrix-org/matrix-react-sdk/pull/6100)
|
||||||
|
* Remove Promise allSettled polyfill as js-sdk uses it directly
|
||||||
|
[\#6097](https://github.com/matrix-org/matrix-react-sdk/pull/6097)
|
||||||
|
* Prevent DecoratedRoomAvatar to update its state for the same value
|
||||||
|
[\#6099](https://github.com/matrix-org/matrix-react-sdk/pull/6099)
|
||||||
|
* Skip generatePreview if event is not part of the live timeline
|
||||||
|
[\#6098](https://github.com/matrix-org/matrix-react-sdk/pull/6098)
|
||||||
|
* fix sticky headers when results num get displayed
|
||||||
|
[\#6095](https://github.com/matrix-org/matrix-react-sdk/pull/6095)
|
||||||
|
* Improve addEventsToTimeline performance scoping WhoIsTypingTile::setState
|
||||||
|
[\#6094](https://github.com/matrix-org/matrix-react-sdk/pull/6094)
|
||||||
|
* Safeguards to prevent layout trashing for window dimensions
|
||||||
|
[\#6092](https://github.com/matrix-org/matrix-react-sdk/pull/6092)
|
||||||
|
* Use local room state to render space hierarchy if the room is known
|
||||||
|
[\#6089](https://github.com/matrix-org/matrix-react-sdk/pull/6089)
|
||||||
|
* Add spinner in UserMenu to list pending long running actions
|
||||||
|
[\#6085](https://github.com/matrix-org/matrix-react-sdk/pull/6085)
|
||||||
|
* Stop overscroll in Firefox Nightly for macOS
|
||||||
|
[\#6093](https://github.com/matrix-org/matrix-react-sdk/pull/6093)
|
||||||
|
* Move SettingsStore watchers/monitors over to ES6 maps for performance
|
||||||
|
[\#6063](https://github.com/matrix-org/matrix-react-sdk/pull/6063)
|
||||||
|
* Bump libolm version.
|
||||||
|
[\#6080](https://github.com/matrix-org/matrix-react-sdk/pull/6080)
|
||||||
|
* Improve styling of the message action bar
|
||||||
|
[\#6066](https://github.com/matrix-org/matrix-react-sdk/pull/6066)
|
||||||
|
* Improve explore rooms when no results are found
|
||||||
|
[\#6070](https://github.com/matrix-org/matrix-react-sdk/pull/6070)
|
||||||
|
* Remove logo spinner
|
||||||
|
[\#6078](https://github.com/matrix-org/matrix-react-sdk/pull/6078)
|
||||||
|
* Fix add reaction prompt showing even when user is not joined to room
|
||||||
|
[\#6073](https://github.com/matrix-org/matrix-react-sdk/pull/6073)
|
||||||
|
* Vectorize spinners
|
||||||
|
[\#5680](https://github.com/matrix-org/matrix-react-sdk/pull/5680)
|
||||||
|
* Fix handling of via servers for suggested rooms
|
||||||
|
[\#6077](https://github.com/matrix-org/matrix-react-sdk/pull/6077)
|
||||||
|
* Upgrade showChatEffects to room-level setting exposure
|
||||||
|
[\#6075](https://github.com/matrix-org/matrix-react-sdk/pull/6075)
|
||||||
|
* Delete RoomView dead code
|
||||||
|
[\#6071](https://github.com/matrix-org/matrix-react-sdk/pull/6071)
|
||||||
|
* Reduce noise in tests
|
||||||
|
[\#6074](https://github.com/matrix-org/matrix-react-sdk/pull/6074)
|
||||||
|
* Fix room name issues in right panel summary card
|
||||||
|
[\#6069](https://github.com/matrix-org/matrix-react-sdk/pull/6069)
|
||||||
|
* Cache normalized room name
|
||||||
|
[\#6072](https://github.com/matrix-org/matrix-react-sdk/pull/6072)
|
||||||
|
* Update MemberList to reflect changes for invite permission change
|
||||||
|
[\#6061](https://github.com/matrix-org/matrix-react-sdk/pull/6061)
|
||||||
|
* Delete RoomView dead code
|
||||||
|
[\#6065](https://github.com/matrix-org/matrix-react-sdk/pull/6065)
|
||||||
|
* Show subspace rooms count even if it is 0 for consistency
|
||||||
|
[\#6067](https://github.com/matrix-org/matrix-react-sdk/pull/6067)
|
||||||
|
|
||||||
|
Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0)
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 11.1.0
|
||||||
|
* [Release] Bump libolm version
|
||||||
|
[\#6087](https://github.com/matrix-org/matrix-react-sdk/pull/6087)
|
||||||
|
|
||||||
|
Changes in [3.22.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0-rc.1) (2021-05-19)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0...v3.22.0-rc.1)
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 11.1.0-rc.1
|
||||||
|
* Translations update from Weblate
|
||||||
|
[\#6068](https://github.com/matrix-org/matrix-react-sdk/pull/6068)
|
||||||
|
* Show DMs in space for invited members too, to match Android impl
|
||||||
|
[\#6062](https://github.com/matrix-org/matrix-react-sdk/pull/6062)
|
||||||
|
* Support filtering by alias in add existing to space dialog
|
||||||
|
[\#6057](https://github.com/matrix-org/matrix-react-sdk/pull/6057)
|
||||||
|
* Fix issue when a room without a name or alias is marked as suggested
|
||||||
|
[\#6064](https://github.com/matrix-org/matrix-react-sdk/pull/6064)
|
||||||
|
* Fix space room hierarchy not updating when removing a room
|
||||||
|
[\#6055](https://github.com/matrix-org/matrix-react-sdk/pull/6055)
|
||||||
|
* Revert "Try putting room list handling behind a lock"
|
||||||
|
[\#6060](https://github.com/matrix-org/matrix-react-sdk/pull/6060)
|
||||||
|
* Stop assuming encrypted messages are decrypted ahead of time
|
||||||
|
[\#6052](https://github.com/matrix-org/matrix-react-sdk/pull/6052)
|
||||||
|
* Add error detail when languges fail to load
|
||||||
|
[\#6059](https://github.com/matrix-org/matrix-react-sdk/pull/6059)
|
||||||
|
* Add space invaders chat effect
|
||||||
|
[\#6053](https://github.com/matrix-org/matrix-react-sdk/pull/6053)
|
||||||
|
* Create SpaceProvider and hide Spaces from the RoomProvider autocompleter
|
||||||
|
[\#6051](https://github.com/matrix-org/matrix-react-sdk/pull/6051)
|
||||||
|
* Don't mark a room as unread when redacted event is present
|
||||||
|
[\#6049](https://github.com/matrix-org/matrix-react-sdk/pull/6049)
|
||||||
|
* Add support for MSC2873: Client information for Widgets
|
||||||
|
[\#6023](https://github.com/matrix-org/matrix-react-sdk/pull/6023)
|
||||||
|
* Support UI for MSC2762: Widgets reading events from rooms
|
||||||
|
[\#5960](https://github.com/matrix-org/matrix-react-sdk/pull/5960)
|
||||||
|
* Fix crash on opening notification panel
|
||||||
|
[\#6047](https://github.com/matrix-org/matrix-react-sdk/pull/6047)
|
||||||
|
* Remove custom LoggedInView::shouldComponentUpdate logic
|
||||||
|
[\#6046](https://github.com/matrix-org/matrix-react-sdk/pull/6046)
|
||||||
|
* Fix edge cases with the new add reactions prompt button
|
||||||
|
[\#6045](https://github.com/matrix-org/matrix-react-sdk/pull/6045)
|
||||||
|
* Add ids to homeserver and passphrase fields
|
||||||
|
[\#6043](https://github.com/matrix-org/matrix-react-sdk/pull/6043)
|
||||||
|
* Update space order field validity requirements to match msc update
|
||||||
|
[\#6042](https://github.com/matrix-org/matrix-react-sdk/pull/6042)
|
||||||
|
* Try putting room list handling behind a lock
|
||||||
|
[\#6024](https://github.com/matrix-org/matrix-react-sdk/pull/6024)
|
||||||
|
* Improve progress bar progression for smaller voice messages
|
||||||
|
[\#6035](https://github.com/matrix-org/matrix-react-sdk/pull/6035)
|
||||||
|
* Fix share space edge case where space is public but not invitable
|
||||||
|
[\#6039](https://github.com/matrix-org/matrix-react-sdk/pull/6039)
|
||||||
|
* Add missing 'rel' to image view download button
|
||||||
|
[\#6033](https://github.com/matrix-org/matrix-react-sdk/pull/6033)
|
||||||
|
* Improve visible waveform for voice messages
|
||||||
|
[\#6034](https://github.com/matrix-org/matrix-react-sdk/pull/6034)
|
||||||
|
* Fix roving tab index intercepting home/end in space create menu
|
||||||
|
[\#6040](https://github.com/matrix-org/matrix-react-sdk/pull/6040)
|
||||||
|
* Decorate room avatars with publicity in add existing to space flow
|
||||||
|
[\#6030](https://github.com/matrix-org/matrix-react-sdk/pull/6030)
|
||||||
|
* Improve Spaces "Just Me" wizard
|
||||||
|
[\#6025](https://github.com/matrix-org/matrix-react-sdk/pull/6025)
|
||||||
|
* Increase hover feedback on room sub list buttons
|
||||||
|
[\#6037](https://github.com/matrix-org/matrix-react-sdk/pull/6037)
|
||||||
|
* Show alternative button during space creation wizard if no rooms
|
||||||
|
[\#6029](https://github.com/matrix-org/matrix-react-sdk/pull/6029)
|
||||||
|
* Swap rotation buttons in the image viewer
|
||||||
|
[\#6032](https://github.com/matrix-org/matrix-react-sdk/pull/6032)
|
||||||
|
* Typo: initilisation -> initialisation
|
||||||
|
[\#5915](https://github.com/matrix-org/matrix-react-sdk/pull/5915)
|
||||||
|
* Save edited state of a message when switching rooms
|
||||||
|
[\#6001](https://github.com/matrix-org/matrix-react-sdk/pull/6001)
|
||||||
|
* Fix shield icon in Untrusted Device Dialog
|
||||||
|
[\#6022](https://github.com/matrix-org/matrix-react-sdk/pull/6022)
|
||||||
|
* Do not eagerly decrypt breadcrumb rooms
|
||||||
|
[\#6028](https://github.com/matrix-org/matrix-react-sdk/pull/6028)
|
||||||
|
* Update spaces.png
|
||||||
|
[\#6031](https://github.com/matrix-org/matrix-react-sdk/pull/6031)
|
||||||
|
* Encourage more diverse reactions to content
|
||||||
|
[\#6027](https://github.com/matrix-org/matrix-react-sdk/pull/6027)
|
||||||
|
* Wrap decodeURIComponent in try-catch to protect against malformed URIs
|
||||||
|
[\#6026](https://github.com/matrix-org/matrix-react-sdk/pull/6026)
|
||||||
|
* Iterate beta feedback dialog
|
||||||
|
[\#6021](https://github.com/matrix-org/matrix-react-sdk/pull/6021)
|
||||||
|
* Disable space fields whilst their form is busy
|
||||||
|
[\#6020](https://github.com/matrix-org/matrix-react-sdk/pull/6020)
|
||||||
|
* Add missing space on beta feedback dialog
|
||||||
|
[\#6018](https://github.com/matrix-org/matrix-react-sdk/pull/6018)
|
||||||
|
* Fix colours used for the back button in space create menu
|
||||||
|
[\#6017](https://github.com/matrix-org/matrix-react-sdk/pull/6017)
|
||||||
|
* Prioritise and reduce the amount of events decrypted on application startup
|
||||||
|
[\#5980](https://github.com/matrix-org/matrix-react-sdk/pull/5980)
|
||||||
|
* Linkify topics in space room directory results
|
||||||
|
[\#6015](https://github.com/matrix-org/matrix-react-sdk/pull/6015)
|
||||||
|
* Persistent space collapsed states
|
||||||
|
[\#5972](https://github.com/matrix-org/matrix-react-sdk/pull/5972)
|
||||||
|
* Catch another instance of unlabeled avatars.
|
||||||
|
[\#6010](https://github.com/matrix-org/matrix-react-sdk/pull/6010)
|
||||||
|
* Rescale and smooth voice message playback waveform to better match
|
||||||
|
expectation
|
||||||
|
[\#5996](https://github.com/matrix-org/matrix-react-sdk/pull/5996)
|
||||||
|
* Scale voice message clock with user's font size
|
||||||
|
[\#5993](https://github.com/matrix-org/matrix-react-sdk/pull/5993)
|
||||||
|
* Remove "in development" flag from voice messages
|
||||||
|
[\#5995](https://github.com/matrix-org/matrix-react-sdk/pull/5995)
|
||||||
|
* Support voice messages on Safari
|
||||||
|
[\#5989](https://github.com/matrix-org/matrix-react-sdk/pull/5989)
|
||||||
|
* Translations update from Weblate
|
||||||
|
[\#6011](https://github.com/matrix-org/matrix-react-sdk/pull/6011)
|
||||||
|
|
||||||
Changes in [3.21.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0) (2021-05-17)
|
Changes in [3.21.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0) (2021-05-17)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0-rc.1...v3.21.0)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0-rc.1...v3.21.0)
|
||||||
|
|
17
package.json
17
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.21.0",
|
"version": "3.23.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -55,7 +55,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"await-lock": "^2.1.0",
|
"await-lock": "^2.1.0",
|
||||||
"blueimp-canvas-to-blob": "^3.28.0",
|
|
||||||
"blurhash": "^1.1.3",
|
"blurhash": "^1.1.3",
|
||||||
"browser-encrypt-attachment": "^0.3.0",
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
"browser-request": "^0.3.3",
|
"browser-request": "^0.3.3",
|
||||||
|
@ -89,18 +88,16 @@
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"qs": "^6.9.6",
|
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "^16.14.0",
|
"react": "^17.0.2",
|
||||||
"react-beautiful-dnd": "^4.0.1",
|
"react-beautiful-dnd": "^4.0.1",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^17.0.2",
|
||||||
"react-focus-lock": "^2.5.0",
|
"react-focus-lock": "^2.5.0",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"rfc4648": "^1.4.0",
|
"rfc4648": "^1.4.0",
|
||||||
"sanitize-html": "^2.3.2",
|
"sanitize-html": "^2.3.2",
|
||||||
"tar-js": "^0.3.0",
|
"tar-js": "^0.3.0",
|
||||||
"text-encoding-utf-8": "^1.0.2",
|
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
"what-input": "^5.2.10",
|
"what-input": "^5.2.10",
|
||||||
"zxcvbn": "^4.4.2"
|
"zxcvbn": "^4.4.2"
|
||||||
|
@ -122,6 +119,7 @@
|
||||||
"@babel/preset-typescript": "^7.12.7",
|
"@babel/preset-typescript": "^7.12.7",
|
||||||
"@babel/register": "^7.12.10",
|
"@babel/register": "^7.12.10",
|
||||||
"@babel/traverse": "^7.12.12",
|
"@babel/traverse": "^7.12.12",
|
||||||
|
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||||
"@peculiar/webcrypto": "^1.1.4",
|
"@peculiar/webcrypto": "^1.1.4",
|
||||||
"@sinonjs/fake-timers": "^7.0.2",
|
"@sinonjs/fake-timers": "^7.0.2",
|
||||||
"@types/classnames": "^2.2.11",
|
"@types/classnames": "^2.2.11",
|
||||||
|
@ -147,7 +145,7 @@
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
"concurrently": "^5.3.0",
|
"concurrently": "^5.3.0",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-adapter-react-16": "^1.15.6",
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
||||||
"eslint": "7.18.0",
|
"eslint": "7.18.0",
|
||||||
"eslint-config-matrix-org": "^0.2.0",
|
"eslint-config-matrix-org": "^0.2.0",
|
||||||
"eslint-plugin-babel": "^5.3.1",
|
"eslint-plugin-babel": "^5.3.1",
|
||||||
|
@ -160,10 +158,9 @@
|
||||||
"jest-environment-jsdom-sixteen": "^1.0.3",
|
"jest-environment-jsdom-sixteen": "^1.0.3",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"matrix-mock-request": "^1.2.3",
|
"matrix-mock-request": "^1.2.3",
|
||||||
"matrix-react-test-utils": "^0.2.2",
|
"matrix-react-test-utils": "^0.2.3",
|
||||||
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
|
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
|
||||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
"react-test-renderer": "^17.0.2",
|
||||||
"react-test-renderer": "^16.14.0",
|
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"stylelint": "^13.9.0",
|
"stylelint": "^13.9.0",
|
||||||
"stylelint-config-standard": "^20.0.0",
|
"stylelint-config-standard": "^20.0.0",
|
||||||
|
|
|
@ -45,6 +45,8 @@ html {
|
||||||
N.B. Breaks things when we have legitimate horizontal overscroll */
|
N.B. Breaks things when we have legitimate horizontal overscroll */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
// Stop similar overscroll bounce in Firefox Nightly for macOS
|
||||||
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -289,6 +291,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
||||||
|
|
||||||
.mx_Dialog_staticWrapper .mx_Dialog {
|
.mx_Dialog_staticWrapper .mx_Dialog {
|
||||||
z-index: 4010;
|
z-index: 4010;
|
||||||
|
contain: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog_background {
|
.mx_Dialog_background {
|
||||||
|
|
|
@ -76,6 +76,7 @@
|
||||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||||
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
||||||
@import "./views/dialogs/_FeedbackDialog.scss";
|
@import "./views/dialogs/_FeedbackDialog.scss";
|
||||||
|
@import "./views/dialogs/_ForwardDialog.scss";
|
||||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||||
@import "./views/dialogs/_HostSignupDialog.scss";
|
@import "./views/dialogs/_HostSignupDialog.scss";
|
||||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||||
|
@ -179,6 +180,7 @@
|
||||||
@import "./views/messages/_common_CryptoEvent.scss";
|
@import "./views/messages/_common_CryptoEvent.scss";
|
||||||
@import "./views/right_panel/_BaseCard.scss";
|
@import "./views/right_panel/_BaseCard.scss";
|
||||||
@import "./views/right_panel/_EncryptionInfo.scss";
|
@import "./views/right_panel/_EncryptionInfo.scss";
|
||||||
|
@import "./views/right_panel/_PinnedMessagesCard.scss";
|
||||||
@import "./views/right_panel/_RoomSummaryCard.scss";
|
@import "./views/right_panel/_RoomSummaryCard.scss";
|
||||||
@import "./views/right_panel/_UserInfo.scss";
|
@import "./views/right_panel/_UserInfo.scss";
|
||||||
@import "./views/right_panel/_VerificationPanel.scss";
|
@import "./views/right_panel/_VerificationPanel.scss";
|
||||||
|
@ -203,7 +205,6 @@
|
||||||
@import "./views/rooms/_NewRoomIntro.scss";
|
@import "./views/rooms/_NewRoomIntro.scss";
|
||||||
@import "./views/rooms/_NotificationBadge.scss";
|
@import "./views/rooms/_NotificationBadge.scss";
|
||||||
@import "./views/rooms/_PinnedEventTile.scss";
|
@import "./views/rooms/_PinnedEventTile.scss";
|
||||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
|
||||||
@import "./views/rooms/_PresenceLabel.scss";
|
@import "./views/rooms/_PresenceLabel.scss";
|
||||||
@import "./views/rooms/_ReplyPreview.scss";
|
@import "./views/rooms/_ReplyPreview.scss";
|
||||||
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
||||||
|
|
|
@ -38,6 +38,7 @@ limitations under the License.
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
z-index: 5001;
|
z-index: 5001;
|
||||||
|
contain: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu_right {
|
.mx_ContextualMenu_right {
|
||||||
|
@ -115,8 +116,3 @@ limitations under the License.
|
||||||
border-top: 8px solid $menu-bg-color;
|
border-top: 8px solid $menu-bg-color;
|
||||||
border-right: 8px solid transparent;
|
border-right: 8px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu_spinner {
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ $roomListCollapsedWidth: 68px;
|
||||||
|
|
||||||
// Create a row-based flexbox for the GroupFilterPanel and the room list
|
// Create a row-based flexbox for the GroupFilterPanel and the room list
|
||||||
display: flex;
|
display: flex;
|
||||||
|
contain: content;
|
||||||
|
|
||||||
.mx_LeftPanel_GroupFilterPanelContainer {
|
.mx_LeftPanel_GroupFilterPanelContainer {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
@ -70,6 +71,7 @@ $roomListCollapsedWidth: 68px;
|
||||||
// aligned correctly. This is also a row-based flexbox.
|
// aligned correctly. This is also a row-based flexbox.
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
contain: content;
|
||||||
|
|
||||||
&.mx_IndicatorScrollbar_leftOverflow {
|
&.mx_IndicatorScrollbar_leftOverflow {
|
||||||
mask-image: linear-gradient(90deg, transparent, black 5%);
|
mask-image: linear-gradient(90deg, transparent, black 5%);
|
||||||
|
|
|
@ -82,7 +82,6 @@ limitations under the License.
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
display: inline;
|
display: inline;
|
||||||
padding-left: 0px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NotificationPanel .mx_EventTile_senderDetails {
|
.mx_NotificationPanel .mx_EventTile_senderDetails {
|
||||||
|
@ -103,6 +102,7 @@ limitations under the License.
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
position: initial;
|
position: initial;
|
||||||
display: inline;
|
display: inline;
|
||||||
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NotificationPanel .mx_EventTile_line {
|
.mx_NotificationPanel .mx_EventTile_line {
|
||||||
|
|
|
@ -25,6 +25,7 @@ limitations under the License.
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
contain: strict;
|
||||||
|
|
||||||
.mx_RoomView_MessageList {
|
.mx_RoomView_MessageList {
|
||||||
padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above
|
padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above
|
||||||
|
@ -98,6 +99,48 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$dot-size: 8px;
|
||||||
|
$pulse-color: $pinned-unread-color;
|
||||||
|
|
||||||
|
.mx_RightPanel_pinnedMessagesButton {
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RightPanel_pinnedMessagesButton_unreadIndicator {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
margin: 4px;
|
||||||
|
width: $dot-size;
|
||||||
|
height: $dot-size;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: scale(1);
|
||||||
|
background: rgba($pulse-color, 1);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
|
||||||
|
animation: mx_RightPanel_indicator_pulse 2s infinite;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mx_RightPanel_indicator_pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 10px rgba($pulse-color, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RightPanel_headerButton_highlight {
|
.mx_RightPanel_headerButton_highlight {
|
||||||
&::before {
|
&::before {
|
||||||
background-color: $accent-color !important;
|
background-color: $accent-color !important;
|
||||||
|
|
|
@ -61,6 +61,39 @@ limitations under the License.
|
||||||
.mx_RoomDirectory_tableWrapper {
|
.mx_RoomDirectory_tableWrapper {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
|
|
||||||
|
.mx_RoomDirectory_footer {
|
||||||
|
margin-top: 24px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-18px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 40px auto 60px;
|
||||||
|
font-size: $font-14px;
|
||||||
|
line-height: $font-20px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
max-width: 464px; // easier reading
|
||||||
|
}
|
||||||
|
|
||||||
|
> hr {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
height: 1px;
|
||||||
|
background-color: $header-panel-bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomDirectory_newRoom {
|
||||||
|
margin: 24px auto 0;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomDirectory_table {
|
.mx_RoomDirectory_table {
|
||||||
|
@ -138,11 +171,6 @@ limitations under the License.
|
||||||
color: $settings-grey-fg-color;
|
color: $settings-grey-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomDirectory_table tr {
|
|
||||||
padding-bottom: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomDirectory .mx_RoomView_MessageList {
|
.mx_RoomDirectory .mx_RoomView_MessageList {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,6 +152,7 @@ limitations under the License.
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
contain: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomView_statusArea {
|
.mx_RoomView_statusArea {
|
||||||
|
@ -237,6 +238,7 @@ hr.mx_RoomView_myReadMarker {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
will-change: width;
|
||||||
transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s;
|
transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s;
|
||||||
width: 99%;
|
width: 99%;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
|
@ -21,5 +21,8 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 50px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -328,6 +328,8 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
> hr {
|
> hr {
|
||||||
|
@ -364,6 +366,45 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_betaWarning {
|
||||||
|
padding: 12px 12px 12px 54px;
|
||||||
|
position: relative;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
width: 432px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $info-plinth-bg-color;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
content: '';
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
left: 14px;
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomView_inviteTeammates {
|
.mx_SpaceRoomView_inviteTeammates {
|
||||||
// XXX remove this when spaces leaves Beta
|
// XXX remove this when spaces leaves Beta
|
||||||
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
|
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_DecoratedRoomAvatar, .mx_ExtraTile {
|
.mx_DecoratedRoomAvatar, .mx_ExtraTile {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
contain: content;
|
||||||
|
|
||||||
&.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar {
|
&.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar {
|
||||||
mask-image: url('$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg');
|
mask-image: url('$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg');
|
||||||
|
|
159
res/css/views/dialogs/_ForwardDialog.scss
Normal file
159
res/css/views/dialogs/_ForwardDialog.scss
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Robin Townsend <robin@robin.town>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT 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_ForwardDialog {
|
||||||
|
width: 520px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
min-height: 0;
|
||||||
|
height: 80vh;
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
font-size: $font-12px;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
line-height: $font-15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .mx_ForwardDialog_preview {
|
||||||
|
max-height: 30%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
|
div {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_msgOption {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When forwarding messages from encrypted rooms, EventTile will complain
|
||||||
|
// that our preview is unencrypted, which doesn't actually matter
|
||||||
|
.mx_EventTile_e2eIcon_unencrypted {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We also hide download links to not encourage users to try interacting
|
||||||
|
.mx_MFileBody_download {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> hr {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid $input-border-color;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .mx_ForwardList {
|
||||||
|
display: contents;
|
||||||
|
|
||||||
|
.mx_SearchBox {
|
||||||
|
// To match the space around the title
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_noResults {
|
||||||
|
display: block;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_results {
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_entry {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 32px;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $groupFilterPanel-bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_roomButton {
|
||||||
|
display: flex;
|
||||||
|
margin-right: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.mx_DecoratedRoomAvatar {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_entry_name {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_sendButton {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:not(.mx_ForwardList_canSend) .mx_ForwardList_sendLabel {
|
||||||
|
// Hide the "Send" label while preserving button size
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_sendIcon, .mx_NotificationBadge {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NotificationBadge {
|
||||||
|
// Match the failed to send indicator's color with the disabled button
|
||||||
|
background-color: $button-danger-disabled-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_ForwardList_sending .mx_ForwardList_sendIcon {
|
||||||
|
background-color: $button-primary-bg-color;
|
||||||
|
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_ForwardList_sent .mx_ForwardList_sendIcon {
|
||||||
|
background-color: $button-primary-bg-color;
|
||||||
|
mask-image: url('$(res)/img/element-icons/circle-sent.svg');
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,9 @@ limitations under the License.
|
||||||
.mx_InviteDialog_addressBar {
|
.mx_InviteDialog_addressBar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
// Right margin for the design. We could apply this to the whole dialog, but then the scrollbar
|
||||||
|
// for the user section gets weird.
|
||||||
|
margin: 8px 45px 0 0;
|
||||||
|
|
||||||
.mx_InviteDialog_editor {
|
.mx_InviteDialog_editor {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -73,7 +76,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_InviteDialog_section {
|
.mx_InviteDialog_section {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 4px;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
|
@ -82,6 +85,14 @@ limitations under the License.
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_InviteDialog_subname {
|
.mx_InviteDialog_subname {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
margin-top: -10px; // HACK: Positioning with margins is bad
|
margin-top: -10px; // HACK: Positioning with margins is bad
|
||||||
|
@ -90,6 +101,63 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_section_hidden_suggestions_disclaimer {
|
||||||
|
padding: 8px 0 16px 0;
|
||||||
|
font-size: $font-14px;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
color: $primary-fg-color;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_footer {
|
||||||
|
border-top: 1px solid $input-border-color;
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
margin: 12px 0;
|
||||||
|
font-size: $font-12px;
|
||||||
|
color: $muted-fg-color;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_footer_link {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: solid 1px $light-fg-color;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
> a {
|
||||||
|
text-decoration: none;
|
||||||
|
flex-shrink: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_footer_link_copy {
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 20px;
|
||||||
|
display: inherit;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
mask-image: url($copy-button-url);
|
||||||
|
background-color: $message-action-bar-fg-color;
|
||||||
|
margin-left: 5px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_InviteDialog_roomTile {
|
.mx_InviteDialog_roomTile {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
|
@ -142,6 +210,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_InviteDialog_roomTile_nameStack {
|
.mx_InviteDialog_roomTile_nameStack {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_InviteDialog_roomTile_name {
|
.mx_InviteDialog_roomTile_name {
|
||||||
|
@ -157,6 +226,13 @@ limitations under the License.
|
||||||
margin-left: 7px;
|
margin-left: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_roomTile_name,
|
||||||
|
.mx_InviteDialog_roomTile_userId {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_InviteDialog_roomTile_time {
|
.mx_InviteDialog_roomTile_time {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
|
@ -212,22 +288,29 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_InviteDialog {
|
.mx_InviteDialog {
|
||||||
// Prevent the dialog from jumping around randomly when elements change.
|
// Prevent the dialog from jumping around randomly when elements change.
|
||||||
height: 590px;
|
height: 600px;
|
||||||
padding-left: 20px; // the design wants some padding on the left
|
padding-left: 20px; // the design wants some padding on the left
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.mx_InviteDialog_content {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_InviteDialog_userSections {
|
.mx_InviteDialog_userSections {
|
||||||
margin-top: 10px;
|
margin-top: 4px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-right: 45px;
|
padding: 0 45px 4px 0;
|
||||||
height: 455px; // mx_InviteDialog's height minus some for the upper elements
|
height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right margin for the design. We could apply this to the whole dialog, but then the scrollbar
|
.mx_InviteDialog_hasFooter .mx_InviteDialog_userSections {
|
||||||
// for the user section gets weird.
|
height: calc(100% - 175px);
|
||||||
.mx_InviteDialog_helpText,
|
}
|
||||||
.mx_InviteDialog_addressBar {
|
|
||||||
margin-right: 45px;
|
.mx_InviteDialog_helpText {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
|
.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
|
||||||
|
|
|
@ -50,7 +50,8 @@ limitations under the License.
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
display: inherit;
|
display: inherit;
|
||||||
}
|
}
|
||||||
.mx_ShareDialog_matrixto_copy > div {
|
.mx_ShareDialog_matrixto_copy::after {
|
||||||
|
content: "";
|
||||||
mask-image: url($copy-button-url);
|
mask-image: url($copy-button-url);
|
||||||
background-color: $message-action-bar-fg-color;
|
background-color: $message-action-bar-fg-color;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
|
|
@ -22,6 +22,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ImageView_image_wrapper {
|
.mx_ImageView_image_wrapper {
|
||||||
|
pointer-events: initial;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -30,7 +31,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ImageView_image {
|
.mx_ImageView_image {
|
||||||
pointer-events: all;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ImageView_info_wrapper {
|
.mx_ImageView_info_wrapper {
|
||||||
pointer-events: all;
|
pointer-events: initial;
|
||||||
padding-left: 32px;
|
padding-left: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -63,7 +63,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_ImageView_toolbar {
|
.mx_ImageView_toolbar {
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
pointer-events: all;
|
pointer-events: initial;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,11 @@ limitations under the License.
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_InlineSpinner_spin img {
|
.mx_InlineSpinner img, .mx_InlineSpinner_icon {
|
||||||
margin: 0px 6px;
|
margin: 0px 6px;
|
||||||
vertical-align: -3px;
|
vertical-align: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_InlineSpinner_icon {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
|
@ -28,8 +28,7 @@ limitations under the License.
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before, &::after {
|
.mx_MiniAvatarUploader_indicator {
|
||||||
content: '';
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
height: 26px;
|
height: 26px;
|
||||||
|
@ -37,27 +36,22 @@ limitations under the License.
|
||||||
|
|
||||||
right: -6px;
|
right: -6px;
|
||||||
bottom: -6px;
|
bottom: -6px;
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
.mx_MiniAvatarUploader_cameraIcon {
|
||||||
background-color: $secondary-fg-color;
|
height: 100%;
|
||||||
mask-position: center;
|
width: 100%;
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-image: url('$(res)/img/element-icons/camera.svg');
|
|
||||||
mask-size: 16px;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_MiniAvatarUploader_busy::after {
|
background-color: $secondary-fg-color;
|
||||||
background: url("$(res)/img/spinner.gif") no-repeat center;
|
mask-position: center;
|
||||||
background-size: 80%;
|
mask-repeat: no-repeat;
|
||||||
mask: unset;
|
mask-image: url('$(res)/img/element-icons/camera.svg');
|
||||||
|
mask-size: 16px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,3 +26,19 @@ limitations under the License.
|
||||||
.mx_MatrixChat_middlePanel .mx_Spinner {
|
.mx_MatrixChat_middlePanel .mx_Spinner {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotateZ(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotateZ(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Spinner_icon {
|
||||||
|
background-color: $primary-fg-color;
|
||||||
|
mask: url('$(res)/img/spinner.svg');
|
||||||
|
mask-size: contain;
|
||||||
|
animation: 1.1s steps(12, end) infinite spin;
|
||||||
|
}
|
||||||
|
|
|
@ -20,11 +20,12 @@ limitations under the License.
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 24px;
|
height: 32px;
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
background: $message-action-bar-bg-color;
|
background: $primary-bg-color;
|
||||||
top: -26px;
|
border: 1px solid $input-border-color;
|
||||||
|
top: -32px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
// Ensure the action bar appears above over things, like the read marker.
|
// Ensure the action bar appears above over things, like the read marker.
|
||||||
|
@ -51,31 +52,19 @@ limitations under the License.
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid $message-action-bar-border-color;
|
margin: 2px;
|
||||||
margin-left: -1px;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: $message-action-bar-hover-border-color;
|
background: $roomlist-button-bg-color;
|
||||||
|
border-radius: 6px;
|
||||||
z-index: 1;
|
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 {
|
.mx_MessageActionBar_maskButton {
|
||||||
width: 27px;
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageActionBar_maskButton::after {
|
.mx_MessageActionBar_maskButton::after {
|
||||||
|
@ -88,7 +77,11 @@ limitations under the License.
|
||||||
mask-size: 18px;
|
mask-size: 18px;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
background-color: $message-action-bar-fg-color;
|
background-color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar_maskButton:hover::after {
|
||||||
|
background-color: $primary-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageActionBar_reactButton::after {
|
.mx_MessageActionBar_reactButton::after {
|
||||||
|
|
|
@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_SenderProfile_name {
|
.mx_SenderProfile_displayName {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SenderProfile_mxid {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-left: 5px;
|
||||||
|
opacity: 0.5; // Match mx_TextualEvent
|
||||||
|
}
|
||||||
|
|
90
res/css/views/right_panel/_PinnedMessagesCard.scss
Normal file
90
res/css/views/right_panel/_PinnedMessagesCard.scss
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT 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_PinnedMessagesCard {
|
||||||
|
padding-top: 0;
|
||||||
|
|
||||||
|
.mx_BaseCard_header {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0;
|
||||||
|
border-bottom: 1px solid $menu-border-color;
|
||||||
|
|
||||||
|
> h2 {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-18px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BaseCard_close {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_PinnedMessagesCard_empty {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
height: max-content;
|
||||||
|
text-align: center;
|
||||||
|
margin: auto 40px;
|
||||||
|
|
||||||
|
.mx_PinnedMessagesCard_MessageActionBar {
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
height: 32px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: $primary-bg-color;
|
||||||
|
border: 1px solid $input-border-color;
|
||||||
|
padding: 1px;
|
||||||
|
width: max-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.mx_MessageActionBar_maskButton {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar_optionsButton {
|
||||||
|
background: $roomlist-button-bg-color;
|
||||||
|
border-radius: 6px;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> h2 {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ limitations under the License.
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSummaryCard_avatar {
|
.mx_RoomSummaryCard_avatar {
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$left-gutter: 64px;
|
$left-gutter: 64px;
|
||||||
|
$hover-select-border: 4px;
|
||||||
|
|
||||||
.mx_EventTile {
|
.mx_EventTile {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -85,12 +86,11 @@ $left-gutter: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_isEditing .mx_MessageTimestamp {
|
.mx_EventTile_isEditing .mx_MessageTimestamp {
|
||||||
visibility: hidden !important;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile .mx_MessageTimestamp {
|
.mx_EventTile .mx_MessageTimestamp {
|
||||||
display: block;
|
display: block;
|
||||||
visibility: hidden;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -104,7 +104,7 @@ $left-gutter: 64px;
|
||||||
.mx_EventTile_line, .mx_EventTile_reply {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: $left-gutter;
|
padding-left: $left-gutter;
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomView_timeline_rr_enabled,
|
.mx_RoomView_timeline_rr_enabled,
|
||||||
|
@ -142,27 +142,8 @@ $left-gutter: 64px;
|
||||||
line-height: 57px !important;
|
line-height: 57px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessagePanel_alwaysShowTimestamps .mx_MessageTimestamp {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
|
.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
|
||||||
left: 3px;
|
left: calc(-$hover-select-border);
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
|
||||||
// The first set is to handle the 'group layout' (default) and the second for the IRC layout
|
|
||||||
.mx_EventTile_last > div > a > .mx_MessageTimestamp,
|
|
||||||
.mx_EventTile:hover > div > a > .mx_MessageTimestamp,
|
|
||||||
.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp,
|
|
||||||
.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_ReplyThread .mx_EventTile > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_MessageActionBar,
|
.mx_EventTile:hover .mx_MessageActionBar,
|
||||||
|
@ -177,7 +158,7 @@ $left-gutter: 64px;
|
||||||
*/
|
*/
|
||||||
.mx_EventTile_selected > .mx_EventTile_line {
|
.mx_EventTile_selected > .mx_EventTile_line {
|
||||||
border-left: $accent-color 4px solid;
|
border-left: $accent-color 4px solid;
|
||||||
padding-left: 60px;
|
padding-left: calc($left-gutter - $hover-select-border);
|
||||||
background-color: $event-selected-color;
|
background-color: $event-selected-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,8 +171,12 @@ $left-gutter: 64px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_info .mx_EventTile_line {
|
||||||
|
padding-left: calc($left-gutter + 18px);
|
||||||
|
}
|
||||||
|
|
||||||
.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
|
.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
|
||||||
padding-left: 78px;
|
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_EventTile_line,
|
.mx_EventTile:hover .mx_EventTile_line,
|
||||||
|
@ -280,6 +265,7 @@ $left-gutter: 64px;
|
||||||
height: $font-14px;
|
height: $font-14px;
|
||||||
width: $font-14px;
|
width: $font-14px;
|
||||||
|
|
||||||
|
will-change: left, top;
|
||||||
transition:
|
transition:
|
||||||
left var(--transition-short) ease-out,
|
left var(--transition-short) ease-out,
|
||||||
top var(--transition-standard) ease-out;
|
top var(--transition-standard) ease-out;
|
||||||
|
@ -426,7 +412,7 @@ $left-gutter: 64px;
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
|
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
|
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
|
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
|
||||||
padding-left: 60px;
|
padding-left: calc($left-gutter - $hover-select-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
|
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
|
||||||
|
@ -444,7 +430,7 @@ $left-gutter: 64px;
|
||||||
.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
|
.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
|
.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
|
.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
|
||||||
padding-left: 78px;
|
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* End to end encryption stuff */
|
/* End to end encryption stuff */
|
||||||
|
@ -456,7 +442,7 @@ $left-gutter: 64px;
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
|
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
|
||||||
width: $MessageTimestamp_width_hover;
|
left: calc(-$hover-select-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||||
|
|
|
@ -24,10 +24,6 @@ $left-gutter: 64px;
|
||||||
margin-left: $left-gutter;
|
margin-left: $left-gutter;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .mx_EventTile_line {
|
|
||||||
padding-left: $left-gutter;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .mx_EventTile_avatar {
|
> .mx_EventTile_avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
@ -43,10 +39,6 @@ $left-gutter: 64px;
|
||||||
line-height: $font-22px;
|
line-height: $font-22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_info .mx_EventTile_line {
|
|
||||||
padding-left: calc($left-gutter + 18px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compact layout overrides */
|
/* Compact layout overrides */
|
||||||
|
|
|
@ -115,8 +115,7 @@ $irc-line-height: $font-18px;
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
.mx_EventTile_e2eIcon,
|
.mx_EventTile_e2eIcon,
|
||||||
.mx_TextualEvent,
|
.mx_TextualEvent,
|
||||||
.mx_MTextBody,
|
.mx_MTextBody {
|
||||||
.mx_ReplyThread_wrapper_empty {
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,16 +176,13 @@ $irc-line-height: $font-18px;
|
||||||
.mx_SenderProfile_hover {
|
.mx_SenderProfile_hover {
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
> span {
|
> .mx_SenderProfile_displayName {
|
||||||
display: flex;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
> .mx_SenderProfile_name {
|
min-width: var(--name-width);
|
||||||
overflow: hidden;
|
text-align: end;
|
||||||
text-overflow: ellipsis;
|
|
||||||
min-width: var(--name-width);
|
|
||||||
text-align: end;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,7 +207,7 @@ $irc-line-height: $font-18px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
> .mx_SenderProfile_name {
|
> .mx_SenderProfile_displayName {
|
||||||
min-width: inherit;
|
min-width: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_JumpToBottomButton_scrollDown {
|
.mx_JumpToBottomButton_scrollDown {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: block;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: 19px;
|
border-radius: 19px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
|
@ -18,8 +18,8 @@ limitations under the License.
|
||||||
margin: 40px 0 48px 64px;
|
margin: 40px 0 48px 64px;
|
||||||
|
|
||||||
.mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) {
|
.mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) {
|
||||||
&::before, &::after {
|
.mx_MiniAvatarUploader_indicator {
|
||||||
content: unset;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,62 +16,91 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_PinnedEventTile {
|
.mx_PinnedEventTile {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
margin-bottom: 5px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 5px; // for the hover
|
padding: 0 4px 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile:hover {
|
display: grid;
|
||||||
background-color: $event-selected-color;
|
grid-template-areas:
|
||||||
}
|
"avatar name remove"
|
||||||
|
"content content content"
|
||||||
|
"footer footer footer";
|
||||||
|
grid-template-rows: max-content auto max-content;
|
||||||
|
grid-template-columns: 24px auto 24px;
|
||||||
|
grid-row-gap: 12px;
|
||||||
|
grid-column-gap: 8px;
|
||||||
|
|
||||||
.mx_PinnedEventTile .mx_PinnedEventTile_sender,
|
& + .mx_PinnedEventTile {
|
||||||
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
|
padding: 12px 4px;
|
||||||
color: #868686;
|
border-top: 1px solid $menu-border-color;
|
||||||
font-size: 0.8em;
|
}
|
||||||
vertical-align: top;
|
|
||||||
display: inline-block;
|
|
||||||
padding-bottom: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
|
.mx_PinnedEventTile_senderAvatar {
|
||||||
padding-left: 15px;
|
grid-area: avatar;
|
||||||
display: none;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar {
|
.mx_PinnedEventTile_sender {
|
||||||
float: left;
|
grid-area: name;
|
||||||
margin-right: 10px;
|
font-weight: $font-semi-bold;
|
||||||
}
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_PinnedEventTile_actions {
|
.mx_PinnedEventTile_unpinButton {
|
||||||
float: right;
|
visibility: hidden;
|
||||||
margin-right: 10px;
|
grid-area: remove;
|
||||||
display: none;
|
position: relative;
|
||||||
}
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp {
|
&:hover {
|
||||||
display: inline-block;
|
background-color: $roomheader-addroom-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions {
|
&::before {
|
||||||
display: block;
|
content: "";
|
||||||
}
|
position: absolute;
|
||||||
|
//top: 0;
|
||||||
|
//left: 0;
|
||||||
|
height: inherit;
|
||||||
|
width: inherit;
|
||||||
|
background: $secondary-fg-color;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: 8px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-image: url('$(res)/img/image-view/close.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_PinnedEventTile_unpinButton {
|
.mx_PinnedEventTile_message {
|
||||||
display: inline-block;
|
grid-area: content;
|
||||||
cursor: pointer;
|
}
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile_gotoButton {
|
.mx_PinnedEventTile_footer {
|
||||||
display: inline-block;
|
grid-area: footer;
|
||||||
font-size: 0.7em; // Smaller text to avoid conflicting with the layout
|
font-size: 10px;
|
||||||
}
|
line-height: 12px;
|
||||||
|
|
||||||
.mx_PinnedEventTile_message {
|
.mx_PinnedEventTile_timestamp {
|
||||||
margin-left: 50px;
|
font-size: inherit;
|
||||||
position: relative;
|
line-height: inherit;
|
||||||
top: 0;
|
color: $secondary-fg-color;
|
||||||
left: 0;
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 12px;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.mx_PinnedEventTile_unpinButton {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 Travis Ralston
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_PinnedEventsPanel {
|
|
||||||
border-top: 1px solid $primary-hairline-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventsPanel_body {
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventsPanel_header {
|
|
||||||
margin: 0;
|
|
||||||
padding-top: 8px;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventsPanel_cancel {
|
|
||||||
margin: 12px;
|
|
||||||
float: right;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
|
@ -32,14 +32,14 @@ limitations under the License.
|
||||||
// first triggering the enter state with the newest breadcrumb off screen (-40px) then
|
// first triggering the enter state with the newest breadcrumb off screen (-40px) then
|
||||||
// sliding it into view.
|
// sliding it into view.
|
||||||
&.mx_RoomBreadcrumbs-enter {
|
&.mx_RoomBreadcrumbs-enter {
|
||||||
margin-left: -40px; // 32px for the avatar, 8px for the margin
|
transform: translateX(-40px); // 32px for the avatar, 8px for the margin
|
||||||
}
|
}
|
||||||
&.mx_RoomBreadcrumbs-enter-active {
|
&.mx_RoomBreadcrumbs-enter-active {
|
||||||
margin-left: 0;
|
transform: translateX(0);
|
||||||
|
|
||||||
// Timing function is as-requested by design.
|
// Timing function is as-requested by design.
|
||||||
// NOTE: The transition time MUST match the value passed to CSSTransition!
|
// NOTE: The transition time MUST match the value passed to CSSTransition!
|
||||||
transition: margin-left 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
|
transition: transform 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomBreadcrumbs_placeholder {
|
.mx_RoomBreadcrumbs_placeholder {
|
||||||
|
|
|
@ -277,24 +277,6 @@ limitations under the License.
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader_pinnedButton::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_pinsIndicator {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 4px;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: $pinned-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_pinsIndicatorUnread {
|
|
||||||
background-color: $pinned-unread-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
.mx_RoomHeader_wrapper {
|
.mx_RoomHeader_wrapper {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -61,8 +61,8 @@ limitations under the License.
|
||||||
&.mx_RoomSublist_headerContainer_sticky {
|
&.mx_RoomSublist_headerContainer_sticky {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 32px; // to match the header container
|
height: 32px; // to match the header container
|
||||||
// width set by JS
|
// width set by JS because of a compat issue between Firefox and Chrome
|
||||||
width: calc(100% - 22px);
|
width: calc(100% - 15px);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't have a top style because the top is dependent on the room list header's
|
// We don't have a top style because the top is dependent on the room list header's
|
||||||
|
@ -198,6 +198,7 @@ limitations under the License.
|
||||||
// as the box model should be top aligned. Happens in both FF and Chromium
|
// as the box model should be top aligned. Happens in both FF and Chromium
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-self: stretch;
|
||||||
|
|
||||||
mask-image: linear-gradient(0deg, transparent, black 4px);
|
mask-image: linear-gradient(0deg, transparent, black 4px);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,10 @@ limitations under the License.
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
|
||||||
|
contain: content; // Not strict as it will break when resizing a sublist vertically
|
||||||
|
height: 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
// The tile is also a flexbox row itself
|
// The tile is also a flexbox row itself
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M18.5151 20.0831L15.6941 17.2621L17.2621 15.6941L20.0831 18.5151C21.5741 20.0061 22.1529 21.7793 21.9661 21.9661C21.7793 22.1529 20.0061 21.5741 18.5151 20.0831Z" fill="black"/>
|
<path d="M18.5151 20.0831L15.6941 17.2621L17.2621 15.6941L20.0831 18.5151C21.5741 20.0061 22.1529 21.7793 21.9661 21.9661C21.7793 22.1529 20.0061 21.5741 18.5151 20.0831Z" fill="#737D8C"/>
|
||||||
<path d="M7.46196 11.3821C7.07677 11.5059 5.49073 12.0989 3.63366 12.0744C1.77658 12.0499 1.67795 10.8941 2.46811 10.1039L6.28598 6.28602L9.42196 9.42203L7.46196 11.3821Z" fill="black"/>
|
<path d="M7.46196 11.3821C7.07677 11.5059 5.49073 12.0989 3.63366 12.0744C1.77658 12.0499 1.67795 10.8941 2.46811 10.1039L6.28598 6.28602L9.42196 9.42203L7.46196 11.3821Z" fill="#737D8C"/>
|
||||||
<path d="M11.3821 7.46202C11.5059 7.07682 12.0989 5.49077 12.0744 3.63368C12.0499 1.77658 10.8941 1.67795 10.1039 2.46812L6.28598 6.28602L9.42196 9.42203L11.3821 7.46202Z" fill="black"/>
|
<path d="M11.3821 7.46202C11.5059 7.07682 12.0989 5.49077 12.0744 3.63368C12.0499 1.77658 10.8941 1.67795 10.1039 2.46812L6.28598 6.28602L9.42196 9.42203L11.3821 7.46202Z" fill="#737D8C"/>
|
||||||
<path d="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" fill="black"/>
|
<path d="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" fill="#737D8C"/>
|
||||||
<path d="M11.774 11.774C9.31114 14.2369 8.61779 17.7115 9.83827 20.3213C10.3104 21.3308 11.6288 21.3273 12.4169 20.5392L20.5391 12.4169C21.3271 11.6289 21.3307 10.3104 20.3212 9.83829C17.7114 8.61779 14.2369 9.31115 11.774 11.774Z" fill="black"/>
|
<path d="M11.774 11.774C9.31114 14.2369 8.61779 17.7115 9.83827 20.3213C10.3104 21.3308 11.6288 21.3273 12.4169 20.5392L20.5391 12.4169C21.3271 11.6289 21.3307 10.3104 20.3212 9.83829C17.7114 8.61779 14.2369 9.31115 11.774 11.774Z" fill="#737D8C"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1,015 B After Width: | Height: | Size: 1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 34 KiB |
|
@ -1,141 +1,96 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink" preserveAspectRatio="none" viewBox="0 0 375 375" style="background-color:#FFFFFF00; overflow:visible">
|
<svg
|
||||||
<title>start</title>
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
<!-- Layers -->
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
<!-- Layer: Icon -->
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
<svg x="188" y="187" width="0.01" height="0.01" style ="overflow:visible" opacity="1">
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="188;187.75;187.5"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="187;187.25;187.5"/>
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
<g transform="scale(1 1)">
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
<g transform="rotate(0)">
|
width="128"
|
||||||
<animateTransform attributeName="transform" calcMode="spline" dur="2" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1"
|
height="128"
|
||||||
repeatCount="indefinite" type="rotate" values="0;180;360"/>
|
viewBox="0 0 33.866666 33.866668"
|
||||||
<svg x="-100" y="-100" width="200" height="200" style ="overflow:visible" opacity="1">
|
version="1.1"
|
||||||
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-100;-117.5;-100"/>
|
id="svg920"
|
||||||
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-100;-117.5;-100"/>
|
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
|
||||||
<animate attributeName="width" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="200;235;200"/>
|
sodipodi:docname="spinner.svg">
|
||||||
<animate attributeName="height" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 1 1;0 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="200;235;200"/>
|
<defs
|
||||||
<g clip-path="">
|
id="defs914" />
|
||||||
<g filter="">
|
<metadata
|
||||||
<!-- Layer: 1024@2x -->
|
id="metadata917">
|
||||||
<svg x="100" y="100" width="0.01" height="0.01" style ="overflow:visible" opacity="1">
|
<rdf:RDF>
|
||||||
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="100;117.5;100"/>
|
<cc:Work
|
||||||
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="100;117.5;100"/>
|
rdf:about="">
|
||||||
<g transform="scale(1 1)">
|
<dc:format>image/svg+xml</dc:format>
|
||||||
<g transform="rotate(0)">
|
<dc:type
|
||||||
<svg x="-100" y="-100" width="200" height="200" style ="overflow:visible" opacity="1">
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-100;-117.5;-100"/>
|
<dc:title />
|
||||||
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-100;-117.5;-100"/>
|
</cc:Work>
|
||||||
<animate attributeName="width" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="200;235;200"/>
|
</rdf:RDF>
|
||||||
<animate attributeName="height" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="200;235;200"/>
|
</metadata>
|
||||||
<g clip-path="">
|
<g
|
||||||
<g filter="">
|
inkscape:label="Layer 1"
|
||||||
<!-- Layer: Path -->
|
inkscape:groupmode="layer"
|
||||||
<svg x="118" y="46" width="0.01" height="0.01" style ="overflow:visible" opacity="1">
|
id="layer1">
|
||||||
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="118;138.65;118"/>
|
<path
|
||||||
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="46;54.05;46"/>
|
style="stroke-width:0;fill-opacity:0.30000001"
|
||||||
<g transform="scale(1 1)">
|
d="M 59,95.605469 V 123 c 0,2.77 2.23,5 5,5 2.77,0 5,-2.23 5,-5 V 95.605469 A 31.999998,31.999998 0 0 1 64,96 31.999998,31.999998 0 0 1 59,95.605469 Z"
|
||||||
<g transform="rotate(0)">
|
transform="scale(0.26458333)"
|
||||||
<svg x="-46" y="-46" width="92" height="92" style ="overflow:visible" opacity="1">
|
id="path2350" />
|
||||||
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
|
<path
|
||||||
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
|
style="stroke-width:0;fill-opacity:0.7020452"
|
||||||
<animate attributeName="width" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
|
d="M 64,0 C 61.23,0 59,2.2300001 59,5 V 32.394531 A 31.999998,31.999998 0 0 1 64,32 31.999998,31.999998 0 0 1 69,32.394531 V 5 C 69,2.2300001 66.77,0 64,0 Z"
|
||||||
<animate attributeName="height" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
|
transform="scale(0.26458333)"
|
||||||
<g clip-path="">
|
id="rect2283" />
|
||||||
<g filter="">
|
<path
|
||||||
<path d="M0,12c0,-6.627,5.373,-12,12,-12 44.183,0,80,35.817,80,80 0,6.627,-5.373,12,-12,12 -6.627,0,-12,-5.373,-12,-12 0,-30.928,-25.072,-56,-56,-56 -6.627,0,-12,-5.373,-12,-12zM0,12" fill="#0DBD8B" id="path" stroke="#00000000" stroke-dasharray="0" stroke-dashoffset="0" stroke-miterlimit="10" stroke-width="0">
|
style="stroke-width:0;fill-opacity:0.30000001"
|
||||||
<animate attributeName="d" calcMode="spline" dur="2s" fill="freeze" href="#path" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="M0,12c0,-6.627,5.373,-12,12,-12 44.183,0,80,35.817,80,80 0,6.627,-5.373,12,-12,12 -6.627,0,-12,-5.373,-12,-12 0,-30.928,-25.072,-56,-56,-56 -6.627,0,-12,-5.373,-12,-12zM0,12;M0,14.1c0,-7.787,6.313,-14.1,14.1,-14.1 51.915,0,94,42.085,94,94 0,7.787,-6.313,14.1,-14.1,14.1 -7.787,0,-14.1,-6.313,-14.1,-14.1 0,-36.34,-29.46,-65.8,-65.8,-65.8 -7.787,0,-14.1,-6.313,-14.1,-14.1zM0,14.1;M0,12c0,-6.627,5.373,-12,12,-12 44.183,0,80,35.817,80,80 0,6.627,-5.373,12,-12,12 -6.627,0,-12,-5.373,-12,-12 0,-30.928,-25.072,-56,-56,-56 -6.627,0,-12,-5.373,-12,-12zM0,12"/>
|
d="M 43.867188,88.871094 30.169922,112.5957 c -1.385,2.39889 -0.568812,5.44508 1.830078,6.83008 2.39889,1.385 5.445078,0.56881 6.830078,-1.83008 L 52.527344,93.873047 a 31.999998,31.999998 0 0 1 -8.660156,-5.001953 z"
|
||||||
</path>
|
transform="scale(0.26458333)"
|
||||||
|
id="path2346" />
|
||||||
</g>
|
<path
|
||||||
</g>
|
style="stroke-width:0;fill-opacity:0.80019373"
|
||||||
</svg>
|
d="m 93.150391,7.9121094 c -1.599848,0.111837 -3.114844,0.992881 -3.980469,2.4921876 L 75.472656,34.126953 a 31.999998,31.999998 0 0 1 8.660156,5.001953 L 97.830078,15.404297 C 99.215078,13.005407 98.39889,9.9592187 96,8.5742188 95.100416,8.0548438 94.110299,7.8450072 93.150391,7.9121094 Z"
|
||||||
</g>
|
transform="scale(0.26458333)"
|
||||||
</g>
|
id="rect2285" />
|
||||||
</svg>
|
<path
|
||||||
<!-- Layer: Path -->
|
style="stroke-width:0;fill-opacity:0.30000001"
|
||||||
<svg x="82" y="154" width="0.01" height="0.01" style ="overflow:visible" opacity="1">
|
d="M 34.126953,75.474609 10.404297,89.169922 C 8.0054066,90.554922 7.1892188,93.60111 8.5742188,96 c 1.3849999,2.39889 4.4311882,3.215078 6.8300782,1.830078 L 39.128906,84.132812 a 31.999998,31.999998 0 0 1 -5.001953,-8.658203 z"
|
||||||
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="82;96.35;82"/>
|
transform="scale(0.26458333)"
|
||||||
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="154;180.95;154"/>
|
id="path2342" />
|
||||||
<g transform="scale(1 1)">
|
<path
|
||||||
<g transform="rotate(0)">
|
style="stroke-width:0;fill-opacity:0.90226436"
|
||||||
<svg x="-46" y="-46" width="92" height="92" style ="overflow:visible" opacity="1">
|
d="m 115.44531,29.507812 c -0.95991,-0.0671 -1.95002,0.142735 -2.84961,0.66211 L 88.871094,43.867188 a 31.999998,31.999998 0 0 1 5.001953,8.658203 L 117.5957,38.830078 c 2.39889,-1.385 3.21508,-4.431188 1.83008,-6.830078 -0.86562,-1.499306 -2.38062,-2.38035 -3.98047,-2.492188 z"
|
||||||
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
|
transform="scale(0.26458333)"
|
||||||
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
|
id="rect2287" />
|
||||||
<animate attributeName="width" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
|
<path
|
||||||
<animate attributeName="height" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
|
style="stroke-width:0;fill-opacity:1"
|
||||||
<g clip-path="">
|
d="M 95.605469,59 A 31.999998,31.999998 0 0 1 96,64 31.999998,31.999998 0 0 1 95.605469,69 H 123 c 2.77,0 5,-2.23 5,-5 0,-2.77 -2.23,-5 -5,-5 z"
|
||||||
<g filter="">
|
transform="scale(0.26458333)"
|
||||||
<path d="M92,80c0,6.627,-5.373,12,-12,12 -44.183,0,-80,-35.817,-80,-80 0,-6.627,5.373,-12,12,-12 6.627,0,12,5.373,12,12 0,30.928,25.072,56,56,56 6.627,0,12,5.373,12,12zM92,80" fill="#0DBD8B" id="path_1" stroke="#00000000" stroke-dasharray="0" stroke-dashoffset="0" stroke-miterlimit="10" stroke-width="0">
|
id="path2338" />
|
||||||
<animate attributeName="d" calcMode="spline" dur="2s" fill="freeze" href="#path_1" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="M92,80c0,6.627,-5.373,12,-12,12 -44.183,0,-80,-35.817,-80,-80 0,-6.627,5.373,-12,12,-12 6.627,0,12,5.373,12,12 0,30.928,25.072,56,56,56 6.627,0,12,5.373,12,12zM92,80;M108.1,94c0,7.787,-6.313,14.1,-14.1,14.1 -51.915,0,-94,-42.085,-94,-94 0,-7.787,6.313,-14.1,14.1,-14.1 7.787,0,14.1,6.313,14.1,14.1 0,36.34,29.46,65.8,65.8,65.8 7.787,0,14.1,6.313,14.1,14.1zM108.1,94;M92,80c0,6.627,-5.373,12,-12,12 -44.183,0,-80,-35.817,-80,-80 0,-6.627,5.373,-12,12,-12 6.627,0,12,5.373,12,12 0,30.928,25.072,56,56,56 6.627,0,12,5.373,12,12zM92,80"/>
|
<path
|
||||||
</path>
|
style="stroke-width:0;fill-opacity:0.40288368"
|
||||||
|
d="m 5,59 c -2.7699999,0 -5,2.23 -5,5 0,2.77 2.2300001,5 5,5 H 32.394531 A 31.999998,31.999998 0 0 1 32,64 31.999998,31.999998 0 0 1 32.394531,59 Z"
|
||||||
</g>
|
transform="scale(0.26458333)"
|
||||||
</g>
|
id="rect2289" />
|
||||||
</svg>
|
<path
|
||||||
</g>
|
style="stroke-width:0;fill-opacity:0.30000001"
|
||||||
</g>
|
d="m 93.873047,75.472656 a 31.999998,31.999998 0 0 1 -5.001953,8.660156 L 112.5957,97.830078 c 2.39889,1.385 5.44508,0.568812 6.83008,-1.830078 1.385,-2.39889 0.56881,-5.445078 -1.83008,-6.830078 z"
|
||||||
</svg>
|
transform="scale(0.26458333)"
|
||||||
<!-- Layer: Path -->
|
id="path2334" />
|
||||||
<svg x="46" y="82" width="0.01" height="0.01" style ="overflow:visible" opacity="1">
|
<path
|
||||||
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="46;54.05;46"/>
|
style="stroke-width:0;fill-opacity:0.49898377"
|
||||||
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="82;96.35;82"/>
|
d="M 12.554688,29.507812 C 10.95484,29.61965 9.4398437,30.500694 8.5742188,32 c -1.385,2.39889 -0.5688122,5.445078 1.8300782,6.830078 l 23.722656,13.697266 a 31.999998,31.999998 0 0 1 5.001953,-8.660156 L 15.404297,30.169922 c -0.899584,-0.519375 -1.889701,-0.729212 -2.849609,-0.66211 z"
|
||||||
<g transform="scale(1 1)">
|
transform="scale(0.26458333)"
|
||||||
<g transform="rotate(0)">
|
id="rect2291" />
|
||||||
<svg x="-46" y="-46" width="92" height="92" style ="overflow:visible" opacity="1">
|
<path
|
||||||
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
|
style="stroke-width:0;fill-opacity:0.30000001"
|
||||||
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
|
d="m 84.132812,88.871094 a 31.999998,31.999998 0 0 1 -8.658203,5.001953 L 89.169922,117.5957 c 1.385,2.39889 4.431188,3.21508 6.830078,1.83008 2.39889,-1.385 3.215078,-4.43119 1.830078,-6.83008 z"
|
||||||
<animate attributeName="width" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
|
transform="scale(0.26458333)"
|
||||||
<animate attributeName="height" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
|
id="path2330" />
|
||||||
<g clip-path="">
|
<path
|
||||||
<g filter="">
|
style="stroke-width:0;fill-opacity:0.5998317"
|
||||||
<path d="M12,92c-6.627,0,-12,-5.373,-12,-12 0,-44.183,35.817,-80,80,-80 6.627,0,12,5.373,12,12 0,6.627,-5.373,12,-12,12 -30.928,0,-56,25.072,-56,56 0,6.627,-5.373,12,-12,12zM12,92" fill="#0DBD8B" id="path_2" stroke="#00000000" stroke-dasharray="0" stroke-dashoffset="0" stroke-miterlimit="10" stroke-width="0">
|
d="M 34.849609,7.9121094 C 33.889701,7.8450072 32.899584,8.0548438 32,8.5742188 29.60111,9.9592187 28.784922,13.005407 30.169922,15.404297 l 13.697266,23.724609 a 31.999998,31.999998 0 0 1 8.658203,-5.001953 L 38.830078,10.404297 C 37.964453,8.9049904 36.449457,8.0239464 34.849609,7.9121094 Z"
|
||||||
<animate attributeName="d" calcMode="spline" dur="2s" fill="freeze" href="#path_2" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="M12,92c-6.627,0,-12,-5.373,-12,-12 0,-44.183,35.817,-80,80,-80 6.627,0,12,5.373,12,12 0,6.627,-5.373,12,-12,12 -30.928,0,-56,25.072,-56,56 0,6.627,-5.373,12,-12,12zM12,92;M14.1,108.1c-7.787,0,-14.1,-6.313,-14.1,-14.1 0,-51.915,42.085,-94,94,-94 7.787,0,14.1,6.313,14.1,14.1 0,7.787,-6.313,14.1,-14.1,14.1 -36.34,0,-65.8,29.46,-65.8,65.8 0,7.787,-6.313,14.1,-14.1,14.1zM14.1,108.1;M12,92c-6.627,0,-12,-5.373,-12,-12 0,-44.183,35.817,-80,80,-80 6.627,0,12,5.373,12,12 0,6.627,-5.373,12,-12,12 -30.928,0,-56,25.072,-56,56 0,6.627,-5.373,12,-12,12zM12,92"/>
|
transform="scale(0.26458333)"
|
||||||
</path>
|
id="rect2293" />
|
||||||
|
</g>
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<!-- Layer: Path -->
|
|
||||||
<svg x="154" y="118" width="0.01" height="0.01" style ="overflow:visible" opacity="1">
|
|
||||||
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="154;180.95;154"/>
|
|
||||||
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="118;138.65;118"/>
|
|
||||||
<g transform="scale(1 1)">
|
|
||||||
<g transform="rotate(0)">
|
|
||||||
<svg x="-46" y="-46" width="92" height="92" style ="overflow:visible" opacity="1">
|
|
||||||
<animate attributeName="x" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
|
|
||||||
<animate attributeName="y" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="-46;-54.05;-46"/>
|
|
||||||
<animate attributeName="width" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
|
|
||||||
<animate attributeName="height" calcMode="spline" dur="2s" fill="freeze" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="92;108.1;92"/>
|
|
||||||
<g clip-path="">
|
|
||||||
<g filter="">
|
|
||||||
<path d="M80,0c6.627,0,12,5.373,12,12 0,44.183,-35.817,80,-80,80 -6.627,0,-12,-5.373,-12,-12 0,-6.627,5.373,-12,12,-12 30.928,0,56,-25.072,56,-56 0,-6.627,5.373,-12,12,-12zM80,0" fill="#0DBD8B" id="path_3" stroke="#00000000" stroke-dasharray="0" stroke-dashoffset="0" stroke-miterlimit="10" stroke-width="0">
|
|
||||||
<animate attributeName="d" calcMode="spline" dur="2s" fill="freeze" href="#path_3" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" keyTimes="0;0.5;1" repeatCount="indefinite" values="M80,0c6.627,0,12,5.373,12,12 0,44.183,-35.817,80,-80,80 -6.627,0,-12,-5.373,-12,-12 0,-6.627,5.373,-12,12,-12 30.928,0,56,-25.072,56,-56 0,-6.627,5.373,-12,12,-12zM80,0;M94,0c7.787,0,14.1,6.313,14.1,14.1 0,51.915,-42.085,94,-94,94 -7.787,0,-14.1,-6.313,-14.1,-14.1 0,-7.787,6.313,-14.1,14.1,-14.1 36.34,0,65.8,-29.46,65.8,-65.8 0,-7.787,6.313,-14.1,14.1,-14.1zM94,0;M80,0c6.627,0,12,5.373,12,12 0,44.183,-35.817,80,-80,80 -6.627,0,-12,-5.373,-12,-12 0,-6.627,5.373,-12,12,-12 30.928,0,56,-25.072,56,-56 0,-6.627,5.373,-12,12,-12zM80,0"/>
|
|
||||||
</path>
|
|
||||||
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 5.1 KiB |
5
src/@types/global.d.ts
vendored
5
src/@types/global.d.ts
vendored
|
@ -42,6 +42,8 @@ import {SpaceStoreClass} from "../stores/SpaceStore";
|
||||||
import TypingStore from "../stores/TypingStore";
|
import TypingStore from "../stores/TypingStore";
|
||||||
import { EventIndexPeg } from "../indexing/EventIndexPeg";
|
import { EventIndexPeg } from "../indexing/EventIndexPeg";
|
||||||
import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
|
import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
|
||||||
|
import PerformanceMonitor from "../performance";
|
||||||
|
import UIStore from "../stores/UIStore";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -79,6 +81,9 @@ declare global {
|
||||||
mxVoiceRecordingStore: VoiceRecordingStore;
|
mxVoiceRecordingStore: VoiceRecordingStore;
|
||||||
mxTypingStore: TypingStore;
|
mxTypingStore: TypingStore;
|
||||||
mxEventIndexPeg: EventIndexPeg;
|
mxEventIndexPeg: EventIndexPeg;
|
||||||
|
mxPerformanceMonitor: PerformanceMonitor;
|
||||||
|
mxPerformanceEntryNames: any;
|
||||||
|
mxUIStore: UIStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,52 +14,61 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { ComponentType } from "react";
|
||||||
|
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
import { IDialogProps } from "./components/views/dialogs/IDialogProps";
|
||||||
|
|
||||||
|
type AsyncImport<T> = { default: T };
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
// A promise which resolves with the real component
|
||||||
|
prom: Promise<ComponentType | AsyncImport<ComponentType>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
component?: ComponentType;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrap an asynchronous loader function with a react component which shows a
|
* Wrap an asynchronous loader function with a react component which shows a
|
||||||
* spinner until the real component loads.
|
* spinner until the real component loads.
|
||||||
*/
|
*/
|
||||||
export default class AsyncWrapper extends React.Component {
|
export default class AsyncWrapper extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private unmounted = false;
|
||||||
/** A promise which resolves with the real component
|
|
||||||
*/
|
|
||||||
prom: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
public state = {
|
||||||
component: null,
|
component: null,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._unmounted = false;
|
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/element-web/issues/3148
|
// https://github.com/vector-im/element-web/issues/3148
|
||||||
console.log('Starting load of AsyncWrapper for modal');
|
console.log('Starting load of AsyncWrapper for modal');
|
||||||
this.props.prom.then((result) => {
|
this.props.prom.then((result) => {
|
||||||
if (this._unmounted) {
|
if (this.unmounted) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Take the 'default' member if it's there, then we support
|
// Take the 'default' member if it's there, then we support
|
||||||
// passing in just an import()ed module, since ES6 async import
|
// passing in just an import()ed module, since ES6 async import
|
||||||
// always returns a module *namespace*.
|
// always returns a module *namespace*.
|
||||||
const component = result.default ? result.default : result;
|
const component = (result as AsyncImport<ComponentType>).default
|
||||||
this.setState({component});
|
? (result as AsyncImport<ComponentType>).default
|
||||||
|
: result as ComponentType;
|
||||||
|
this.setState({ component });
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
console.warn('AsyncWrapper promise failed', e);
|
console.warn('AsyncWrapper promise failed', e);
|
||||||
this.setState({error: e});
|
this.setState({ error: e });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWrapperCancelClick = () => {
|
private onWrapperCancelClick = () => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,12 +79,10 @@ export default class AsyncWrapper extends React.Component {
|
||||||
} else if (this.state.error) {
|
} else if (this.state.error) {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <BaseDialog onFinished={this.props.onFinished}
|
return <BaseDialog onFinished={this.props.onFinished} title={_t("Error")}>
|
||||||
title={_t("Error")}
|
{ _t("Unable to load! Check your network connectivity and try again.") }
|
||||||
>
|
|
||||||
{_t("Unable to load! Check your network connectivity and try again.")}
|
|
||||||
<DialogButtons primaryButton={_t("Dismiss")}
|
<DialogButtons primaryButton={_t("Dismiss")}
|
||||||
onPrimaryButtonClick={this._onWrapperCancelClick}
|
onPrimaryButtonClick={this.onWrapperCancelClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
/>
|
/>
|
||||||
</BaseDialog>;
|
</BaseDialog>;
|
|
@ -264,7 +264,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSupportsVirtualRooms() {
|
public getSupportsVirtualRooms() {
|
||||||
return this.supportsPstnProtocol;
|
return this.supportsSipNativeVirtual;
|
||||||
}
|
}
|
||||||
|
|
||||||
public pstnLookup(phoneNumber: string): Promise<ThirdpartyLookupResponse[]> {
|
public pstnLookup(phoneNumber: string): Promise<ThirdpartyLookupResponse[]> {
|
||||||
|
@ -462,6 +462,9 @@ export default class CallHandler extends EventEmitter {
|
||||||
if (call.hangupReason === CallErrorCode.UserHangup) {
|
if (call.hangupReason === CallErrorCode.UserHangup) {
|
||||||
title = _t("Call Declined");
|
title = _t("Call Declined");
|
||||||
description = _t("The other party declined the call.");
|
description = _t("The other party declined the call.");
|
||||||
|
} else if (call.hangupReason === CallErrorCode.UserBusy) {
|
||||||
|
title = _t("User Busy");
|
||||||
|
description = _t("The user you called is busy.");
|
||||||
} else if (call.hangupReason === CallErrorCode.InviteTimeout) {
|
} else if (call.hangupReason === CallErrorCode.InviteTimeout) {
|
||||||
title = _t("Call Failed");
|
title = _t("Call Failed");
|
||||||
// XXX: full stop appended as some relic here, but these
|
// XXX: full stop appended as some relic here, but these
|
||||||
|
@ -518,7 +521,9 @@ export default class CallHandler extends EventEmitter {
|
||||||
let newNativeAssertedIdentity = newAssertedIdentity;
|
let newNativeAssertedIdentity = newAssertedIdentity;
|
||||||
if (newAssertedIdentity) {
|
if (newAssertedIdentity) {
|
||||||
const response = await this.sipNativeLookup(newAssertedIdentity);
|
const response = await this.sipNativeLookup(newAssertedIdentity);
|
||||||
if (response.length) newNativeAssertedIdentity = response[0].userid;
|
if (response.length && response[0].fields.lookup_success) {
|
||||||
|
newNativeAssertedIdentity = response[0].userid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`);
|
console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`);
|
||||||
|
|
||||||
|
@ -537,6 +542,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
if (newMappedRoomId !== mappedRoomId) {
|
if (newMappedRoomId !== mappedRoomId) {
|
||||||
this.removeCallForRoom(mappedRoomId);
|
this.removeCallForRoom(mappedRoomId);
|
||||||
mappedRoomId = newMappedRoomId;
|
mappedRoomId = newMappedRoomId;
|
||||||
|
console.log("Moving call to room " + mappedRoomId);
|
||||||
this.calls.set(mappedRoomId, call);
|
this.calls.set(mappedRoomId, call);
|
||||||
this.emit(CallHandlerEvent.CallChangeRoom, call);
|
this.emit(CallHandlerEvent.CallChangeRoom, call);
|
||||||
}
|
}
|
||||||
|
@ -602,6 +608,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeCallForRoom(roomId: string) {
|
private removeCallForRoom(roomId: string) {
|
||||||
|
console.log("Removing call for room ", roomId);
|
||||||
this.calls.delete(roomId);
|
this.calls.delete(roomId);
|
||||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
}
|
}
|
||||||
|
@ -675,6 +682,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
|
console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
|
||||||
const call = MatrixClientPeg.get().createCall(mappedRoomId);
|
const call = MatrixClientPeg.get().createCall(mappedRoomId);
|
||||||
|
|
||||||
|
console.log("Adding call for room ", roomId);
|
||||||
this.calls.set(roomId, call);
|
this.calls.set(roomId, call);
|
||||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
if (transferee) {
|
if (transferee) {
|
||||||
|
@ -799,11 +807,15 @@ export default class CallHandler extends EventEmitter {
|
||||||
|
|
||||||
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
|
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
|
||||||
if (this.getCallForRoom(mappedRoomId)) {
|
if (this.getCallForRoom(mappedRoomId)) {
|
||||||
// ignore multiple incoming calls to the same room
|
console.log(
|
||||||
|
"Got incoming call for room " + mappedRoomId +
|
||||||
|
" but there's already a call for this room: ignoring",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||||
|
console.log("Adding call for room ", mappedRoomId);
|
||||||
this.calls.set(mappedRoomId, call)
|
this.calls.set(mappedRoomId, call)
|
||||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
|
@ -856,9 +868,43 @@ export default class CallHandler extends EventEmitter {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case Action.DialNumber:
|
||||||
|
this.dialNumber(payload.number);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async dialNumber(number: string) {
|
||||||
|
const results = await this.pstnLookup(number);
|
||||||
|
if (!results || results.length === 0 || !results[0].userid) {
|
||||||
|
Modal.createTrackedDialog('', '', ErrorDialog, {
|
||||||
|
title: _t("Unable to look up phone number"),
|
||||||
|
description: _t("There was an error looking up the phone number"),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const userId = results[0].userid;
|
||||||
|
|
||||||
|
// Now check to see if this is a virtual user, in which case we should find the
|
||||||
|
// native user
|
||||||
|
let nativeUserId;
|
||||||
|
if (this.getSupportsVirtualRooms()) {
|
||||||
|
const nativeLookupResults = await this.sipNativeLookup(userId);
|
||||||
|
const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success;
|
||||||
|
nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId;
|
||||||
|
console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId);
|
||||||
|
} else {
|
||||||
|
nativeUserId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomId = await ensureDMExists(MatrixClientPeg.get(), nativeUserId);
|
||||||
|
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setActiveCallRoomId(activeCallRoomId: string) {
|
setActiveCallRoomId(activeCallRoomId: string) {
|
||||||
logger.info("Setting call in room " + activeCallRoomId + " active");
|
logger.info("Setting call in room " + activeCallRoomId + " active");
|
||||||
|
|
||||||
|
|
|
@ -29,8 +29,6 @@ import encrypt from "browser-encrypt-attachment";
|
||||||
import extractPngChunks from "png-chunks-extract";
|
import extractPngChunks from "png-chunks-extract";
|
||||||
import Spinner from "./components/views/elements/Spinner";
|
import Spinner from "./components/views/elements/Spinner";
|
||||||
|
|
||||||
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
|
|
||||||
import "blueimp-canvas-to-blob";
|
|
||||||
import { Action } from "./dispatcher/actions";
|
import { Action } from "./dispatcher/actions";
|
||||||
import CountlyAnalytics from "./CountlyAnalytics";
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
import {
|
import {
|
||||||
|
@ -41,6 +39,7 @@ import {
|
||||||
UploadStartedPayload,
|
UploadStartedPayload,
|
||||||
} from "./dispatcher/payloads/UploadPayload";
|
} from "./dispatcher/payloads/UploadPayload";
|
||||||
import {IUpload} from "./models/IUpload";
|
import {IUpload} from "./models/IUpload";
|
||||||
|
import { IImageInfo } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
const MAX_WIDTH = 800;
|
const MAX_WIDTH = 800;
|
||||||
const MAX_HEIGHT = 600;
|
const MAX_HEIGHT = 600;
|
||||||
|
@ -224,12 +223,12 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageInfo;
|
let imageInfo;
|
||||||
return loadImageElement(imageFile).then(function(r) {
|
return loadImageElement(imageFile).then((r) => {
|
||||||
return createThumbnail(r.img, r.width, r.height, thumbnailType);
|
return createThumbnail(r.img, r.width, r.height, thumbnailType);
|
||||||
}).then(function(result) {
|
}).then((result) => {
|
||||||
imageInfo = result.info;
|
imageInfo = result.info;
|
||||||
return uploadFile(matrixClient, roomId, result.thumbnail);
|
return uploadFile(matrixClient, roomId, result.thumbnail);
|
||||||
}).then(function(result) {
|
}).then((result) => {
|
||||||
imageInfo.thumbnail_url = result.url;
|
imageInfo.thumbnail_url = result.url;
|
||||||
imageInfo.thumbnail_file = result.file;
|
imageInfo.thumbnail_file = result.file;
|
||||||
return imageInfo;
|
return imageInfo;
|
||||||
|
@ -286,12 +285,12 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
|
||||||
const thumbnailType = "image/jpeg";
|
const thumbnailType = "image/jpeg";
|
||||||
|
|
||||||
let videoInfo;
|
let videoInfo;
|
||||||
return loadVideoElement(videoFile).then(function(video) {
|
return loadVideoElement(videoFile).then((video) => {
|
||||||
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
|
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
|
||||||
}).then(function(result) {
|
}).then((result) => {
|
||||||
videoInfo = result.info;
|
videoInfo = result.info;
|
||||||
return uploadFile(matrixClient, roomId, result.thumbnail);
|
return uploadFile(matrixClient, roomId, result.thumbnail);
|
||||||
}).then(function(result) {
|
}).then((result) => {
|
||||||
videoInfo.thumbnail_url = result.url;
|
videoInfo.thumbnail_url = result.url;
|
||||||
videoInfo.thumbnail_file = result.file;
|
videoInfo.thumbnail_file = result.file;
|
||||||
return videoInfo;
|
return videoInfo;
|
||||||
|
@ -330,7 +329,12 @@ function readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
|
||||||
* If the file is unencrypted then the object will have a "url" key.
|
* If the file is unencrypted then the object will have a "url" key.
|
||||||
* If the file is encrypted then the object will have a "file" key.
|
* If the file is encrypted then the object will have a "file" key.
|
||||||
*/
|
*/
|
||||||
function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blob, progressHandler?: any) {
|
function uploadFile(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
roomId: string,
|
||||||
|
file: File | Blob,
|
||||||
|
progressHandler?: any, // TODO: Types
|
||||||
|
): Promise<{url?: string, file?: any}> { // TODO: Types
|
||||||
let canceled = false;
|
let canceled = false;
|
||||||
if (matrixClient.isRoomEncrypted(roomId)) {
|
if (matrixClient.isRoomEncrypted(roomId)) {
|
||||||
// If the room is encrypted then encrypt the file before uploading it.
|
// If the room is encrypted then encrypt the file before uploading it.
|
||||||
|
@ -377,7 +381,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo
|
||||||
// If the attachment isn't encrypted then include the URL directly.
|
// If the attachment isn't encrypted then include the URL directly.
|
||||||
return { url };
|
return { url };
|
||||||
});
|
});
|
||||||
promise1.abort = () => {
|
(promise1 as any).abort = () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
matrixClient.cancelUpload(basePromise);
|
matrixClient.cancelUpload(basePromise);
|
||||||
};
|
};
|
||||||
|
@ -389,7 +393,7 @@ export default class ContentMessages {
|
||||||
private inprogress: IUpload[] = [];
|
private inprogress: IUpload[] = [];
|
||||||
private mediaConfig: IMediaConfig = null;
|
private mediaConfig: IMediaConfig = null;
|
||||||
|
|
||||||
sendStickerContentToRoom(url: string, roomId: string, info: object, text: string, matrixClient: MatrixClient) {
|
sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
|
||||||
const startTime = CountlyAnalytics.getTimestamp();
|
const startTime = CountlyAnalytics.getTimestamp();
|
||||||
const prom = matrixClient.sendStickerMessage(roomId, url, info, text).catch((e) => {
|
const prom = matrixClient.sendStickerMessage(roomId, url, info, text).catch((e) => {
|
||||||
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
||||||
|
@ -463,7 +467,7 @@ export default class ContentMessages {
|
||||||
let uploadAll = false;
|
let uploadAll = false;
|
||||||
// Promise to complete before sending next file into room, used for synchronisation of file-sending
|
// Promise to complete before sending next file into room, used for synchronisation of file-sending
|
||||||
// to match the order the files were specified in
|
// to match the order the files were specified in
|
||||||
let promBefore = Promise.resolve();
|
let promBefore: Promise<any> = Promise.resolve();
|
||||||
for (let i = 0; i < okFiles.length; ++i) {
|
for (let i = 0; i < okFiles.length; ++i) {
|
||||||
const file = okFiles[i];
|
const file = okFiles[i];
|
||||||
if (!uploadAll) {
|
if (!uploadAll) {
|
||||||
|
|
|
@ -22,13 +22,7 @@ import SdkConfig from './SdkConfig';
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||||
import {sleep} from "./utils/promise";
|
import {sleep} from "./utils/promise";
|
||||||
import RoomViewStore from "./stores/RoomViewStore";
|
import RoomViewStore from "./stores/RoomViewStore";
|
||||||
|
import { Action } from "./dispatcher/actions";
|
||||||
// polyfill textencoder if necessary
|
|
||||||
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
|
|
||||||
let TextEncoder = window.TextEncoder;
|
|
||||||
if (!TextEncoder) {
|
|
||||||
TextEncoder = TextEncodingUtf8.TextEncoder;
|
|
||||||
}
|
|
||||||
|
|
||||||
const INACTIVITY_TIME = 20; // seconds
|
const INACTIVITY_TIME = 20; // seconds
|
||||||
const HEARTBEAT_INTERVAL = 5_000; // ms
|
const HEARTBEAT_INTERVAL = 5_000; // ms
|
||||||
|
@ -265,7 +259,7 @@ interface ICreateRoomEvent extends IEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IJoinRoomEvent extends IEvent {
|
interface IJoinRoomEvent extends IEvent {
|
||||||
key: "join_room";
|
key: Action.JoinRoom;
|
||||||
dur: number; // how long it took to join (until remote echo)
|
dur: number; // how long it took to join (until remote echo)
|
||||||
segmentation: {
|
segmentation: {
|
||||||
room_id: string; // hashed
|
room_id: string; // hashed
|
||||||
|
@ -684,7 +678,9 @@ export default class CountlyAnalytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOrientation = (): Orientation => {
|
private getOrientation = (): Orientation => {
|
||||||
return window.innerWidth > window.innerHeight ? Orientation.Landscape : Orientation.Portrait;
|
return window.matchMedia("(orientation: landscape)").matches
|
||||||
|
? Orientation.Landscape
|
||||||
|
: Orientation.Portrait
|
||||||
};
|
};
|
||||||
|
|
||||||
private reportOrientation = () => {
|
private reportOrientation = () => {
|
||||||
|
@ -813,7 +809,9 @@ export default class CountlyAnalytics {
|
||||||
window.addEventListener("mousemove", this.onUserActivity);
|
window.addEventListener("mousemove", this.onUserActivity);
|
||||||
window.addEventListener("click", this.onUserActivity);
|
window.addEventListener("click", this.onUserActivity);
|
||||||
window.addEventListener("keydown", this.onUserActivity);
|
window.addEventListener("keydown", this.onUserActivity);
|
||||||
window.addEventListener("scroll", this.onUserActivity);
|
// Using the passive option to not block the main thread
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
|
window.addEventListener("scroll", this.onUserActivity, { passive: true });
|
||||||
|
|
||||||
this.activityIntervalId = setInterval(() => {
|
this.activityIntervalId = setInterval(() => {
|
||||||
this.inactivityCounter++;
|
this.inactivityCounter++;
|
||||||
|
@ -858,7 +856,7 @@ export default class CountlyAnalytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
public trackRoomJoin(startTime: number, roomId: string, type: IJoinRoomEvent["segmentation"]["type"]) {
|
public trackRoomJoin(startTime: number, roomId: string, type: IJoinRoomEvent["segmentation"]["type"]) {
|
||||||
this.track<IJoinRoomEvent>("join_room", { type }, roomId, {
|
this.track<IJoinRoomEvent>(Action.JoinRoom, { type }, roomId, {
|
||||||
dur: CountlyAnalytics.getTimestamp() - startTime,
|
dur: CountlyAnalytics.getTimestamp() - startTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import MultiInviter from './utils/MultiInviter';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
import GroupStore from './stores/GroupStore';
|
import GroupStore from './stores/GroupStore';
|
||||||
import {allSettled} from "./utils/promise";
|
|
||||||
import StyledCheckbox from './components/views/elements/StyledCheckbox';
|
import StyledCheckbox from './components/views/elements/StyledCheckbox';
|
||||||
|
|
||||||
export function showGroupInviteDialog(groupId) {
|
export function showGroupInviteDialog(groupId) {
|
||||||
|
@ -120,7 +119,7 @@ function _onGroupInviteFinished(groupId, addrs) {
|
||||||
function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
|
function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
|
||||||
const matrixClient = MatrixClientPeg.get();
|
const matrixClient = MatrixClientPeg.get();
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
return allSettled(addrs.map((addr) => {
|
return Promise.allSettled(addrs.map((addr) => {
|
||||||
return GroupStore
|
return GroupStore
|
||||||
.addRoomToGroup(groupId, addr.address, addRoomsPublicly)
|
.addRoomToGroup(groupId, addr.address, addRoomsPublicly)
|
||||||
.catch(() => { errorList.push(addr.address); })
|
.catch(() => { errorList.push(addr.address); })
|
||||||
|
|
15
src/Login.ts
15
src/Login.ts
|
@ -31,12 +31,12 @@ interface IPasswordFlow {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum IdentityProviderBrand {
|
export enum IdentityProviderBrand {
|
||||||
Gitlab = "org.matrix.gitlab",
|
Gitlab = "gitlab",
|
||||||
Github = "org.matrix.github",
|
Github = "github",
|
||||||
Apple = "org.matrix.apple",
|
Apple = "apple",
|
||||||
Google = "org.matrix.google",
|
Google = "google",
|
||||||
Facebook = "org.matrix.facebook",
|
Facebook = "facebook",
|
||||||
Twitter = "org.matrix.twitter",
|
Twitter = "twitter",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIdentityProvider {
|
export interface IIdentityProvider {
|
||||||
|
@ -48,7 +48,8 @@ export interface IIdentityProvider {
|
||||||
|
|
||||||
export interface ISSOFlow {
|
export interface ISSOFlow {
|
||||||
type: "m.login.sso" | "m.login.cas";
|
type: "m.login.sso" | "m.login.cas";
|
||||||
"org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858
|
// eslint-disable-next-line camelcase
|
||||||
|
identity_providers: IIdentityProvider[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LoginFlow = ISSOFlow | IPasswordFlow;
|
export type LoginFlow = ISSOFlow | IPasswordFlow;
|
||||||
|
|
|
@ -331,6 +331,8 @@ export const Notifier = {
|
||||||
if (!this.isSyncing) return; // don't alert for any messages initially
|
if (!this.isSyncing) return; // don't alert for any messages initially
|
||||||
if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return;
|
if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return;
|
||||||
|
|
||||||
|
MatrixClientPeg.get().decryptEventIfNeeded(ev);
|
||||||
|
|
||||||
// If it's an encrypted event and the type is still 'm.room.encrypted',
|
// If it's an encrypted event and the type is still 'm.room.encrypted',
|
||||||
// it hasn't yet been decrypted, so wait until it is.
|
// it hasn't yet been decrypted, so wait until it is.
|
||||||
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) {
|
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) {
|
||||||
|
|
|
@ -98,7 +98,7 @@ class Presence {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await MatrixClientPeg.get().setPresence(this.state);
|
await MatrixClientPeg.get().setPresence({presence: this.state});
|
||||||
console.info("Presence:", newState);
|
console.info("Presence:", newState);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to set presence:", err);
|
console.error("Failed to set presence:", err);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,35 +14,37 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||||
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
|
||||||
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
|
||||||
|
|
||||||
export default class Resend {
|
export default class Resend {
|
||||||
static resendUnsentEvents(room) {
|
static resendUnsentEvents(room: Room): Promise<void[]> {
|
||||||
return Promise.all(room.getPendingEvents().filter(function(ev) {
|
return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) {
|
||||||
return ev.status === EventStatus.NOT_SENT;
|
return ev.status === EventStatus.NOT_SENT;
|
||||||
}).map(function(event) {
|
}).map(function(event: MatrixEvent) {
|
||||||
return Resend.resend(event);
|
return Resend.resend(event);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
static cancelUnsentEvents(room) {
|
static cancelUnsentEvents(room: Room): void {
|
||||||
room.getPendingEvents().filter(function(ev) {
|
room.getPendingEvents().filter(function(ev: MatrixEvent) {
|
||||||
return ev.status === EventStatus.NOT_SENT;
|
return ev.status === EventStatus.NOT_SENT;
|
||||||
}).forEach(function(event) {
|
}).forEach(function(event: MatrixEvent) {
|
||||||
Resend.removeFromQueue(event);
|
Resend.removeFromQueue(event);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static resend(event) {
|
static resend(event: MatrixEvent): Promise<void> {
|
||||||
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||||
return MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
|
return MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent',
|
action: 'message_sent',
|
||||||
event: event,
|
event: event,
|
||||||
});
|
});
|
||||||
}, function(err) {
|
}, function(err: Error) {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/element-web/issues/3148
|
// https://github.com/vector-im/element-web/issues/3148
|
||||||
console.log('Resend got send failure: ' + err.name + '(' + err + ')');
|
console.log('Resend got send failure: ' + err.name + '(' + err + ')');
|
||||||
|
@ -55,7 +56,7 @@ export default class Resend {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static removeFromQueue(event) {
|
static removeFromQueue(event: MatrixEvent): void {
|
||||||
MatrixClientPeg.get().cancelPendingEvent(event);
|
MatrixClientPeg.get().cancelPendingEvent(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -24,12 +24,12 @@ limitations under the License.
|
||||||
* A similar thing could also be achieved via `pushState` with a state object,
|
* A similar thing could also be achieved via `pushState` with a state object,
|
||||||
* but keeping it separate like this seems easier in case we do want to extend.
|
* but keeping it separate like this seems easier in case we do want to extend.
|
||||||
*/
|
*/
|
||||||
const aliasToIDMap = new Map();
|
const aliasToIDMap = new Map<string, string>();
|
||||||
|
|
||||||
export function storeRoomAliasInCache(alias, id) {
|
export function storeRoomAliasInCache(alias: string, id: string): void {
|
||||||
aliasToIDMap.set(alias, id);
|
aliasToIDMap.set(alias, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCachedRoomIDForAlias(alias) {
|
export function getCachedRoomIDForAlias(alias: string): string {
|
||||||
return aliasToIDMap.get(alias);
|
return aliasToIDMap.get(alias);
|
||||||
}
|
}
|
|
@ -66,7 +66,7 @@ async function serverSideSearchProcess(term, roomId = undefined) {
|
||||||
highlights: [],
|
highlights: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return client._processRoomEventsSearch(searchResult, result.response);
|
return client.processRoomEventsSearch(searchResult, result.response);
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareEvents(a, b) {
|
function compareEvents(a, b) {
|
||||||
|
@ -131,7 +131,7 @@ async function combinedSearch(searchTerm) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = client._processRoomEventsSearch(emptyResult, response);
|
const result = client.processRoomEventsSearch(emptyResult, response);
|
||||||
|
|
||||||
// Restore our encryption info so we can properly re-verify the events.
|
// Restore our encryption info so we can properly re-verify the events.
|
||||||
restoreEncryptionInfo(result.results);
|
restoreEncryptionInfo(result.results);
|
||||||
|
@ -185,7 +185,7 @@ async function localSearchProcess(searchTerm, roomId = undefined) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const processedResult = MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response);
|
const processedResult = MatrixClientPeg.get().processRoomEventsSearch(emptyResult, response);
|
||||||
// Restore our encryption info so we can properly re-verify the events.
|
// Restore our encryption info so we can properly re-verify the events.
|
||||||
restoreEncryptionInfo(processedResult.results);
|
restoreEncryptionInfo(processedResult.results);
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ async function localPagination(searchResult) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = MatrixClientPeg.get()._processRoomEventsSearch(searchResult, response);
|
const result = MatrixClientPeg.get().processRoomEventsSearch(searchResult, response);
|
||||||
|
|
||||||
// Restore our encryption info so we can properly re-verify the events.
|
// Restore our encryption info so we can properly re-verify the events.
|
||||||
const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0));
|
const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0));
|
||||||
|
@ -520,7 +520,7 @@ async function combinedPagination(searchResult) {
|
||||||
const oldResultCount = searchResult.results ? searchResult.results.length : 0;
|
const oldResultCount = searchResult.results ? searchResult.results.length : 0;
|
||||||
|
|
||||||
// Let the client process the combined result.
|
// Let the client process the combined result.
|
||||||
const result = client._processRoomEventsSearch(searchResult, response);
|
const result = client.processRoomEventsSearch(searchResult, response);
|
||||||
|
|
||||||
// Restore our encryption info so we can properly re-verify the events.
|
// Restore our encryption info so we can properly re-verify the events.
|
||||||
const newResultCount = result.results.length - oldResultCount;
|
const newResultCount = result.results.length - oldResultCount;
|
||||||
|
|
|
@ -271,7 +271,7 @@ async function onSecretRequested(
|
||||||
}
|
}
|
||||||
return key && encodeBase64(key);
|
return key && encodeBase64(key);
|
||||||
} else if (name === "m.megolm_backup.v1") {
|
} else if (name === "m.megolm_backup.v1") {
|
||||||
const key = await client._crypto.getSessionBackupPrivateKey();
|
const key = await client.crypto.getSessionBackupPrivateKey();
|
||||||
if (!key) {
|
if (!key) {
|
||||||
console.log(
|
console.log(
|
||||||
`session backup key requested by ${deviceId}, but not found in cache`,
|
`session backup key requested by ${deviceId}, but not found in cache`,
|
||||||
|
|
16
src/Terms.ts
16
src/Terms.ts
|
@ -36,14 +36,18 @@ export class Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Policy {
|
export interface LocalisedPolicy {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Policy {
|
||||||
// @ts-ignore: No great way to express indexed types together with other keys
|
// @ts-ignore: No great way to express indexed types together with other keys
|
||||||
version: string;
|
version: string;
|
||||||
[lang: string]: {
|
[lang: string]: LocalisedPolicy;
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
type Policies = {
|
|
||||||
|
export type Policies = {
|
||||||
[policy: string]: Policy,
|
[policy: string]: Policy,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -99,7 +103,7 @@ export async function startTermsFlow(
|
||||||
|
|
||||||
// fetch the set of agreed policy URLs from account data
|
// fetch the set of agreed policy URLs from account data
|
||||||
const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms');
|
const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms');
|
||||||
let agreedUrlSet;
|
let agreedUrlSet: Set<string>;
|
||||||
if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) {
|
if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) {
|
||||||
agreedUrlSet = new Set();
|
agreedUrlSet = new Set();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -21,153 +21,161 @@ import SettingsStore from "./settings/SettingsStore";
|
||||||
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
|
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
|
||||||
import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
|
import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
|
||||||
|
|
||||||
function textForMemberEvent(ev) {
|
// These functions are frequently used just to check whether an event has
|
||||||
|
// any text to display at all. For this reason they return deferred values
|
||||||
|
// to avoid the expense of looking up translations when they're not needed.
|
||||||
|
|
||||||
|
function textForMemberEvent(ev): () => string | null {
|
||||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||||
const senderName = ev.sender ? ev.sender.name : ev.getSender();
|
const senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||||
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||||
const prevContent = ev.getPrevContent();
|
const prevContent = ev.getPrevContent();
|
||||||
const content = ev.getContent();
|
const content = ev.getContent();
|
||||||
|
|
||||||
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
||||||
switch (content.membership) {
|
switch (content.membership) {
|
||||||
case 'invite': {
|
case 'invite': {
|
||||||
const threePidContent = content.third_party_invite;
|
const threePidContent = content.third_party_invite;
|
||||||
if (threePidContent) {
|
if (threePidContent) {
|
||||||
if (threePidContent.display_name) {
|
if (threePidContent.display_name) {
|
||||||
return _t('%(targetName)s accepted the invitation for %(displayName)s.', {
|
return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', {
|
||||||
targetName,
|
targetName,
|
||||||
displayName: threePidContent.display_name,
|
displayName: threePidContent.display_name,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return _t('%(targetName)s accepted an invitation.', {targetName});
|
return () => _t('%(targetName)s accepted an invitation.', {targetName});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'ban':
|
case 'ban':
|
||||||
return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
|
||||||
case 'join':
|
case 'join':
|
||||||
if (prevContent && prevContent.membership === 'join') {
|
if (prevContent && prevContent.membership === 'join') {
|
||||||
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
||||||
return _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
|
return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
|
||||||
oldDisplayName: prevContent.displayname,
|
oldDisplayName: prevContent.displayname,
|
||||||
displayName: content.displayname,
|
displayName: content.displayname,
|
||||||
});
|
});
|
||||||
} else if (!prevContent.displayname && content.displayname) {
|
} else if (!prevContent.displayname && content.displayname) {
|
||||||
return _t('%(senderName)s set their display name to %(displayName)s.', {
|
return () => _t('%(senderName)s set their display name to %(displayName)s.', {
|
||||||
senderName: ev.getSender(),
|
senderName: ev.getSender(),
|
||||||
displayName: content.displayname,
|
displayName: content.displayname,
|
||||||
});
|
});
|
||||||
} else if (prevContent.displayname && !content.displayname) {
|
} else if (prevContent.displayname && !content.displayname) {
|
||||||
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
|
return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
|
||||||
senderName,
|
senderName,
|
||||||
oldDisplayName: prevContent.displayname,
|
oldDisplayName: prevContent.displayname,
|
||||||
});
|
});
|
||||||
} else if (prevContent.avatar_url && !content.avatar_url) {
|
} else if (prevContent.avatar_url && !content.avatar_url) {
|
||||||
return _t('%(senderName)s removed their profile picture.', {senderName});
|
return () => _t('%(senderName)s removed their profile picture.', {senderName});
|
||||||
} else if (prevContent.avatar_url && content.avatar_url &&
|
} else if (prevContent.avatar_url && content.avatar_url &&
|
||||||
prevContent.avatar_url !== content.avatar_url) {
|
prevContent.avatar_url !== content.avatar_url) {
|
||||||
return _t('%(senderName)s changed their profile picture.', {senderName});
|
return () => _t('%(senderName)s changed their profile picture.', {senderName});
|
||||||
} else if (!prevContent.avatar_url && content.avatar_url) {
|
} else if (!prevContent.avatar_url && content.avatar_url) {
|
||||||
return _t('%(senderName)s set a profile picture.', {senderName});
|
return () => _t('%(senderName)s set a profile picture.', {senderName});
|
||||||
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||||
// This is a null rejoin, it will only be visible if the Labs option is enabled
|
// This is a null rejoin, it will only be visible if the Labs option is enabled
|
||||||
return _t("%(senderName)s made no change.", {senderName});
|
return () => _t("%(senderName)s made no change.", {senderName});
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
||||||
return _t('%(targetName)s joined the room.', {targetName});
|
return () => _t('%(targetName)s joined the room.', {targetName});
|
||||||
}
|
}
|
||||||
case 'leave':
|
case 'leave':
|
||||||
if (ev.getSender() === ev.getStateKey()) {
|
if (ev.getSender() === ev.getStateKey()) {
|
||||||
if (prevContent.membership === "invite") {
|
if (prevContent.membership === "invite") {
|
||||||
return _t('%(targetName)s rejected the invitation.', {targetName});
|
return () => _t('%(targetName)s rejected the invitation.', {targetName});
|
||||||
} else {
|
} else {
|
||||||
return _t('%(targetName)s left the room.', {targetName});
|
return () => _t('%(targetName)s left the room.', {targetName});
|
||||||
}
|
}
|
||||||
} else if (prevContent.membership === "ban") {
|
} else if (prevContent.membership === "ban") {
|
||||||
return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
|
return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
|
||||||
} else if (prevContent.membership === "invite") {
|
} else if (prevContent.membership === "invite") {
|
||||||
return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
|
return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
|
||||||
senderName,
|
senderName,
|
||||||
targetName,
|
targetName,
|
||||||
}) + ' ' + reason;
|
}) + ' ' + getReason();
|
||||||
} else if (prevContent.membership === "join") {
|
} else if (prevContent.membership === "join") {
|
||||||
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForTopicEvent(ev) {
|
function textForTopicEvent(ev): () => string | null {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
|
return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
topic: ev.getContent().topic,
|
topic: ev.getContent().topic,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForRoomNameEvent(ev) {
|
function textForRoomNameEvent(ev): () => string | null {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
|
|
||||||
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
|
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
|
||||||
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
|
||||||
}
|
}
|
||||||
if (ev.getPrevContent().name) {
|
if (ev.getPrevContent().name) {
|
||||||
return _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
|
return () => _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
oldRoomName: ev.getPrevContent().name,
|
oldRoomName: ev.getPrevContent().name,
|
||||||
newRoomName: ev.getContent().name,
|
newRoomName: ev.getContent().name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
|
return () => _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
roomName: ev.getContent().name,
|
roomName: ev.getContent().name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForTombstoneEvent(ev) {
|
function textForTombstoneEvent(ev): () => string | null {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
return _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForJoinRulesEvent(ev) {
|
function textForJoinRulesEvent(ev): () => string | null {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
switch (ev.getContent().join_rule) {
|
switch (ev.getContent().join_rule) {
|
||||||
case "public":
|
case "public":
|
||||||
return _t('%(senderDisplayName)s made the room public to whoever knows the link.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s made the room public to whoever knows the link.', {
|
||||||
|
senderDisplayName,
|
||||||
|
});
|
||||||
case "invite":
|
case "invite":
|
||||||
return _t('%(senderDisplayName)s made the room invite only.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s made the room invite only.', {
|
||||||
|
senderDisplayName,
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
// The spec supports "knock" and "private", however nothing implements these.
|
// The spec supports "knock" and "private", however nothing implements these.
|
||||||
return _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
|
return () => _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
rule: ev.getContent().join_rule,
|
rule: ev.getContent().join_rule,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForGuestAccessEvent(ev) {
|
function textForGuestAccessEvent(ev): () => string | null {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
switch (ev.getContent().guest_access) {
|
switch (ev.getContent().guest_access) {
|
||||||
case "can_join":
|
case "can_join":
|
||||||
return _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
|
||||||
case "forbidden":
|
case "forbidden":
|
||||||
return _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
|
||||||
default:
|
default:
|
||||||
// There's no other options we can expect, however just for safety's sake we'll do this.
|
// There's no other options we can expect, however just for safety's sake we'll do this.
|
||||||
return _t('%(senderDisplayName)s changed guest access to %(rule)s', {
|
return () => _t('%(senderDisplayName)s changed guest access to %(rule)s', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
rule: ev.getContent().guest_access,
|
rule: ev.getContent().guest_access,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForRelatedGroupsEvent(ev) {
|
function textForRelatedGroupsEvent(ev): () => string | null {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
const groups = ev.getContent().groups || [];
|
const groups = ev.getContent().groups || [];
|
||||||
const prevGroups = ev.getPrevContent().groups || [];
|
const prevGroups = ev.getPrevContent().groups || [];
|
||||||
|
@ -175,17 +183,17 @@ function textForRelatedGroupsEvent(ev) {
|
||||||
const removed = prevGroups.filter((g) => !groups.includes(g));
|
const removed = prevGroups.filter((g) => !groups.includes(g));
|
||||||
|
|
||||||
if (added.length && !removed.length) {
|
if (added.length && !removed.length) {
|
||||||
return _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
|
return () => _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
groups: added.join(', '),
|
groups: added.join(', '),
|
||||||
});
|
});
|
||||||
} else if (!added.length && removed.length) {
|
} else if (!added.length && removed.length) {
|
||||||
return _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
|
return () => _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
groups: removed.join(', '),
|
groups: removed.join(', '),
|
||||||
});
|
});
|
||||||
} else if (added.length && removed.length) {
|
} else if (added.length && removed.length) {
|
||||||
return _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
|
return () => _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
|
||||||
'%(oldGroups)s in this room.', {
|
'%(oldGroups)s in this room.', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
newGroups: added.join(', '),
|
newGroups: added.join(', '),
|
||||||
|
@ -193,11 +201,11 @@ function textForRelatedGroupsEvent(ev) {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Don't bother rendering this change (because there were no changes)
|
// Don't bother rendering this change (because there were no changes)
|
||||||
return '';
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForServerACLEvent(ev) {
|
function textForServerACLEvent(ev): () => string | null {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
const prevContent = ev.getPrevContent();
|
const prevContent = ev.getPrevContent();
|
||||||
const current = ev.getContent();
|
const current = ev.getContent();
|
||||||
|
@ -207,11 +215,11 @@ function textForServerACLEvent(ev) {
|
||||||
allow_ip_literals: !(prevContent.allow_ip_literals === false),
|
allow_ip_literals: !(prevContent.allow_ip_literals === false),
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = "";
|
let getText = null;
|
||||||
if (prev.deny.length === 0 && prev.allow.length === 0) {
|
if (prev.deny.length === 0 && prev.allow.length === 0) {
|
||||||
text = _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
|
getText = () => _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
|
||||||
} else {
|
} else {
|
||||||
text = _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
|
getText = () => _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(current.allow)) {
|
if (!Array.isArray(current.allow)) {
|
||||||
|
@ -220,24 +228,27 @@ function textForServerACLEvent(ev) {
|
||||||
|
|
||||||
// If we know for sure everyone is banned, mark the room as obliterated
|
// If we know for sure everyone is banned, mark the room as obliterated
|
||||||
if (current.allow.length === 0) {
|
if (current.allow.length === 0) {
|
||||||
return text + " " + _t("🎉 All servers are banned from participating! This room can no longer be used.");
|
return () => getText() + " " +
|
||||||
|
_t("🎉 All servers are banned from participating! This room can no longer be used.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return text;
|
return getText;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForMessageEvent(ev) {
|
function textForMessageEvent(ev): () => string | null {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
return () => {
|
||||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
if (ev.getContent().msgtype === "m.emote") {
|
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||||
message = "* " + senderDisplayName + " " + message;
|
if (ev.getContent().msgtype === "m.emote") {
|
||||||
} else if (ev.getContent().msgtype === "m.image") {
|
message = "* " + senderDisplayName + " " + message;
|
||||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
} else if (ev.getContent().msgtype === "m.image") {
|
||||||
}
|
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
||||||
return message;
|
}
|
||||||
|
return message;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCanonicalAliasEvent(ev) {
|
function textForCanonicalAliasEvent(ev): () => string | null {
|
||||||
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
const oldAlias = ev.getPrevContent().alias;
|
const oldAlias = ev.getPrevContent().alias;
|
||||||
const oldAltAliases = ev.getPrevContent().alt_aliases || [];
|
const oldAltAliases = ev.getPrevContent().alt_aliases || [];
|
||||||
|
@ -248,96 +259,100 @@ function textForCanonicalAliasEvent(ev) {
|
||||||
|
|
||||||
if (!removedAltAliases.length && !addedAltAliases.length) {
|
if (!removedAltAliases.length && !addedAltAliases.length) {
|
||||||
if (newAlias) {
|
if (newAlias) {
|
||||||
return _t('%(senderName)s set the main address for this room to %(address)s.', {
|
return () => _t('%(senderName)s set the main address for this room to %(address)s.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
address: ev.getContent().alias,
|
address: ev.getContent().alias,
|
||||||
});
|
});
|
||||||
} else if (oldAlias) {
|
} else if (oldAlias) {
|
||||||
return _t('%(senderName)s removed the main address for this room.', {
|
return () => _t('%(senderName)s removed the main address for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (newAlias === oldAlias) {
|
} else if (newAlias === oldAlias) {
|
||||||
if (addedAltAliases.length && !removedAltAliases.length) {
|
if (addedAltAliases.length && !removedAltAliases.length) {
|
||||||
return _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
|
return () => _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
addresses: addedAltAliases.join(", "),
|
addresses: addedAltAliases.join(", "),
|
||||||
count: addedAltAliases.length,
|
count: addedAltAliases.length,
|
||||||
});
|
});
|
||||||
} if (removedAltAliases.length && !addedAltAliases.length) {
|
} if (removedAltAliases.length && !addedAltAliases.length) {
|
||||||
return _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
|
return () => _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
addresses: removedAltAliases.join(", "),
|
addresses: removedAltAliases.join(", "),
|
||||||
count: removedAltAliases.length,
|
count: removedAltAliases.length,
|
||||||
});
|
});
|
||||||
} if (removedAltAliases.length && addedAltAliases.length) {
|
} if (removedAltAliases.length && addedAltAliases.length) {
|
||||||
return _t('%(senderName)s changed the alternative addresses for this room.', {
|
return () => _t('%(senderName)s changed the alternative addresses for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// both alias and alt_aliases where modified
|
// both alias and alt_aliases where modified
|
||||||
return _t('%(senderName)s changed the main and alternative addresses for this room.', {
|
return () => _t('%(senderName)s changed the main and alternative addresses for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// in case there is no difference between the two events,
|
// in case there is no difference between the two events,
|
||||||
// say something as we can't simply hide the tile from here
|
// say something as we can't simply hide the tile from here
|
||||||
return _t('%(senderName)s changed the addresses for this room.', {
|
return () => _t('%(senderName)s changed the addresses for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallAnswerEvent(event) {
|
function textForCallAnswerEvent(event): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
return () => {
|
||||||
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
|
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
||||||
|
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallHangupEvent(event) {
|
function textForCallHangupEvent(event): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
||||||
const eventContent = event.getContent();
|
const eventContent = event.getContent();
|
||||||
let reason = "";
|
let getReason = () => "";
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
reason = _t('(not supported by this browser)');
|
getReason = () => _t('(not supported by this browser)');
|
||||||
} else if (eventContent.reason) {
|
} else if (eventContent.reason) {
|
||||||
if (eventContent.reason === "ice_failed") {
|
if (eventContent.reason === "ice_failed") {
|
||||||
// We couldn't establish a connection at all
|
// We couldn't establish a connection at all
|
||||||
reason = _t('(could not connect media)');
|
getReason = () => _t('(could not connect media)');
|
||||||
} else if (eventContent.reason === "ice_timeout") {
|
} else if (eventContent.reason === "ice_timeout") {
|
||||||
// We established a connection but it died
|
// We established a connection but it died
|
||||||
reason = _t('(connection failed)');
|
getReason = () => _t('(connection failed)');
|
||||||
} else if (eventContent.reason === "user_media_failed") {
|
} else if (eventContent.reason === "user_media_failed") {
|
||||||
// The other side couldn't open capture devices
|
// The other side couldn't open capture devices
|
||||||
reason = _t("(their device couldn't start the camera / microphone)");
|
getReason = () => _t("(their device couldn't start the camera / microphone)");
|
||||||
} else if (eventContent.reason === "unknown_error") {
|
} else if (eventContent.reason === "unknown_error") {
|
||||||
// An error code the other side doesn't have a way to express
|
// An error code the other side doesn't have a way to express
|
||||||
// (as opposed to an error code they gave but we don't know about,
|
// (as opposed to an error code they gave but we don't know about,
|
||||||
// in which case we show the error code)
|
// in which case we show the error code)
|
||||||
reason = _t("(an error occurred)");
|
getReason = () => _t("(an error occurred)");
|
||||||
} else if (eventContent.reason === "invite_timeout") {
|
} else if (eventContent.reason === "invite_timeout") {
|
||||||
reason = _t('(no answer)');
|
getReason = () => _t('(no answer)');
|
||||||
} else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
|
} else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
|
||||||
// workaround for https://github.com/vector-im/element-web/issues/5178
|
// workaround for https://github.com/vector-im/element-web/issues/5178
|
||||||
// it seems Android randomly sets a reason of "user hangup" which is
|
// it seems Android randomly sets a reason of "user hangup" which is
|
||||||
// interpreted as an error code :(
|
// interpreted as an error code :(
|
||||||
// https://github.com/vector-im/riot-android/issues/2623
|
// https://github.com/vector-im/riot-android/issues/2623
|
||||||
// Also the correct hangup code as of VoIP v1 (with underscore)
|
// Also the correct hangup code as of VoIP v1 (with underscore)
|
||||||
reason = '';
|
getReason = () => '';
|
||||||
} else {
|
} else {
|
||||||
reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
|
getReason = () => _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason;
|
return () => _t('%(senderName)s ended the call.', {senderName: getSenderName()}) + ' ' + getReason();
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallRejectEvent(event) {
|
function textForCallRejectEvent(event): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
return () => {
|
||||||
return _t('%(senderName)s declined the call.', {senderName});
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
|
return _t('%(senderName)s declined the call.', {senderName});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallInviteEvent(event) {
|
function textForCallInviteEvent(event): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
||||||
// FIXME: Find a better way to determine this from the event?
|
// FIXME: Find a better way to determine this from the event?
|
||||||
let isVoice = true;
|
let isVoice = true;
|
||||||
if (event.getContent().offer && event.getContent().offer.sdp &&
|
if (event.getContent().offer && event.getContent().offer.sdp &&
|
||||||
|
@ -350,48 +365,55 @@ function textForCallInviteEvent(event) {
|
||||||
// can have a hard time translating those strings. In an effort to make translations easier
|
// can have a hard time translating those strings. In an effort to make translations easier
|
||||||
// and more accurate, we break out the string-based variables to a couple booleans.
|
// and more accurate, we break out the string-based variables to a couple booleans.
|
||||||
if (isVoice && isSupported) {
|
if (isVoice && isSupported) {
|
||||||
return _t("%(senderName)s placed a voice call.", {senderName});
|
return () => _t("%(senderName)s placed a voice call.", {
|
||||||
|
senderName: getSenderName(),
|
||||||
|
});
|
||||||
} else if (isVoice && !isSupported) {
|
} else if (isVoice && !isSupported) {
|
||||||
return _t("%(senderName)s placed a voice call. (not supported by this browser)", {senderName});
|
return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", {
|
||||||
|
senderName: getSenderName(),
|
||||||
|
});
|
||||||
} else if (!isVoice && isSupported) {
|
} else if (!isVoice && isSupported) {
|
||||||
return _t("%(senderName)s placed a video call.", {senderName});
|
return () => _t("%(senderName)s placed a video call.", {
|
||||||
|
senderName: getSenderName(),
|
||||||
|
});
|
||||||
} else if (!isVoice && !isSupported) {
|
} else if (!isVoice && !isSupported) {
|
||||||
return _t("%(senderName)s placed a video call. (not supported by this browser)", {senderName});
|
return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
|
||||||
|
senderName: getSenderName(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForThreePidInviteEvent(event) {
|
function textForThreePidInviteEvent(event): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
|
|
||||||
if (!isValid3pidInvite(event)) {
|
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.', {
|
||||||
return _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
|
|
||||||
senderName,
|
senderName,
|
||||||
targetDisplayName,
|
targetDisplayName: event.getPrevContent().display_name || _t("Someone"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
|
return () => _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
|
||||||
senderName,
|
senderName,
|
||||||
targetDisplayName: event.getContent().display_name,
|
targetDisplayName: event.getContent().display_name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForHistoryVisibilityEvent(event) {
|
function textForHistoryVisibilityEvent(event): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
switch (event.getContent().history_visibility) {
|
switch (event.getContent().history_visibility) {
|
||||||
case 'invited':
|
case 'invited':
|
||||||
return _t('%(senderName)s made future room history visible to all room members, '
|
return () => _t('%(senderName)s made future room history visible to all room members, '
|
||||||
+ 'from the point they are invited.', {senderName});
|
+ 'from the point they are invited.', {senderName});
|
||||||
case 'joined':
|
case 'joined':
|
||||||
return _t('%(senderName)s made future room history visible to all room members, '
|
return () => _t('%(senderName)s made future room history visible to all room members, '
|
||||||
+ 'from the point they joined.', {senderName});
|
+ 'from the point they joined.', {senderName});
|
||||||
case 'shared':
|
case 'shared':
|
||||||
return _t('%(senderName)s made future room history visible to all room members.', {senderName});
|
return () => _t('%(senderName)s made future room history visible to all room members.', {senderName});
|
||||||
case 'world_readable':
|
case 'world_readable':
|
||||||
return _t('%(senderName)s made future room history visible to anyone.', {senderName});
|
return () => _t('%(senderName)s made future room history visible to anyone.', {senderName});
|
||||||
default:
|
default:
|
||||||
return _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
|
return () => _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
|
||||||
senderName,
|
senderName,
|
||||||
visibility: event.getContent().history_visibility,
|
visibility: event.getContent().history_visibility,
|
||||||
});
|
});
|
||||||
|
@ -399,11 +421,11 @@ function textForHistoryVisibilityEvent(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently will only display a change if a user's power level is changed
|
// Currently will only display a change if a user's power level is changed
|
||||||
function textForPowerEvent(event) {
|
function textForPowerEvent(event): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
if (!event.getPrevContent() || !event.getPrevContent().users ||
|
if (!event.getPrevContent() || !event.getPrevContent().users ||
|
||||||
!event.getContent() || !event.getContent().users) {
|
!event.getContent() || !event.getContent().users) {
|
||||||
return '';
|
return null;
|
||||||
}
|
}
|
||||||
const userDefault = event.getContent().users_default || 0;
|
const userDefault = event.getContent().users_default || 0;
|
||||||
// Construct set of userIds
|
// Construct set of userIds
|
||||||
|
@ -418,38 +440,38 @@ function textForPowerEvent(event) {
|
||||||
if (users.indexOf(userId) === -1) users.push(userId);
|
if (users.indexOf(userId) === -1) users.push(userId);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const diff = [];
|
const diffs = [];
|
||||||
// XXX: This is also surely broken for i18n
|
|
||||||
users.forEach((userId) => {
|
users.forEach((userId) => {
|
||||||
// Previous power level
|
// Previous power level
|
||||||
const from = event.getPrevContent().users[userId];
|
const from = event.getPrevContent().users[userId];
|
||||||
// Current power level
|
// Current power level
|
||||||
const to = event.getContent().users[userId];
|
const to = event.getContent().users[userId];
|
||||||
if (to !== from) {
|
if (to !== from) {
|
||||||
diff.push(
|
diffs.push({ userId, from, to });
|
||||||
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
|
||||||
userId,
|
|
||||||
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
|
|
||||||
toPowerLevel: Roles.textualPowerLevel(to, userDefault),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!diff.length) {
|
if (!diffs.length) {
|
||||||
return '';
|
return null;
|
||||||
}
|
}
|
||||||
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
|
// XXX: This is also surely broken for i18n
|
||||||
|
return () => _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
|
||||||
senderName,
|
senderName,
|
||||||
powerLevelDiffText: diff.join(", "),
|
powerLevelDiffText: diffs.map(diff =>
|
||||||
|
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
||||||
|
userId: diff.userId,
|
||||||
|
fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault),
|
||||||
|
toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault),
|
||||||
|
}),
|
||||||
|
).join(", "),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForPinnedEvent(event) {
|
function textForPinnedEvent(event): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
return _t("%(senderName)s changed the pinned messages for the room.", {senderName});
|
return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForWidgetEvent(event) {
|
function textForWidgetEvent(event): () => string | null {
|
||||||
const senderName = event.getSender();
|
const senderName = event.getSender();
|
||||||
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
|
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
|
||||||
const {name, type, url} = event.getContent() || {};
|
const {name, type, url} = event.getContent() || {};
|
||||||
|
@ -464,27 +486,27 @@ function textForWidgetEvent(event) {
|
||||||
// equivalent to that condition.
|
// equivalent to that condition.
|
||||||
if (url) {
|
if (url) {
|
||||||
if (prevUrl) {
|
if (prevUrl) {
|
||||||
return _t('%(widgetName)s widget modified by %(senderName)s', {
|
return () => _t('%(widgetName)s widget modified by %(senderName)s', {
|
||||||
widgetName, senderName,
|
widgetName, senderName,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return _t('%(widgetName)s widget added by %(senderName)s', {
|
return () => _t('%(widgetName)s widget added by %(senderName)s', {
|
||||||
widgetName, senderName,
|
widgetName, senderName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return _t('%(widgetName)s widget removed by %(senderName)s', {
|
return () => _t('%(widgetName)s widget removed by %(senderName)s', {
|
||||||
widgetName, senderName,
|
widgetName, senderName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForWidgetLayoutEvent(event) {
|
function textForWidgetLayoutEvent(event): () => string | null {
|
||||||
const senderName = event.sender?.name || event.getSender();
|
const senderName = event.sender?.name || event.getSender();
|
||||||
return _t("%(senderName)s has updated the widget layout", {senderName});
|
return () => _t("%(senderName)s has updated the widget layout", {senderName});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForMjolnirEvent(event) {
|
function textForMjolnirEvent(event): () => string | null {
|
||||||
const senderName = event.getSender();
|
const senderName = event.getSender();
|
||||||
const {entity: prevEntity} = event.getPrevContent();
|
const {entity: prevEntity} = event.getPrevContent();
|
||||||
const {entity, recommendation, reason} = event.getContent();
|
const {entity, recommendation, reason} = event.getContent();
|
||||||
|
@ -492,74 +514,74 @@ function textForMjolnirEvent(event) {
|
||||||
// Rule removed
|
// Rule removed
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s removed the rule banning users matching %(glob)s",
|
return () => _t("%(senderName)s removed the rule banning users matching %(glob)s",
|
||||||
{senderName, glob: prevEntity});
|
{senderName, glob: prevEntity});
|
||||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
|
return () => _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
|
||||||
{senderName, glob: prevEntity});
|
{senderName, glob: prevEntity});
|
||||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s removed the rule banning servers matching %(glob)s",
|
return () => _t("%(senderName)s removed the rule banning servers matching %(glob)s",
|
||||||
{senderName, glob: prevEntity});
|
{senderName, glob: prevEntity});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type. We'll say something, but we shouldn't end up here.
|
// Unknown type. We'll say something, but we shouldn't end up here.
|
||||||
return _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
|
return () => _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalid rule
|
// Invalid rule
|
||||||
if (!recommendation || !reason) return _t(`%(senderName)s updated an invalid ban rule`, {senderName});
|
if (!recommendation || !reason) return () => _t(`%(senderName)s updated an invalid ban rule`, {senderName});
|
||||||
|
|
||||||
// Rule updated
|
// Rule updated
|
||||||
if (entity === prevEntity) {
|
if (entity === prevEntity) {
|
||||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type. We'll say something but we shouldn't end up here.
|
// Unknown type. We'll say something but we shouldn't end up here.
|
||||||
return _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
}
|
}
|
||||||
|
|
||||||
// New rule
|
// New rule
|
||||||
if (!prevEntity) {
|
if (!prevEntity) {
|
||||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type. We'll say something but we shouldn't end up here.
|
// Unknown type. We'll say something but we shouldn't end up here.
|
||||||
return _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
}
|
}
|
||||||
|
|
||||||
// else the entity !== prevEntity - count as a removal & add
|
// else the entity !== prevEntity - count as a removal & add
|
||||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t(
|
return () => _t(
|
||||||
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
|
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
|
||||||
"%(newGlob)s for %(reason)s",
|
"%(newGlob)s for %(reason)s",
|
||||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||||
);
|
);
|
||||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t(
|
return () => _t(
|
||||||
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
|
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
|
||||||
"%(newGlob)s for %(reason)s",
|
"%(newGlob)s for %(reason)s",
|
||||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||||
);
|
);
|
||||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t(
|
return () => _t(
|
||||||
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
|
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
|
||||||
"%(newGlob)s for %(reason)s",
|
"%(newGlob)s for %(reason)s",
|
||||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||||
|
@ -567,11 +589,15 @@ function textForMjolnirEvent(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type. We'll say something but we shouldn't end up here.
|
// Unknown type. We'll say something but we shouldn't end up here.
|
||||||
return _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
|
return () => _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
|
||||||
"for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
"for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlers = {
|
interface IHandlers {
|
||||||
|
[type: string]: (ev: any) => (() => string | null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers: IHandlers = {
|
||||||
'm.room.message': textForMessageEvent,
|
'm.room.message': textForMessageEvent,
|
||||||
'm.call.invite': textForCallInviteEvent,
|
'm.call.invite': textForCallInviteEvent,
|
||||||
'm.call.answer': textForCallAnswerEvent,
|
'm.call.answer': textForCallAnswerEvent,
|
||||||
|
@ -579,7 +605,7 @@ const handlers = {
|
||||||
'm.call.reject': textForCallRejectEvent,
|
'm.call.reject': textForCallRejectEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateHandlers = {
|
const stateHandlers: IHandlers = {
|
||||||
'm.room.canonical_alias': textForCanonicalAliasEvent,
|
'm.room.canonical_alias': textForCanonicalAliasEvent,
|
||||||
'm.room.name': textForRoomNameEvent,
|
'm.room.name': textForRoomNameEvent,
|
||||||
'm.room.topic': textForTopicEvent,
|
'm.room.topic': textForTopicEvent,
|
||||||
|
@ -604,8 +630,12 @@ for (const evType of ALL_RULE_TYPES) {
|
||||||
stateHandlers[evType] = textForMjolnirEvent;
|
stateHandlers[evType] = textForMjolnirEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function textForEvent(ev) {
|
export function hasText(ev): boolean {
|
||||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||||
if (handler) return handler(ev);
|
return Boolean(handler?.(ev));
|
||||||
return '';
|
}
|
||||||
|
|
||||||
|
export function textForEvent(ev): string {
|
||||||
|
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||||
|
return handler?.(ev)?.() || '';
|
||||||
}
|
}
|
|
@ -33,7 +33,7 @@ export default class VoipUserMapper {
|
||||||
|
|
||||||
private async userToVirtualUser(userId: string): Promise<string> {
|
private async userToVirtualUser(userId: string): Promise<string> {
|
||||||
const results = await CallHandler.sharedInstance().sipVirtualLookup(userId);
|
const results = await CallHandler.sharedInstance().sipVirtualLookup(userId);
|
||||||
if (results.length === 0) return null;
|
if (results.length === 0 || !results[0].fields.lookup_success) return null;
|
||||||
return results[0].userid;
|
return results[0].userid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,14 +82,14 @@ export default class VoipUserMapper {
|
||||||
return Boolean(claimedNativeRoomId);
|
return Boolean(claimedNativeRoomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onNewInvitedRoom(invitedRoom: Room) {
|
public async onNewInvitedRoom(invitedRoom: Room): Promise<void> {
|
||||||
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
|
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
|
||||||
|
|
||||||
const inviterId = invitedRoom.getDMInviter();
|
const inviterId = invitedRoom.getDMInviter();
|
||||||
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
|
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
|
||||||
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
|
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
|
||||||
if (result.length === 0) {
|
if (result.length === 0) {
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result[0].fields.is_virtual) {
|
if (result[0].fields.is_virtual) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import dis from '../dispatcher/dispatcher';
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
|
||||||
|
|
||||||
|
import dis from "../dispatcher/dispatcher";
|
||||||
|
import {ActionPayload} from "../dispatcher/payloads";
|
||||||
|
|
||||||
// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events
|
// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events
|
||||||
// become dispatches in the same place.
|
// become dispatches in the same place.
|
||||||
|
@ -27,7 +33,7 @@ import dis from '../dispatcher/dispatcher';
|
||||||
* @param {string} prevState the previous sync state.
|
* @param {string} prevState the previous sync state.
|
||||||
* @returns {Object} an action of type MatrixActions.sync.
|
* @returns {Object} an action of type MatrixActions.sync.
|
||||||
*/
|
*/
|
||||||
function createSyncAction(matrixClient, state, prevState) {
|
function createSyncAction(matrixClient: MatrixClient, state: string, prevState: string): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.sync',
|
action: 'MatrixActions.sync',
|
||||||
state,
|
state,
|
||||||
|
@ -53,7 +59,7 @@ function createSyncAction(matrixClient, state, prevState) {
|
||||||
* @param {MatrixEvent} accountDataEvent the account data event.
|
* @param {MatrixEvent} accountDataEvent the account data event.
|
||||||
* @returns {AccountDataAction} an action of type MatrixActions.accountData.
|
* @returns {AccountDataAction} an action of type MatrixActions.accountData.
|
||||||
*/
|
*/
|
||||||
function createAccountDataAction(matrixClient, accountDataEvent) {
|
function createAccountDataAction(matrixClient: MatrixClient, accountDataEvent: MatrixEvent): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.accountData',
|
action: 'MatrixActions.accountData',
|
||||||
event: accountDataEvent,
|
event: accountDataEvent,
|
||||||
|
@ -81,7 +87,11 @@ function createAccountDataAction(matrixClient, accountDataEvent) {
|
||||||
* @param {Room} room the room where account data was changed
|
* @param {Room} room the room where account data was changed
|
||||||
* @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData.
|
* @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData.
|
||||||
*/
|
*/
|
||||||
function createRoomAccountDataAction(matrixClient, accountDataEvent, room) {
|
function createRoomAccountDataAction(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
accountDataEvent: MatrixEvent,
|
||||||
|
room: Room,
|
||||||
|
): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.Room.accountData',
|
action: 'MatrixActions.Room.accountData',
|
||||||
event: accountDataEvent,
|
event: accountDataEvent,
|
||||||
|
@ -106,7 +116,7 @@ function createRoomAccountDataAction(matrixClient, accountDataEvent, room) {
|
||||||
* @param {Room} room the Room that was stored.
|
* @param {Room} room the Room that was stored.
|
||||||
* @returns {RoomAction} an action of type `MatrixActions.Room`.
|
* @returns {RoomAction} an action of type `MatrixActions.Room`.
|
||||||
*/
|
*/
|
||||||
function createRoomAction(matrixClient, room) {
|
function createRoomAction(matrixClient: MatrixClient, room: Room): ActionPayload {
|
||||||
return { action: 'MatrixActions.Room', room };
|
return { action: 'MatrixActions.Room', room };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +137,7 @@ function createRoomAction(matrixClient, room) {
|
||||||
* @param {Room} room the Room whose tags were changed.
|
* @param {Room} room the Room whose tags were changed.
|
||||||
* @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`.
|
* @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`.
|
||||||
*/
|
*/
|
||||||
function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
|
function createRoomTagsAction(matrixClient: MatrixClient, roomTagsEvent: MatrixEvent, room: Room): ActionPayload {
|
||||||
return { action: 'MatrixActions.Room.tags', room };
|
return { action: 'MatrixActions.Room.tags', room };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +150,7 @@ function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
|
||||||
* @param {Room} room the room the receipt happened in.
|
* @param {Room} room the room the receipt happened in.
|
||||||
* @returns {Object} an action of type MatrixActions.Room.receipt.
|
* @returns {Object} an action of type MatrixActions.Room.receipt.
|
||||||
*/
|
*/
|
||||||
function createRoomReceiptAction(matrixClient, event, room) {
|
function createRoomReceiptAction(matrixClient: MatrixClient, event: MatrixEvent, room: Room): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.Room.receipt',
|
action: 'MatrixActions.Room.receipt',
|
||||||
event,
|
event,
|
||||||
|
@ -178,7 +188,17 @@ function createRoomReceiptAction(matrixClient, event, room) {
|
||||||
* @param {EventTimeline} data.timeline the timeline being altered.
|
* @param {EventTimeline} data.timeline the timeline being altered.
|
||||||
* @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`.
|
* @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`.
|
||||||
*/
|
*/
|
||||||
function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTimeline, removed, data) {
|
function createRoomTimelineAction(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
timelineEvent: MatrixEvent,
|
||||||
|
room: Room,
|
||||||
|
toStartOfTimeline: boolean,
|
||||||
|
removed: boolean,
|
||||||
|
data: {
|
||||||
|
liveEvent: boolean;
|
||||||
|
timeline: EventTimeline;
|
||||||
|
},
|
||||||
|
): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.Room.timeline',
|
action: 'MatrixActions.Room.timeline',
|
||||||
event: timelineEvent,
|
event: timelineEvent,
|
||||||
|
@ -208,8 +228,13 @@ function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTi
|
||||||
* @param {string} oldMembership the previous membership, can be null.
|
* @param {string} oldMembership the previous membership, can be null.
|
||||||
* @returns {RoomMembershipAction} an action of type `MatrixActions.Room.myMembership`.
|
* @returns {RoomMembershipAction} an action of type `MatrixActions.Room.myMembership`.
|
||||||
*/
|
*/
|
||||||
function createSelfMembershipAction(matrixClient, room, membership, oldMembership) {
|
function createSelfMembershipAction(
|
||||||
return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership};
|
matrixClient: MatrixClient,
|
||||||
|
room: Room,
|
||||||
|
membership: string,
|
||||||
|
oldMembership: string,
|
||||||
|
): ActionPayload {
|
||||||
|
return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -228,61 +253,65 @@ function createSelfMembershipAction(matrixClient, room, membership, oldMembershi
|
||||||
* @param {MatrixEvent} event the matrix event that was decrypted.
|
* @param {MatrixEvent} event the matrix event that was decrypted.
|
||||||
* @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`.
|
* @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`.
|
||||||
*/
|
*/
|
||||||
function createEventDecryptedAction(matrixClient, event) {
|
function createEventDecryptedAction(matrixClient: MatrixClient, event: MatrixEvent): ActionPayload {
|
||||||
return { action: 'MatrixActions.Event.decrypted', event };
|
return { action: 'MatrixActions.Event.decrypted', event };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Listener = () => void;
|
||||||
|
type ActionCreator = (matrixClient: MatrixClient, ...args: any) => ActionPayload;
|
||||||
|
|
||||||
|
// A list of callbacks to call to unregister all listeners added
|
||||||
|
let matrixClientListenersStop: Listener[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start listening to events of type eventName on matrixClient and when they are emitted,
|
||||||
|
* dispatch an action created by the actionCreator function.
|
||||||
|
* @param {MatrixClient} matrixClient a MatrixClient to register a listener with.
|
||||||
|
* @param {string} eventName the event to listen to on MatrixClient.
|
||||||
|
* @param {function} actionCreator a function that should return an action to dispatch
|
||||||
|
* when given the MatrixClient as an argument as well as
|
||||||
|
* arguments emitted in the MatrixClient event.
|
||||||
|
*/
|
||||||
|
function addMatrixClientListener(matrixClient: MatrixClient, eventName: string, actionCreator: ActionCreator): void {
|
||||||
|
const listener: Listener = (...args) => {
|
||||||
|
const payload = actionCreator(matrixClient, ...args);
|
||||||
|
if (payload) {
|
||||||
|
dis.dispatch(payload, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
matrixClient.on(eventName, listener);
|
||||||
|
matrixClientListenersStop.push(() => {
|
||||||
|
matrixClient.removeListener(eventName, listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This object is responsible for dispatching actions when certain events are emitted by
|
* This object is responsible for dispatching actions when certain events are emitted by
|
||||||
* the given MatrixClient.
|
* the given MatrixClient.
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
// A list of callbacks to call to unregister all listeners added
|
|
||||||
_matrixClientListenersStop: [],
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start listening to certain events from the MatrixClient and dispatch actions when
|
* Start listening to certain events from the MatrixClient and dispatch actions when
|
||||||
* they are emitted.
|
* they are emitted.
|
||||||
* @param {MatrixClient} matrixClient the MatrixClient to listen to events from
|
* @param {MatrixClient} matrixClient the MatrixClient to listen to events from
|
||||||
*/
|
*/
|
||||||
start(matrixClient) {
|
start(matrixClient: MatrixClient) {
|
||||||
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
|
addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Room', createRoomAction);
|
addMatrixClientListener(matrixClient, 'Room', createRoomAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
|
addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction);
|
addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
|
addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction);
|
addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction);
|
addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction);
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start listening to events of type eventName on matrixClient and when they are emitted,
|
|
||||||
* dispatch an action created by the actionCreator function.
|
|
||||||
* @param {MatrixClient} matrixClient a MatrixClient to register a listener with.
|
|
||||||
* @param {string} eventName the event to listen to on MatrixClient.
|
|
||||||
* @param {function} actionCreator a function that should return an action to dispatch
|
|
||||||
* when given the MatrixClient as an argument as well as
|
|
||||||
* arguments emitted in the MatrixClient event.
|
|
||||||
*/
|
|
||||||
_addMatrixClientListener(matrixClient, eventName, actionCreator) {
|
|
||||||
const listener = (...args) => {
|
|
||||||
const payload = actionCreator(matrixClient, ...args);
|
|
||||||
if (payload) {
|
|
||||||
dis.dispatch(payload, true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
matrixClient.on(eventName, listener);
|
|
||||||
this._matrixClientListenersStop.push(() => {
|
|
||||||
matrixClient.removeListener(eventName, listener);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop listening to events.
|
* Stop listening to events.
|
||||||
*/
|
*/
|
||||||
stop() {
|
stop() {
|
||||||
this._matrixClientListenersStop.forEach((stopListener) => stopListener());
|
matrixClientListenersStop.forEach((stopListener) => stopListener());
|
||||||
|
matrixClientListenersStop = [];
|
||||||
},
|
},
|
||||||
};
|
};
|
|
@ -21,7 +21,7 @@ import {removeHiddenChars} from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
interface IOptions<T extends {}> {
|
interface IOptions<T extends {}> {
|
||||||
keys: Array<string | keyof T>;
|
keys: Array<string | keyof T>;
|
||||||
funcs?: Array<(T) => string>;
|
funcs?: Array<(T) => string | string[]>;
|
||||||
shouldMatchWordsOnly?: boolean;
|
shouldMatchWordsOnly?: boolean;
|
||||||
// whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true
|
// whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true
|
||||||
fuzzy?: boolean;
|
fuzzy?: boolean;
|
||||||
|
@ -69,7 +69,12 @@ export default class QueryMatcher<T extends Object> {
|
||||||
|
|
||||||
if (this._options.funcs) {
|
if (this._options.funcs) {
|
||||||
for (const f of this._options.funcs) {
|
for (const f of this._options.funcs) {
|
||||||
keyValues.push(f(object));
|
const v = f(object);
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
keyValues.push(...v);
|
||||||
|
} else {
|
||||||
|
keyValues.push(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT 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";
|
|
||||||
|
|
||||||
export default class AutoHideScrollbar extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this._collectContainerRef = this._collectContainerRef.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
_collectContainerRef(ref) {
|
|
||||||
if (ref && !this.containerRef) {
|
|
||||||
this.containerRef = ref;
|
|
||||||
}
|
|
||||||
if (this.props.wrappedRef) {
|
|
||||||
this.props.wrappedRef(ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getScrollTop() {
|
|
||||||
return this.containerRef.scrollTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
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}
|
|
||||||
tabIndex={this.props.tabIndex}
|
|
||||||
>
|
|
||||||
{ this.props.children }
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
}
|
|
65
src/components/structures/AutoHideScrollbar.tsx
Normal file
65
src/components/structures/AutoHideScrollbar.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT 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";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
onScroll?: () => void;
|
||||||
|
onWheel?: () => void;
|
||||||
|
style?: React.CSSProperties
|
||||||
|
tabIndex?: number,
|
||||||
|
wrappedRef?: (ref: HTMLDivElement) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AutoHideScrollbar extends React.Component<IProps> {
|
||||||
|
private containerRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
if (this.containerRef.current && this.props.onScroll) {
|
||||||
|
// Using the passive option to not block the main thread
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
|
this.containerRef.current.addEventListener("scroll", this.props.onScroll, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.wrappedRef) {
|
||||||
|
this.props.wrappedRef(this.containerRef.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
if (this.containerRef.current && this.props.onScroll) {
|
||||||
|
this.containerRef.current.removeEventListener("scroll", this.props.onScroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getScrollTop(): number {
|
||||||
|
return this.containerRef.current.scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (<div
|
||||||
|
ref={this.containerRef}
|
||||||
|
style={this.props.style}
|
||||||
|
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
|
||||||
|
onWheel={this.props.onWheel}
|
||||||
|
tabIndex={this.props.tabIndex}
|
||||||
|
>
|
||||||
|
{ this.props.children }
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import classNames from "classnames";
|
||||||
import {Key} from "../../Keyboard";
|
import {Key} from "../../Keyboard";
|
||||||
import {Writeable} from "../../@types/common";
|
import {Writeable} from "../../@types/common";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||||
|
@ -410,12 +411,12 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
|
||||||
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
||||||
const buttonTop = elementRect.top + window.pageYOffset;
|
const buttonTop = elementRect.top + window.pageYOffset;
|
||||||
// Align the right edge of the menu to the right edge of the button
|
// Align the right edge of the menu to the right edge of the button
|
||||||
menuOptions.right = window.innerWidth - buttonRight;
|
menuOptions.right = UIStore.instance.windowWidth - buttonRight;
|
||||||
// Align the menu vertically on whichever side of the button has more space available.
|
// Align the menu vertically on whichever side of the button has more space available.
|
||||||
if (buttonBottom < window.innerHeight / 2) {
|
if (buttonBottom < UIStore.instance.windowHeight / 2) {
|
||||||
menuOptions.top = buttonBottom + vPadding;
|
menuOptions.top = buttonBottom + vPadding;
|
||||||
} else {
|
} else {
|
||||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
|
||||||
}
|
}
|
||||||
|
|
||||||
return menuOptions;
|
return menuOptions;
|
||||||
|
@ -430,12 +431,12 @@ export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFac
|
||||||
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
||||||
const buttonTop = elementRect.top + window.pageYOffset;
|
const buttonTop = elementRect.top + window.pageYOffset;
|
||||||
// Align the right edge of the menu to the right edge of the button
|
// Align the right edge of the menu to the right edge of the button
|
||||||
menuOptions.right = window.innerWidth - buttonRight;
|
menuOptions.right = UIStore.instance.windowWidth - buttonRight;
|
||||||
// Align the menu vertically on whichever side of the button has more space available.
|
// Align the menu vertically on whichever side of the button has more space available.
|
||||||
if (buttonBottom < window.innerHeight / 2) {
|
if (buttonBottom < UIStore.instance.windowHeight / 2) {
|
||||||
menuOptions.top = buttonBottom + vPadding;
|
menuOptions.top = buttonBottom + vPadding;
|
||||||
} else {
|
} else {
|
||||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
|
||||||
}
|
}
|
||||||
|
|
||||||
return menuOptions;
|
return menuOptions;
|
||||||
|
@ -451,7 +452,7 @@ export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFa
|
||||||
// Align the left edge of the menu to the left edge of the button
|
// Align the left edge of the menu to the left edge of the button
|
||||||
menuOptions.left = buttonLeft;
|
menuOptions.left = buttonLeft;
|
||||||
// Align the menu vertically above the menu
|
// Align the menu vertically above the menu
|
||||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
|
||||||
|
|
||||||
return menuOptions;
|
return menuOptions;
|
||||||
};
|
};
|
||||||
|
|
|
@ -50,6 +50,9 @@ class FilePanel extends React.Component {
|
||||||
if (room?.roomId !== this.props?.roomId) return;
|
if (room?.roomId !== this.props?.roomId) return;
|
||||||
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
|
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
client.decryptEventIfNeeded(ev);
|
||||||
|
|
||||||
if (ev.isBeingDecrypted()) {
|
if (ev.isBeingDecrypted()) {
|
||||||
this.decryptingEvents.add(ev.getId());
|
this.decryptingEvents.add(ev.getId());
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -36,7 +36,7 @@ import FlairStore from '../../stores/FlairStore';
|
||||||
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||||
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
|
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
|
||||||
import {Group} from "matrix-js-sdk/src/models/group";
|
import {Group} from "matrix-js-sdk/src/models/group";
|
||||||
import {allSettled, sleep} from "../../utils/promise";
|
import {sleep} from "../../utils/promise";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import {mediaFromMxc} from "../../customisations/Media";
|
import {mediaFromMxc} from "../../customisations/Media";
|
||||||
|
@ -99,7 +99,7 @@ class CategoryRoomList extends React.Component {
|
||||||
onFinished: (success, addrs) => {
|
onFinished: (success, addrs) => {
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
allSettled(addrs.map((addr) => {
|
Promise.allSettled(addrs.map((addr) => {
|
||||||
return GroupStore
|
return GroupStore
|
||||||
.addRoomToGroupSummary(this.props.groupId, addr.address)
|
.addRoomToGroupSummary(this.props.groupId, addr.address)
|
||||||
.catch(() => { errorList.push(addr.address); });
|
.catch(() => { errorList.push(addr.address); });
|
||||||
|
@ -274,7 +274,7 @@ class RoleUserList extends React.Component {
|
||||||
onFinished: (success, addrs) => {
|
onFinished: (success, addrs) => {
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
allSettled(addrs.map((addr) => {
|
Promise.allSettled(addrs.map((addr) => {
|
||||||
return GroupStore
|
return GroupStore
|
||||||
.addUserToGroupSummary(addr.address)
|
.addUserToGroupSummary(addr.address)
|
||||||
.catch(() => { errorList.push(addr.address); });
|
.catch(() => { errorList.push(addr.address); });
|
||||||
|
|
|
@ -24,13 +24,16 @@ import { HostSignupStore } from "../../stores/HostSignupStore";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {
|
||||||
|
onClick?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
interface IState {}
|
interface IState {}
|
||||||
|
|
||||||
@replaceableComponent("structures.HostSignupAction")
|
@replaceableComponent("structures.HostSignupAction")
|
||||||
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
|
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
|
||||||
private openDialog = async () => {
|
private openDialog = async () => {
|
||||||
|
this.props.onClick?.();
|
||||||
await HostSignupStore.instance.setHostSignupActive(true);
|
await HostSignupStore.instance.setHostSignupActive(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,9 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
_collectScroller(scroller) {
|
_collectScroller(scroller) {
|
||||||
if (scroller && !this._scrollElement) {
|
if (scroller && !this._scrollElement) {
|
||||||
this._scrollElement = scroller;
|
this._scrollElement = scroller;
|
||||||
this._scrollElement.addEventListener("scroll", this.checkOverflow);
|
// Using the passive option to not block the main thread
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
|
this._scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
|
||||||
this.checkOverflow();
|
this.checkOverflow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../customisations/Media";
|
import {mediaFromMxc} from "../../customisations/Media";
|
||||||
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
|
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
|
||||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||||
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
|
@ -66,6 +67,7 @@ const cssClasses = [
|
||||||
|
|
||||||
@replaceableComponent("structures.LeftPanel")
|
@replaceableComponent("structures.LeftPanel")
|
||||||
export default class LeftPanel extends React.Component<IProps, IState> {
|
export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
|
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private groupFilterPanelWatcherRef: string;
|
private groupFilterPanelWatcherRef: string;
|
||||||
private bgImageWatcherRef: string;
|
private bgImageWatcherRef: string;
|
||||||
|
@ -90,10 +92,14 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
||||||
this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
|
this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// We watch the middle panel because we don't actually get resized, the middle panel does.
|
public componentDidMount() {
|
||||||
// We listen to the noisy channel to avoid choppy reaction times.
|
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
|
||||||
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
|
UIStore.instance.on("ListContainer", this.refreshStickyHeaders);
|
||||||
|
// Using the passive option to not block the main thread
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
|
this.listContainerRef.current?.addEventListener("scroll", this.onScroll, { passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -103,7 +109,15 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
||||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||||
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
UIStore.instance.stopTrackingElementDimensions("ListContainer");
|
||||||
|
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
|
||||||
|
this.listContainerRef.current?.removeEventListener("scroll", this.onScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||||
|
if (prevState.activeSpace !== this.state.activeSpace) {
|
||||||
|
this.refreshStickyHeaders();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateActiveSpace = (activeSpace: Room) => {
|
private updateActiveSpace = (activeSpace: Room) => {
|
||||||
|
@ -114,6 +128,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
dis.fire(Action.ViewRoomDirectory);
|
dis.fire(Action.ViewRoomDirectory);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private refreshStickyHeaders = () => {
|
||||||
|
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
|
||||||
|
this.handleStickyHeaders(this.listContainerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
private onBreadcrumbsUpdate = () => {
|
private onBreadcrumbsUpdate = () => {
|
||||||
const newVal = BreadcrumbsStore.instance.visible;
|
const newVal = BreadcrumbsStore.instance.visible;
|
||||||
if (newVal !== this.state.showBreadcrumbs) {
|
if (newVal !== this.state.showBreadcrumbs) {
|
||||||
|
@ -156,9 +175,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
const bottomEdge = list.offsetHeight + list.scrollTop;
|
const bottomEdge = list.offsetHeight + list.scrollTop;
|
||||||
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist:not(.mx_RoomSublist_hidden)");
|
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist:not(.mx_RoomSublist_hidden)");
|
||||||
|
|
||||||
const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
|
|
||||||
const headerStickyWidth = list.clientWidth - headerRightMargin;
|
|
||||||
|
|
||||||
// We track which styles we want on a target before making the changes to avoid
|
// We track which styles we want on a target before making the changes to avoid
|
||||||
// excessive layout updates.
|
// excessive layout updates.
|
||||||
const targetStyles = new Map<HTMLDivElement, {
|
const targetStyles = new Map<HTMLDivElement, {
|
||||||
|
@ -228,7 +244,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
header.classList.add("mx_RoomSublist_headerContainer_stickyBottom");
|
header.classList.add("mx_RoomSublist_headerContainer_stickyBottom");
|
||||||
}
|
}
|
||||||
|
|
||||||
const offset = window.innerHeight - (list.parentElement.offsetTop + list.parentElement.offsetHeight);
|
const offset = UIStore.instance.windowHeight -
|
||||||
|
(list.parentElement.offsetTop + list.parentElement.offsetHeight);
|
||||||
const newBottom = `${offset}px`;
|
const newBottom = `${offset}px`;
|
||||||
if (header.style.bottom !== newBottom) {
|
if (header.style.bottom !== newBottom) {
|
||||||
header.style.bottom = newBottom;
|
header.style.bottom = newBottom;
|
||||||
|
@ -247,14 +264,20 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
header.classList.add("mx_RoomSublist_headerContainer_sticky");
|
header.classList.add("mx_RoomSublist_headerContainer_sticky");
|
||||||
}
|
}
|
||||||
|
|
||||||
const newWidth = `${headerStickyWidth}px`;
|
const listDimensions = UIStore.instance.getElementDimensions("ListContainer");
|
||||||
if (header.style.width !== newWidth) {
|
if (listDimensions) {
|
||||||
header.style.width = newWidth;
|
const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
|
||||||
|
const headerStickyWidth = listDimensions.width - headerRightMargin;
|
||||||
|
const newWidth = `${headerStickyWidth}px`;
|
||||||
|
if (header.style.width !== newWidth) {
|
||||||
|
header.style.width = newWidth;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (!style.stickyTop && !style.stickyBottom) {
|
} else if (!style.stickyTop && !style.stickyBottom) {
|
||||||
if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
||||||
header.classList.remove("mx_RoomSublist_headerContainer_sticky");
|
header.classList.remove("mx_RoomSublist_headerContainer_sticky");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header.style.width) {
|
if (header.style.width) {
|
||||||
header.style.removeProperty('width');
|
header.style.removeProperty('width');
|
||||||
}
|
}
|
||||||
|
@ -276,16 +299,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
|
private onScroll = (ev: Event) => {
|
||||||
const list = ev.target as HTMLDivElement;
|
const list = ev.target as HTMLDivElement;
|
||||||
this.handleStickyHeaders(list);
|
this.handleStickyHeaders(list);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onResize = () => {
|
|
||||||
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
|
|
||||||
this.handleStickyHeaders(this.listContainerRef.current);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onFocus = (ev: React.FocusEvent) => {
|
private onFocus = (ev: React.FocusEvent) => {
|
||||||
this.focusedElement = ev.target;
|
this.focusedElement = ev.target;
|
||||||
};
|
};
|
||||||
|
@ -420,8 +438,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
onResize={this.onResize}
|
|
||||||
activeSpace={this.state.activeSpace}
|
activeSpace={this.state.activeSpace}
|
||||||
|
onResize={this.refreshStickyHeaders}
|
||||||
|
onListCollapse={this.refreshStickyHeaders}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
const containerClasses = classNames({
|
const containerClasses = classNames({
|
||||||
|
@ -435,17 +454,16 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses} ref={this.ref}>
|
||||||
{leftLeftPanel}
|
{leftLeftPanel}
|
||||||
<aside className="mx_LeftPanel_roomListContainer">
|
<aside className="mx_LeftPanel_roomListContainer">
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
{this.renderSearchExplore()}
|
{this.renderSearchExplore()}
|
||||||
{this.renderBreadcrumbs()}
|
{this.renderBreadcrumbs()}
|
||||||
<RoomListNumResults />
|
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
|
||||||
<div className="mx_LeftPanel_roomListWrapper">
|
<div className="mx_LeftPanel_roomListWrapper">
|
||||||
<div
|
<div
|
||||||
className={roomListClasses}
|
className={roomListClasses}
|
||||||
onScroll={this.onScroll}
|
|
||||||
ref={this.listContainerRef}
|
ref={this.listContainerRef}
|
||||||
// Firefox sometimes makes this element focusable due to
|
// Firefox sometimes makes this element focusable due to
|
||||||
// overflow:scroll;, so force it out of tab order.
|
// overflow:scroll;, so force it out of tab order.
|
||||||
|
@ -454,7 +472,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
{roomList}
|
{roomList}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ !this.props.isMinimized && <LeftPanelWidget onResize={this.onResize} /> }
|
{ !this.props.isMinimized && <LeftPanelWidget /> }
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useContext, useEffect, useMemo} from "react";
|
import React, {useContext, useMemo} from "react";
|
||||||
import {Resizable} from "re-resizable";
|
import {Resizable} from "re-resizable";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
@ -27,16 +27,13 @@ import WidgetUtils, {IWidgetEvent} from "../../utils/WidgetUtils";
|
||||||
import {useAccountData} from "../../hooks/useAccountData";
|
import {useAccountData} from "../../hooks/useAccountData";
|
||||||
import AppTile from "../views/elements/AppTile";
|
import AppTile from "../views/elements/AppTile";
|
||||||
import {useSettingValue} from "../../hooks/useSettings";
|
import {useSettingValue} from "../../hooks/useSettings";
|
||||||
|
import UIStore from "../../stores/UIStore";
|
||||||
interface IProps {
|
|
||||||
onResize(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MIN_HEIGHT = 100;
|
const MIN_HEIGHT = 100;
|
||||||
const MAX_HEIGHT = 500; // or 50% of the window height
|
const MAX_HEIGHT = 500; // or 50% of the window height
|
||||||
const INITIAL_HEIGHT = 280;
|
const INITIAL_HEIGHT = 280;
|
||||||
|
|
||||||
const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
|
const LeftPanelWidget: React.FC = () => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
const mWidgetsEvent = useAccountData<Record<string, IWidgetEvent>>(cli, "m.widgets");
|
const mWidgetsEvent = useAccountData<Record<string, IWidgetEvent>>(cli, "m.widgets");
|
||||||
|
@ -56,7 +53,6 @@ const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
|
||||||
|
|
||||||
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
|
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
|
||||||
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
|
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
|
||||||
useEffect(onResize, [expanded, onResize]);
|
|
||||||
|
|
||||||
const [onFocus, isActive, ref] = useRovingTabIndex();
|
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||||
const tabIndex = isActive ? 0 : -1;
|
const tabIndex = isActive ? 0 : -1;
|
||||||
|
@ -68,8 +64,7 @@ const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
|
||||||
content = <Resizable
|
content = <Resizable
|
||||||
size={{height} as any}
|
size={{height} as any}
|
||||||
minHeight={MIN_HEIGHT}
|
minHeight={MIN_HEIGHT}
|
||||||
maxHeight={Math.min(window.innerHeight / 2, MAX_HEIGHT)}
|
maxHeight={Math.min(UIStore.instance.windowHeight / 2, MAX_HEIGHT)}
|
||||||
onResize={onResize}
|
|
||||||
onResizeStop={(e, dir, ref, d) => {
|
onResizeStop={(e, dir, ref, d) => {
|
||||||
setHeight(height + d.height);
|
setHeight(height + d.height);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -358,7 +358,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
|
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
|
||||||
for (const eventId of pinnedEventIds) {
|
for (const eventId of pinnedEventIds) {
|
||||||
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
|
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
|
||||||
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
|
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
|
||||||
if (event) events.push(event);
|
if (event) events.push(event);
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,9 @@ import {RoomUpdateCause} from "../../stores/room-list/models";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import SecurityCustomisations from "../../customisations/Security";
|
import SecurityCustomisations from "../../customisations/Security";
|
||||||
|
|
||||||
|
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
|
||||||
|
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
||||||
|
|
||||||
/** constants for MatrixChat.state.view */
|
/** constants for MatrixChat.state.view */
|
||||||
export enum Views {
|
export enum Views {
|
||||||
// a special initial state which is only used at startup, while we are
|
// a special initial state which is only used at startup, while we are
|
||||||
|
@ -223,13 +226,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
firstSyncPromise: IDeferred<void>;
|
firstSyncPromise: IDeferred<void>;
|
||||||
|
|
||||||
private screenAfterLogin?: IScreen;
|
private screenAfterLogin?: IScreen;
|
||||||
private windowWidth: number;
|
|
||||||
private pageChanging: boolean;
|
private pageChanging: boolean;
|
||||||
private tokenLogin?: boolean;
|
private tokenLogin?: boolean;
|
||||||
private accountPassword?: string;
|
private accountPassword?: string;
|
||||||
private accountPasswordTimer?: NodeJS.Timeout;
|
private accountPasswordTimer?: NodeJS.Timeout;
|
||||||
private focusComposer: boolean;
|
private focusComposer: boolean;
|
||||||
private subTitleStatus: string;
|
private subTitleStatus: string;
|
||||||
|
private prevWindowWidth: number;
|
||||||
|
|
||||||
private readonly loggedInView: React.RefObject<LoggedInViewType>;
|
private readonly loggedInView: React.RefObject<LoggedInViewType>;
|
||||||
private readonly dispatcherRef: any;
|
private readonly dispatcherRef: any;
|
||||||
|
@ -275,9 +278,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.windowWidth = 10000;
|
this.prevWindowWidth = UIStore.instance.windowWidth || 1000;
|
||||||
this.handleResize();
|
UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
|
||||||
window.addEventListener('resize', this.handleResize);
|
|
||||||
|
|
||||||
this.pageChanging = false;
|
this.pageChanging = false;
|
||||||
|
|
||||||
|
@ -376,7 +378,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
this.onLoggedIn();
|
this.onLoggedIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
const promisesList = [this.firstSyncPromise.promise];
|
const promisesList: Promise<any>[] = [this.firstSyncPromise.promise];
|
||||||
if (cryptoEnabled) {
|
if (cryptoEnabled) {
|
||||||
// wait for the client to finish downloading cross-signing keys for us so we
|
// wait for the client to finish downloading cross-signing keys for us so we
|
||||||
// know whether or not we have keys set up on this account
|
// know whether or not we have keys set up on this account
|
||||||
|
@ -434,7 +436,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
this.themeWatcher.stop();
|
this.themeWatcher.stop();
|
||||||
this.fontWatcher.stop();
|
this.fontWatcher.stop();
|
||||||
window.removeEventListener('resize', this.handleResize);
|
UIStore.destroy();
|
||||||
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
|
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
|
||||||
|
|
||||||
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
||||||
|
@ -484,42 +486,22 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
startPageChangeTimer() {
|
startPageChangeTimer() {
|
||||||
// Tor doesn't support performance
|
PerformanceMonitor.instance.start(PerformanceEntryNames.PAGE_CHANGE);
|
||||||
if (!performance || !performance.mark) return null;
|
|
||||||
|
|
||||||
// This shouldn't happen because UNSAFE_componentWillUpdate and componentDidUpdate
|
|
||||||
// are used.
|
|
||||||
if (this.pageChanging) {
|
|
||||||
console.warn('MatrixChat.startPageChangeTimer: timer already started');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.pageChanging = true;
|
|
||||||
performance.mark('element_MatrixChat_page_change_start');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stopPageChangeTimer() {
|
stopPageChangeTimer() {
|
||||||
// Tor doesn't support performance
|
const perfMonitor = PerformanceMonitor.instance;
|
||||||
if (!performance || !performance.mark) return null;
|
|
||||||
|
|
||||||
if (!this.pageChanging) {
|
perfMonitor.stop(PerformanceEntryNames.PAGE_CHANGE);
|
||||||
console.warn('MatrixChat.stopPageChangeTimer: timer not started');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.pageChanging = false;
|
|
||||||
performance.mark('element_MatrixChat_page_change_stop');
|
|
||||||
performance.measure(
|
|
||||||
'element_MatrixChat_page_change_delta',
|
|
||||||
'element_MatrixChat_page_change_start',
|
|
||||||
'element_MatrixChat_page_change_stop',
|
|
||||||
);
|
|
||||||
performance.clearMarks('element_MatrixChat_page_change_start');
|
|
||||||
performance.clearMarks('element_MatrixChat_page_change_stop');
|
|
||||||
const measurement = performance.getEntriesByName('element_MatrixChat_page_change_delta').pop();
|
|
||||||
|
|
||||||
// In practice, sometimes the entries list is empty, so we get no measurement
|
const entries = perfMonitor.getEntries({
|
||||||
if (!measurement) return null;
|
name: PerformanceEntryNames.PAGE_CHANGE,
|
||||||
|
});
|
||||||
|
const measurement = entries.pop();
|
||||||
|
|
||||||
return measurement.duration;
|
return measurement
|
||||||
|
? measurement.duration
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldTrackPageChange(prevState: IState, state: IState) {
|
shouldTrackPageChange(prevState: IState, state: IState) {
|
||||||
|
@ -683,7 +665,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'view_create_room':
|
case 'view_create_room':
|
||||||
this.createRoom(payload.public);
|
this.createRoom(payload.public, payload.defaultName);
|
||||||
break;
|
break;
|
||||||
case 'view_create_group': {
|
case 'view_create_group': {
|
||||||
let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog")
|
let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog")
|
||||||
|
@ -1029,7 +1011,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createRoom(defaultPublic = false) {
|
private async createRoom(defaultPublic = false, defaultName?: string) {
|
||||||
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||||
if (communityId) {
|
if (communityId) {
|
||||||
// double check the user will have permission to associate this room with the community
|
// double check the user will have permission to associate this room with the community
|
||||||
|
@ -1043,7 +1025,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
||||||
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { defaultPublic });
|
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
|
||||||
|
defaultPublic,
|
||||||
|
defaultName,
|
||||||
|
});
|
||||||
|
|
||||||
const [shouldCreate, opts] = await modal.finished;
|
const [shouldCreate, opts] = await modal.finished;
|
||||||
if (shouldCreate) {
|
if (shouldCreate) {
|
||||||
|
@ -1632,11 +1617,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
action: 'start_registration',
|
action: 'start_registration',
|
||||||
params: params,
|
params: params,
|
||||||
});
|
});
|
||||||
|
PerformanceMonitor.instance.start(PerformanceEntryNames.REGISTER);
|
||||||
} else if (screen === 'login') {
|
} else if (screen === 'login') {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'start_login',
|
action: 'start_login',
|
||||||
params: params,
|
params: params,
|
||||||
});
|
});
|
||||||
|
PerformanceMonitor.instance.start(PerformanceEntryNames.LOGIN);
|
||||||
} else if (screen === 'forgot_password') {
|
} else if (screen === 'forgot_password') {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'start_password_recovery',
|
action: 'start_password_recovery',
|
||||||
|
@ -1833,18 +1820,19 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResize = () => {
|
handleResize = () => {
|
||||||
const hideLhsThreshold = 1000;
|
const LHS_THRESHOLD = 1000;
|
||||||
const showLhsThreshold = 1000;
|
const width = UIStore.instance.windowWidth;
|
||||||
|
|
||||||
if (this.windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
if (this.prevWindowWidth < LHS_THRESHOLD && width >= LHS_THRESHOLD) {
|
||||||
dis.dispatch({ action: 'hide_left_panel' });
|
|
||||||
}
|
|
||||||
if (this.windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
|
|
||||||
dis.dispatch({ action: 'show_left_panel' });
|
dis.dispatch({ action: 'show_left_panel' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.prevWindowWidth >= LHS_THRESHOLD && width < LHS_THRESHOLD) {
|
||||||
|
dis.dispatch({ action: 'hide_left_panel' });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prevWindowWidth = width;
|
||||||
this.state.resizeNotifier.notifyWindowResized();
|
this.state.resizeNotifier.notifyWindowResized();
|
||||||
this.windowWidth = window.innerWidth;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private dispatchTimelineResize() {
|
private dispatchTimelineResize() {
|
||||||
|
@ -1965,6 +1953,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
// Create and start the client
|
// Create and start the client
|
||||||
await Lifecycle.setLoggedIn(credentials);
|
await Lifecycle.setLoggedIn(credentials);
|
||||||
await this.postLoginSetup();
|
await this.postLoginSetup();
|
||||||
|
PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN);
|
||||||
|
PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER);
|
||||||
};
|
};
|
||||||
|
|
||||||
// complete security / e2e setup has finished
|
// complete security / e2e setup has finished
|
||||||
|
@ -2101,6 +2091,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined}
|
onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined}
|
||||||
onServerConfigChange={this.onServerConfigChange}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
fragmentAfterLogin={fragmentAfterLogin}
|
fragmentAfterLogin={fragmentAfterLogin}
|
||||||
|
defaultUsername={this.props.startingFragmentQueryParams.defaultUsername}
|
||||||
{...this.getServerProperties()}
|
{...this.getServerProperties()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,17 +19,17 @@ limitations under the License.
|
||||||
import React, {createRef} from 'react';
|
import React, {createRef} from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
|
||||||
import shouldHideEvent from '../../shouldHideEvent';
|
import shouldHideEvent from '../../shouldHideEvent';
|
||||||
import {wantsDateSeparator} from '../../DateUtils';
|
import {wantsDateSeparator} from '../../DateUtils';
|
||||||
import * as sdk from '../../index';
|
import * as sdk from '../../index';
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||||
import SettingsStore from '../../settings/SettingsStore';
|
import SettingsStore from '../../settings/SettingsStore';
|
||||||
|
import RoomContext from "../../contexts/RoomContext";
|
||||||
import {Layout, LayoutPropType} from "../../settings/Layout";
|
import {Layout, LayoutPropType} from "../../settings/Layout";
|
||||||
import {_t} from "../../languageHandler";
|
import {_t} from "../../languageHandler";
|
||||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||||
import {textForEvent} from "../../TextForEvent";
|
import {hasText} from "../../TextForEvent";
|
||||||
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
||||||
import DMRoomMap from "../../utils/DMRoomMap";
|
import DMRoomMap from "../../utils/DMRoomMap";
|
||||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||||
|
@ -121,6 +121,9 @@ export default class MessagePanel extends React.Component {
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
|
||||||
|
// callback which is called when the user interacts with the room timeline
|
||||||
|
onUserScroll: PropTypes.func,
|
||||||
|
|
||||||
// callback which is called when more content is needed.
|
// callback which is called when more content is needed.
|
||||||
onFillRequest: PropTypes.func,
|
onFillRequest: PropTypes.func,
|
||||||
|
|
||||||
|
@ -149,6 +152,8 @@ export default class MessagePanel extends React.Component {
|
||||||
enableFlair: PropTypes.bool,
|
enableFlair: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static contextType = RoomContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -378,7 +383,7 @@ export default class MessagePanel extends React.Component {
|
||||||
// Always show highlighted event
|
// Always show highlighted event
|
||||||
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
||||||
|
|
||||||
return !shouldHideEvent(mxEv);
|
return !shouldHideEvent(mxEv, this.context);
|
||||||
}
|
}
|
||||||
|
|
||||||
_readMarkerForEvent(eventId, isLastEvent) {
|
_readMarkerForEvent(eventId, isLastEvent) {
|
||||||
|
@ -613,10 +618,6 @@ export default class MessagePanel extends React.Component {
|
||||||
const eventId = mxEv.getId();
|
const eventId = mxEv.getId();
|
||||||
const highlight = (eventId === this.props.highlightedEventId);
|
const highlight = (eventId === this.props.highlightedEventId);
|
||||||
|
|
||||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
|
||||||
// Local echos have a send "status".
|
|
||||||
const scrollToken = mxEv.status ? undefined : eventId;
|
|
||||||
|
|
||||||
const readReceipts = this._readReceiptsByEvent[eventId];
|
const readReceipts = this._readReceiptsByEvent[eventId];
|
||||||
|
|
||||||
let isLastSuccessful = false;
|
let isLastSuccessful = false;
|
||||||
|
@ -645,39 +646,36 @@ export default class MessagePanel extends React.Component {
|
||||||
|
|
||||||
// use txnId as key if available so that we don't remount during sending
|
// use txnId as key if available so that we don't remount during sending
|
||||||
ret.push(
|
ret.push(
|
||||||
<li
|
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
||||||
key={mxEv.getTxnId() || eventId}
|
<EventTile
|
||||||
ref={this._collectEventNode.bind(this, eventId)}
|
as="li"
|
||||||
data-scroll-tokens={scrollToken}
|
ref={this._collectEventNode.bind(this, eventId)}
|
||||||
>
|
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||||
<TileErrorBoundary mxEvent={mxEv}>
|
mxEvent={mxEv}
|
||||||
<EventTile
|
continuation={continuation}
|
||||||
mxEvent={mxEv}
|
isRedacted={mxEv.isRedacted()}
|
||||||
continuation={continuation}
|
replacingEventId={mxEv.replacingEventId()}
|
||||||
isRedacted={mxEv.isRedacted()}
|
editState={isEditing && this.props.editState}
|
||||||
replacingEventId={mxEv.replacingEventId()}
|
onHeightChanged={this._onHeightChanged}
|
||||||
editState={isEditing && this.props.editState}
|
readReceipts={readReceipts}
|
||||||
onHeightChanged={this._onHeightChanged}
|
readReceiptMap={this._readReceiptMap}
|
||||||
readReceipts={readReceipts}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
readReceiptMap={this._readReceiptMap}
|
checkUnmounting={this._isUnmounting}
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
eventSendStatus={mxEv.getAssociatedStatus()}
|
||||||
checkUnmounting={this._isUnmounting}
|
tileShape={this.props.tileShape}
|
||||||
eventSendStatus={mxEv.getAssociatedStatus()}
|
isTwelveHour={this.props.isTwelveHour}
|
||||||
tileShape={this.props.tileShape}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
isTwelveHour={this.props.isTwelveHour}
|
last={last}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
lastInSection={willWantDateSeparator}
|
||||||
last={last}
|
lastSuccessful={isLastSuccessful}
|
||||||
lastInSection={willWantDateSeparator}
|
isSelectedEvent={highlight}
|
||||||
lastSuccessful={isLastSuccessful}
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
isSelectedEvent={highlight}
|
showReactions={this.props.showReactions}
|
||||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
layout={this.props.layout}
|
||||||
showReactions={this.props.showReactions}
|
enableFlair={this.props.enableFlair}
|
||||||
layout={this.props.layout}
|
showReadReceipts={this.props.showReadReceipts}
|
||||||
enableFlair={this.props.enableFlair}
|
/>
|
||||||
showReadReceipts={this.props.showReadReceipts}
|
</TileErrorBoundary>,
|
||||||
/>
|
|
||||||
</TileErrorBoundary>
|
|
||||||
</li>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -779,7 +777,7 @@ export default class MessagePanel extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_collectEventNode = (eventId, node) => {
|
_collectEventNode = (eventId, node) => {
|
||||||
this.eventNodes[eventId] = node;
|
this.eventNodes[eventId] = node?.ref?.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
// once dynamic content in the events load, make the scrollPanel check the
|
// once dynamic content in the events load, make the scrollPanel check the
|
||||||
|
@ -853,13 +851,6 @@ export default class MessagePanel extends React.Component {
|
||||||
|
|
||||||
const style = this.props.hidden ? { display: 'none' } : {};
|
const style = this.props.hidden ? { display: 'none' } : {};
|
||||||
|
|
||||||
const className = classNames(
|
|
||||||
this.props.className,
|
|
||||||
{
|
|
||||||
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let whoIsTyping;
|
let whoIsTyping;
|
||||||
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
|
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
|
||||||
whoIsTyping = (<WhoIsTypingTile
|
whoIsTyping = (<WhoIsTypingTile
|
||||||
|
@ -883,8 +874,9 @@ export default class MessagePanel extends React.Component {
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ScrollPanel
|
<ScrollPanel
|
||||||
ref={this._scrollPanel}
|
ref={this._scrollPanel}
|
||||||
className={className}
|
className={this.props.className}
|
||||||
onScroll={this.props.onScroll}
|
onScroll={this.props.onScroll}
|
||||||
|
onUserScroll={this.props.onUserScroll}
|
||||||
onResize={this.onResize}
|
onResize={this.onResize}
|
||||||
onFillRequest={this.props.onFillRequest}
|
onFillRequest={this.props.onFillRequest}
|
||||||
onUnfillRequest={this.props.onUnfillRequest}
|
onUnfillRequest={this.props.onUnfillRequest}
|
||||||
|
@ -1175,11 +1167,8 @@ class MemberGrouper {
|
||||||
|
|
||||||
add(ev) {
|
add(ev) {
|
||||||
if (ev.getType() === 'm.room.member') {
|
if (ev.getType() === 'm.room.member') {
|
||||||
// We'll just double check that it's worth our time to do so, through an
|
// We can ignore any events that don't actually have a message to display
|
||||||
// ugly hack. If textForEvent returns something, we should group it for
|
if (!hasText(ev)) return;
|
||||||
// rendering but if it doesn't then we'll exclude it.
|
|
||||||
const renderText = textForEvent(ev);
|
|
||||||
if (!renderText || renderText.trim().length === 0) return; // quietly ignore
|
|
||||||
}
|
}
|
||||||
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
|
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
|
||||||
ev.getId(),
|
ev.getId(),
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 New Vector Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,29 +14,25 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import * as sdk from "../../index";
|
|
||||||
import BaseCard from "../views/right_panel/BaseCard";
|
import BaseCard from "../views/right_panel/BaseCard";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import TimelinePanel from "./TimelinePanel";
|
||||||
|
import Spinner from "../views/elements/Spinner";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Component which shows the global notification list using a TimelinePanel
|
* Component which shows the global notification list using a TimelinePanel
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("structures.NotificationPanel")
|
@replaceableComponent("structures.NotificationPanel")
|
||||||
class NotificationPanel extends React.Component {
|
export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||||
static propTypes = {
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
|
||||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
|
|
||||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||||
<h2>{_t('You’re all caught up')}</h2>
|
<h2>{_t('You’re all caught up')}</h2>
|
||||||
<p>{_t('You have no visible notifications.')}</p>
|
<p>{_t('You have no visible notifications.')}</p>
|
||||||
|
@ -47,6 +41,7 @@ class NotificationPanel extends React.Component {
|
||||||
let content;
|
let content;
|
||||||
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||||
if (timelineSet) {
|
if (timelineSet) {
|
||||||
|
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||||
content = (
|
content = (
|
||||||
<TimelinePanel
|
<TimelinePanel
|
||||||
manageReadReceipts={false}
|
manageReadReceipts={false}
|
||||||
|
@ -55,11 +50,12 @@ class NotificationPanel extends React.Component {
|
||||||
showUrlPreview={false}
|
showUrlPreview={false}
|
||||||
tileShape="notif"
|
tileShape="notif"
|
||||||
empty={emptyState}
|
empty={emptyState}
|
||||||
|
alwaysShowTimestamps={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error("No notifTimelineSet available!");
|
console.error("No notifTimelineSet available!");
|
||||||
content = <Loader />;
|
content = <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
||||||
|
@ -67,5 +63,3 @@ class NotificationPanel extends React.Component {
|
||||||
</BaseCard>;
|
</BaseCard>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotificationPanel;
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2015 - 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,70 +16,92 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
|
|
||||||
import * as sdk from '../../index';
|
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import RateLimitedFunc from '../../ratelimitedfunc';
|
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
|
||||||
import GroupStore from '../../stores/GroupStore';
|
import GroupStore from '../../stores/GroupStore';
|
||||||
import {
|
import {
|
||||||
RightPanelPhases,
|
|
||||||
RIGHT_PANEL_PHASES_NO_ARGS,
|
RIGHT_PANEL_PHASES_NO_ARGS,
|
||||||
RIGHT_PANEL_SPACE_PHASES,
|
RIGHT_PANEL_SPACE_PHASES,
|
||||||
|
RightPanelPhases,
|
||||||
} from "../../stores/RightPanelStorePhases";
|
} from "../../stores/RightPanelStorePhases";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import {Action} from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
import MemberList from "../views/rooms/MemberList";
|
||||||
|
import GroupMemberList from "../views/groups/GroupMemberList";
|
||||||
|
import GroupRoomList from "../views/groups/GroupRoomList";
|
||||||
|
import GroupRoomInfo from "../views/groups/GroupRoomInfo";
|
||||||
|
import UserInfo from "../views/right_panel/UserInfo";
|
||||||
|
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
|
||||||
|
import FilePanel from "./FilePanel";
|
||||||
|
import NotificationPanel from "./NotificationPanel";
|
||||||
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
|
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room?: Room; // if showing panels for a given room, this is set
|
||||||
|
groupId?: string; // if showing panels for a given group, this is set
|
||||||
|
user?: User; // used if we know the user ahead of opening the panel
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
phase: RightPanelPhases;
|
||||||
|
isUserPrivilegedInGroup?: boolean;
|
||||||
|
member?: RoomMember;
|
||||||
|
verificationRequest?: VerificationRequest;
|
||||||
|
verificationRequestPromise?: Promise<VerificationRequest>;
|
||||||
|
space?: Room;
|
||||||
|
widgetId?: string;
|
||||||
|
groupRoomId?: string;
|
||||||
|
groupId?: string;
|
||||||
|
event: MatrixEvent;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.RightPanel")
|
@replaceableComponent("structures.RightPanel")
|
||||||
export default class RightPanel extends React.Component {
|
export default class RightPanel extends React.Component<IProps, IState> {
|
||||||
static get propTypes() {
|
|
||||||
return {
|
|
||||||
room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set
|
|
||||||
groupId: PropTypes.string, // if showing panels for a given group, this is set
|
|
||||||
user: PropTypes.object, // used if we know the user ahead of opening the panel
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
|
private readonly delayedUpdate: RateLimitedFunc;
|
||||||
|
private dispatcherRef: string;
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
|
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
|
||||||
phase: this._getPhaseFromProps(),
|
phase: this.getPhaseFromProps(),
|
||||||
isUserPrivilegedInGroup: null,
|
isUserPrivilegedInGroup: null,
|
||||||
member: this._getUserForPanel(),
|
member: this.getUserForPanel(),
|
||||||
};
|
};
|
||||||
this.onAction = this.onAction.bind(this);
|
|
||||||
this.onRoomStateMember = this.onRoomStateMember.bind(this);
|
|
||||||
this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this);
|
|
||||||
this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this);
|
|
||||||
this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this);
|
|
||||||
|
|
||||||
this._delayedUpdate = new RateLimitedFunc(() => {
|
this.delayedUpdate = new RateLimitedFunc(() => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to split out the logic for _getPhaseFromProps() and the constructor
|
// Helper function to split out the logic for getPhaseFromProps() and the constructor
|
||||||
// as both are called at the same time in the constructor.
|
// as both are called at the same time in the constructor.
|
||||||
_getUserForPanel() {
|
private getUserForPanel() {
|
||||||
if (this.state && this.state.member) return this.state.member;
|
if (this.state && this.state.member) return this.state.member;
|
||||||
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
|
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
|
||||||
return this.props.user || lastParams['member'];
|
return this.props.user || lastParams['member'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// gets the current phase from the props and also maybe the store
|
// gets the current phase from the props and also maybe the store
|
||||||
_getPhaseFromProps() {
|
private getPhaseFromProps() {
|
||||||
const rps = RightPanelStore.getSharedInstance();
|
const rps = RightPanelStore.getSharedInstance();
|
||||||
const userForPanel = this._getUserForPanel();
|
const userForPanel = this.getUserForPanel();
|
||||||
if (this.props.groupId) {
|
if (this.props.groupId) {
|
||||||
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
|
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
|
||||||
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
|
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
|
||||||
|
@ -118,7 +140,7 @@ export default class RightPanel extends React.Component {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
const cli = this.context;
|
const cli = this.context;
|
||||||
cli.on("RoomState.members", this.onRoomStateMember);
|
cli.on("RoomState.members", this.onRoomStateMember);
|
||||||
this._initGroupStore(this.props.groupId);
|
this.initGroupStore(this.props.groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -126,61 +148,47 @@ export default class RightPanel extends React.Component {
|
||||||
if (this.context) {
|
if (this.context) {
|
||||||
this.context.removeListener("RoomState.members", this.onRoomStateMember);
|
this.context.removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
}
|
}
|
||||||
this._unregisterGroupStore(this.props.groupId);
|
this.unregisterGroupStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||||
if (newProps.groupId !== this.props.groupId) {
|
if (newProps.groupId !== this.props.groupId) {
|
||||||
this._unregisterGroupStore(this.props.groupId);
|
this.unregisterGroupStore();
|
||||||
this._initGroupStore(newProps.groupId);
|
this.initGroupStore(newProps.groupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_initGroupStore(groupId) {
|
private initGroupStore(groupId: string) {
|
||||||
if (!groupId) return;
|
if (!groupId) return;
|
||||||
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
_unregisterGroupStore() {
|
private unregisterGroupStore() {
|
||||||
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupStoreUpdated() {
|
private onGroupStoreUpdated = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onInviteToGroupButtonClick() {
|
private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => {
|
||||||
showGroupInviteDialog(this.props.groupId).then(() => {
|
|
||||||
this.setState({
|
|
||||||
phase: RightPanelPhases.GroupMemberList,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddRoomToGroupButtonClick() {
|
|
||||||
showGroupAddRoomDialog(this.props.groupId).then(() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onRoomStateMember(ev, state, member) {
|
|
||||||
if (!this.props.room || member.roomId !== this.props.room.roomId) {
|
if (!this.props.room || member.roomId !== this.props.room.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// redraw the badge on the membership list
|
// redraw the badge on the membership list
|
||||||
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
|
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
|
||||||
this._delayedUpdate();
|
this.delayedUpdate();
|
||||||
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
|
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
|
||||||
member.userId === this.state.member.userId) {
|
member.userId === this.state.member.userId) {
|
||||||
// refresh the member info (e.g. new power level)
|
// refresh the member info (e.g. new power level)
|
||||||
this._delayedUpdate();
|
this.delayedUpdate();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onAction(payload) {
|
private onAction = (payload: ActionPayload) => {
|
||||||
if (payload.action === Action.AfterRightPanelPhaseChange) {
|
if (payload.action === Action.AfterRightPanelPhaseChange) {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: payload.phase,
|
phase: payload.phase,
|
||||||
|
@ -194,9 +202,9 @@ export default class RightPanel extends React.Component {
|
||||||
space: payload.space,
|
space: payload.space,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onClose = () => {
|
private onClose = () => {
|
||||||
// XXX: There are three different ways of 'closing' this panel depending on what state
|
// XXX: There are three different ways of 'closing' this panel depending on what state
|
||||||
// things are in... this knows far more than it should do about the state of the rest
|
// things are in... this knows far more than it should do about the state of the rest
|
||||||
// of the app and is generally a bit silly.
|
// of the app and is generally a bit silly.
|
||||||
|
@ -224,16 +232,6 @@ export default class RightPanel extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
|
||||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
|
||||||
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
|
|
||||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
|
||||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
|
||||||
|
|
||||||
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
|
|
||||||
const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
|
|
||||||
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
|
|
||||||
|
|
||||||
let panel = <div />;
|
let panel = <div />;
|
||||||
const roomId = this.props.room ? this.props.room.roomId : undefined;
|
const roomId = this.props.room ? this.props.room.roomId : undefined;
|
||||||
|
|
||||||
|
@ -285,6 +283,7 @@ export default class RightPanel extends React.Component {
|
||||||
user={this.state.member}
|
user={this.state.member}
|
||||||
groupId={this.props.groupId}
|
groupId={this.props.groupId}
|
||||||
key={this.state.member.userId}
|
key={this.state.member.userId}
|
||||||
|
phase={this.state.phase}
|
||||||
onClose={this.onClose} />;
|
onClose={this.onClose} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -299,6 +298,12 @@ export default class RightPanel extends React.Component {
|
||||||
panel = <NotificationPanel onClose={this.onClose} />;
|
panel = <NotificationPanel onClose={this.onClose} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case RightPanelPhases.PinnedMessages:
|
||||||
|
if (SettingsStore.getValue("feature_pinning")) {
|
||||||
|
panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case RightPanelPhases.FilePanel:
|
case RightPanelPhases.FilePanel:
|
||||||
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
|
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
|
||||||
break;
|
break;
|
|
@ -1,7 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2015, 2016, 2019, 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,39 +15,90 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
|
||||||
import * as sdk from "../../index";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
|
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import SdkConfig from '../../SdkConfig';
|
import SdkConfig from '../../SdkConfig';
|
||||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||||
import Analytics from '../../Analytics';
|
import Analytics from '../../Analytics';
|
||||||
import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
|
import {ALL_ROOMS, IFieldType, IInstance, IProtocol, Protocols} from "../views/directory/NetworkDropdown";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
||||||
import GroupStore from "../../stores/GroupStore";
|
import GroupStore from "../../stores/GroupStore";
|
||||||
import FlairStore from "../../stores/FlairStore";
|
import FlairStore from "../../stores/FlairStore";
|
||||||
import CountlyAnalytics from "../../CountlyAnalytics";
|
import CountlyAnalytics from "../../CountlyAnalytics";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../customisations/Media";
|
import { mediaFromMxc } from "../../customisations/Media";
|
||||||
|
import { IDialogProps } from "../views/dialogs/IDialogProps";
|
||||||
|
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||||
|
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||||
|
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||||
|
import QuestionDialog from "../views/dialogs/QuestionDialog";
|
||||||
|
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||||
|
import DirectorySearchBox from "../views/elements/DirectorySearchBox";
|
||||||
|
import NetworkDropdown from "../views/directory/NetworkDropdown";
|
||||||
|
import ScrollPanel from "./ScrollPanel";
|
||||||
|
import Spinner from "../views/elements/Spinner";
|
||||||
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
|
||||||
|
|
||||||
const MAX_NAME_LENGTH = 80;
|
const MAX_NAME_LENGTH = 80;
|
||||||
const MAX_TOPIC_LENGTH = 800;
|
const MAX_TOPIC_LENGTH = 800;
|
||||||
|
|
||||||
function track(action) {
|
function track(action: string) {
|
||||||
Analytics.trackEvent('RoomDirectory', action);
|
Analytics.trackEvent('RoomDirectory', action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
initialText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
publicRooms: IRoom[];
|
||||||
|
loading: boolean;
|
||||||
|
protocolsLoading: boolean;
|
||||||
|
error?: string;
|
||||||
|
instanceId: string | symbol;
|
||||||
|
roomServer: string;
|
||||||
|
filterString: string;
|
||||||
|
selectedCommunityId?: string;
|
||||||
|
communityName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
interface IRoom {
|
||||||
|
room_id: string;
|
||||||
|
name?: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
topic?: string;
|
||||||
|
canonical_alias?: string;
|
||||||
|
aliases?: string[];
|
||||||
|
world_readable: boolean;
|
||||||
|
guest_can_join: boolean;
|
||||||
|
num_joined_members: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPublicRoomsRequest {
|
||||||
|
limit?: number;
|
||||||
|
since?: string;
|
||||||
|
server?: string;
|
||||||
|
filter?: object;
|
||||||
|
include_all_networks?: boolean;
|
||||||
|
third_party_instance_id?: string;
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
@replaceableComponent("structures.RoomDirectory")
|
@replaceableComponent("structures.RoomDirectory")
|
||||||
export default class RoomDirectory extends React.Component {
|
export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private readonly startTime: number;
|
||||||
initialText: PropTypes.string,
|
private unmounted = false
|
||||||
onFinished: PropTypes.func.isRequired,
|
private nextBatch: string = null;
|
||||||
};
|
private filterTimeout: NodeJS.Timeout;
|
||||||
|
private protocols: Protocols;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -56,41 +106,21 @@ export default class RoomDirectory extends React.Component {
|
||||||
CountlyAnalytics.instance.trackRoomDirectoryBegin();
|
CountlyAnalytics.instance.trackRoomDirectoryBegin();
|
||||||
this.startTime = CountlyAnalytics.getTimestamp();
|
this.startTime = CountlyAnalytics.getTimestamp();
|
||||||
|
|
||||||
const selectedCommunityId = GroupFilterOrderStore.getSelectedTags()[0];
|
const selectedCommunityId = SettingsStore.getValue("feature_communities_v2_prototypes")
|
||||||
this.state = {
|
? GroupFilterOrderStore.getSelectedTags()[0]
|
||||||
publicRooms: [],
|
: null;
|
||||||
loading: true,
|
|
||||||
protocolsLoading: true,
|
|
||||||
error: null,
|
|
||||||
instanceId: undefined,
|
|
||||||
roomServer: MatrixClientPeg.getHomeserverName(),
|
|
||||||
filterString: this.props.initialText || "",
|
|
||||||
selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes")
|
|
||||||
? selectedCommunityId
|
|
||||||
: null,
|
|
||||||
communityName: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
this._unmounted = false;
|
let protocolsLoading = true;
|
||||||
this.nextBatch = null;
|
|
||||||
this.filterTimeout = null;
|
|
||||||
this.scrollPanel = null;
|
|
||||||
this.protocols = null;
|
|
||||||
|
|
||||||
this.state.protocolsLoading = true;
|
|
||||||
if (!MatrixClientPeg.get()) {
|
if (!MatrixClientPeg.get()) {
|
||||||
// We may not have a client yet when invoked from welcome page
|
// We may not have a client yet when invoked from welcome page
|
||||||
this.state.protocolsLoading = false;
|
protocolsLoading = false;
|
||||||
return;
|
} else if (!selectedCommunityId) {
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.selectedCommunityId) {
|
|
||||||
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
|
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
|
||||||
this.protocols = response;
|
this.protocols = response;
|
||||||
this.setState({protocolsLoading: false});
|
this.setState({ protocolsLoading: false });
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.warn(`error loading third party protocols: ${err}`);
|
console.warn(`error loading third party protocols: ${err}`);
|
||||||
this.setState({protocolsLoading: false});
|
this.setState({ protocolsLoading: false });
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
// Guests currently aren't allowed to use this API, so
|
// Guests currently aren't allowed to use this API, so
|
||||||
// ignore this as otherwise this error is literally the
|
// ignore this as otherwise this error is literally the
|
||||||
|
@ -103,19 +133,31 @@ export default class RoomDirectory extends React.Component {
|
||||||
error: _t(
|
error: _t(
|
||||||
'%(brand)s failed to get the protocol list from the homeserver. ' +
|
'%(brand)s failed to get the protocol list from the homeserver. ' +
|
||||||
'The homeserver may be too old to support third party networks.',
|
'The homeserver may be too old to support third party networks.',
|
||||||
{brand},
|
{ brand },
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// We don't use the protocols in the communities v2 prototype experience
|
// We don't use the protocols in the communities v2 prototype experience
|
||||||
this.state.protocolsLoading = false;
|
protocolsLoading = false;
|
||||||
|
|
||||||
// Grab the profile info async
|
// Grab the profile info async
|
||||||
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
|
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
|
||||||
this.setState({communityName: profile.name});
|
this.setState({ communityName: profile.name });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
publicRooms: [],
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
instanceId: undefined,
|
||||||
|
roomServer: MatrixClientPeg.getHomeserverName(),
|
||||||
|
filterString: this.props.initialText || "",
|
||||||
|
selectedCommunityId,
|
||||||
|
communityName: null,
|
||||||
|
protocolsLoading,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -126,10 +168,10 @@ export default class RoomDirectory extends React.Component {
|
||||||
if (this.filterTimeout) {
|
if (this.filterTimeout) {
|
||||||
clearTimeout(this.filterTimeout);
|
clearTimeout(this.filterTimeout);
|
||||||
}
|
}
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshRoomList = () => {
|
private refreshRoomList = () => {
|
||||||
if (this.state.selectedCommunityId) {
|
if (this.state.selectedCommunityId) {
|
||||||
this.setState({
|
this.setState({
|
||||||
publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => {
|
publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => {
|
||||||
|
@ -165,7 +207,7 @@ export default class RoomDirectory extends React.Component {
|
||||||
this.getMoreRooms();
|
this.getMoreRooms();
|
||||||
};
|
};
|
||||||
|
|
||||||
getMoreRooms() {
|
private getMoreRooms() {
|
||||||
if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
|
if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
|
||||||
if (!MatrixClientPeg.get()) return Promise.resolve();
|
if (!MatrixClientPeg.get()) return Promise.resolve();
|
||||||
|
|
||||||
|
@ -173,34 +215,34 @@ export default class RoomDirectory extends React.Component {
|
||||||
loading: true,
|
loading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const my_filter_string = this.state.filterString;
|
const filterString = this.state.filterString;
|
||||||
const my_server = this.state.roomServer;
|
const roomServer = this.state.roomServer;
|
||||||
// remember the next batch token when we sent the request
|
// remember the next batch token when we sent the request
|
||||||
// too. If it's changed, appending to the list will corrupt it.
|
// too. If it's changed, appending to the list will corrupt it.
|
||||||
const my_next_batch = this.nextBatch;
|
const nextBatch = this.nextBatch;
|
||||||
const opts = {limit: 20};
|
const opts: IPublicRoomsRequest = { limit: 20 };
|
||||||
if (my_server != MatrixClientPeg.getHomeserverName()) {
|
if (roomServer != MatrixClientPeg.getHomeserverName()) {
|
||||||
opts.server = my_server;
|
opts.server = roomServer;
|
||||||
}
|
}
|
||||||
if (this.state.instanceId === ALL_ROOMS) {
|
if (this.state.instanceId === ALL_ROOMS) {
|
||||||
opts.include_all_networks = true;
|
opts.include_all_networks = true;
|
||||||
} else if (this.state.instanceId) {
|
} else if (this.state.instanceId) {
|
||||||
opts.third_party_instance_id = this.state.instanceId;
|
opts.third_party_instance_id = this.state.instanceId as string;
|
||||||
}
|
}
|
||||||
if (this.nextBatch) opts.since = this.nextBatch;
|
if (this.nextBatch) opts.since = this.nextBatch;
|
||||||
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string };
|
if (filterString) opts.filter = { generic_search_term: filterString };
|
||||||
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
|
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
|
||||||
if (
|
if (
|
||||||
my_filter_string != this.state.filterString ||
|
filterString != this.state.filterString ||
|
||||||
my_server != this.state.roomServer ||
|
roomServer != this.state.roomServer ||
|
||||||
my_next_batch != this.nextBatch) {
|
nextBatch != this.nextBatch) {
|
||||||
// if the filter or server has changed since this request was sent,
|
// if the filter or server has changed since this request was sent,
|
||||||
// throw away the result (don't even clear the busy flag
|
// throw away the result (don't even clear the busy flag
|
||||||
// since we must still have a request in flight)
|
// since we must still have a request in flight)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._unmounted) {
|
if (this.unmounted) {
|
||||||
// if we've been unmounted, we don't care either.
|
// if we've been unmounted, we don't care either.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -211,23 +253,23 @@ export default class RoomDirectory extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.nextBatch = data.next_batch;
|
this.nextBatch = data.next_batch;
|
||||||
this.setState((s) => {
|
this.setState((s) => ({
|
||||||
s.publicRooms.push(...(data.chunk || []));
|
...s,
|
||||||
s.loading = false;
|
publicRooms: [...s.publicRooms, ...(data.chunk || [])],
|
||||||
return s;
|
loading: false,
|
||||||
});
|
}));
|
||||||
return Boolean(data.next_batch);
|
return Boolean(data.next_batch);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
if (
|
if (
|
||||||
my_filter_string != this.state.filterString ||
|
filterString != this.state.filterString ||
|
||||||
my_server != this.state.roomServer ||
|
roomServer != this.state.roomServer ||
|
||||||
my_next_batch != this.nextBatch) {
|
nextBatch != this.nextBatch) {
|
||||||
// as above: we don't care about errors for old
|
// as above: we don't care about errors for old
|
||||||
// requests either
|
// requests either
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._unmounted) {
|
if (this.unmounted) {
|
||||||
// if we've been unmounted, we don't care either.
|
// if we've been unmounted, we don't care either.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -252,13 +294,10 @@ export default class RoomDirectory extends React.Component {
|
||||||
* HS admins to do this through the RoomSettings interface, but
|
* HS admins to do this through the RoomSettings interface, but
|
||||||
* this needs SPEC-417.
|
* this needs SPEC-417.
|
||||||
*/
|
*/
|
||||||
removeFromDirectory(room) {
|
private removeFromDirectory(room: IRoom) {
|
||||||
const alias = get_display_alias_for_room(room);
|
const alias = getDisplayAliasForRoom(room);
|
||||||
const name = room.name || alias || _t('Unnamed room');
|
const name = room.name || alias || _t('Unnamed room');
|
||||||
|
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
|
|
||||||
let desc;
|
let desc;
|
||||||
if (alias) {
|
if (alias) {
|
||||||
desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
|
desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
|
||||||
|
@ -269,11 +308,10 @@ export default class RoomDirectory extends React.Component {
|
||||||
Modal.createTrackedDialog('Remove from Directory', '', QuestionDialog, {
|
Modal.createTrackedDialog('Remove from Directory', '', QuestionDialog, {
|
||||||
title: _t('Remove from Directory'),
|
title: _t('Remove from Directory'),
|
||||||
description: desc,
|
description: desc,
|
||||||
onFinished: (should_delete) => {
|
onFinished: (shouldDelete: boolean) => {
|
||||||
if (!should_delete) return;
|
if (!shouldDelete) return;
|
||||||
|
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const modal = Modal.createDialog(Spinner);
|
||||||
const modal = Modal.createDialog(Loader);
|
|
||||||
let step = _t('remove %(name)s from the directory.', {name: name});
|
let step = _t('remove %(name)s from the directory.', {name: name});
|
||||||
|
|
||||||
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
|
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
|
||||||
|
@ -289,14 +327,16 @@ export default class RoomDirectory extends React.Component {
|
||||||
console.error("Failed to " + step + ": " + err);
|
console.error("Failed to " + step + ": " + err);
|
||||||
Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, {
|
Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, {
|
||||||
title: _t('Error'),
|
title: _t('Error'),
|
||||||
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')),
|
description: (err && err.message)
|
||||||
|
? err.message
|
||||||
|
: _t('The server may be unavailable or overloaded'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onRoomClicked = (room, ev) => {
|
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
|
||||||
if (ev.shiftKey && !this.state.selectedCommunityId) {
|
if (ev.shiftKey && !this.state.selectedCommunityId) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.removeFromDirectory(room);
|
this.removeFromDirectory(room);
|
||||||
|
@ -305,7 +345,7 @@ export default class RoomDirectory extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onOptionChange = (server, instanceId) => {
|
private onOptionChange = (server: string, instanceId?: string | symbol) => {
|
||||||
// clear next batch so we don't try to load more rooms
|
// clear next batch so we don't try to load more rooms
|
||||||
this.nextBatch = null;
|
this.nextBatch = null;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -325,13 +365,13 @@ export default class RoomDirectory extends React.Component {
|
||||||
// Easiest to just blow away the state & re-fetch.
|
// Easiest to just blow away the state & re-fetch.
|
||||||
};
|
};
|
||||||
|
|
||||||
onFillRequest = (backwards) => {
|
private onFillRequest = (backwards: boolean) => {
|
||||||
if (backwards || !this.nextBatch) return Promise.resolve(false);
|
if (backwards || !this.nextBatch) return Promise.resolve(false);
|
||||||
|
|
||||||
return this.getMoreRooms();
|
return this.getMoreRooms();
|
||||||
};
|
};
|
||||||
|
|
||||||
onFilterChange = (alias) => {
|
private onFilterChange = (alias: string) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
filterString: alias || null,
|
filterString: alias || null,
|
||||||
});
|
});
|
||||||
|
@ -349,7 +389,7 @@ export default class RoomDirectory extends React.Component {
|
||||||
}, 700);
|
}, 700);
|
||||||
};
|
};
|
||||||
|
|
||||||
onFilterClear = () => {
|
private onFilterClear = () => {
|
||||||
// update immediately
|
// update immediately
|
||||||
this.setState({
|
this.setState({
|
||||||
filterString: null,
|
filterString: null,
|
||||||
|
@ -360,7 +400,7 @@ export default class RoomDirectory extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onJoinFromSearchClick = (alias) => {
|
private onJoinFromSearchClick = (alias: string) => {
|
||||||
// If we don't have a particular instance id selected, just show that rooms alias
|
// If we don't have a particular instance id selected, just show that rooms alias
|
||||||
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||||
// If the user specified an alias without a domain, add on whichever server is selected
|
// If the user specified an alias without a domain, add on whichever server is selected
|
||||||
|
@ -373,9 +413,10 @@ export default class RoomDirectory extends React.Component {
|
||||||
// This is a 3rd party protocol. Let's see if we can join it
|
// This is a 3rd party protocol. Let's see if we can join it
|
||||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||||
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
|
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
|
||||||
const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null;
|
const fields = protocolName
|
||||||
|
? this.getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance)
|
||||||
|
: null;
|
||||||
if (!fields) {
|
if (!fields) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, {
|
Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, {
|
||||||
title: _t('Unable to join network'),
|
title: _t('Unable to join network'),
|
||||||
|
@ -387,14 +428,12 @@ export default class RoomDirectory extends React.Component {
|
||||||
if (resp.length > 0 && resp[0].alias) {
|
if (resp.length > 0 && resp[0].alias) {
|
||||||
this.showRoomAlias(resp[0].alias, true);
|
this.showRoomAlias(resp[0].alias, true);
|
||||||
} else {
|
} else {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
|
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
|
||||||
title: _t('Room not found'),
|
title: _t('Room not found'),
|
||||||
description: _t('Couldn\'t find a matching Matrix room'),
|
description: _t('Couldn\'t find a matching Matrix room'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, (e) => {
|
}, (e) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, {
|
Modal.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, {
|
||||||
title: _t('Fetching third party location failed'),
|
title: _t('Fetching third party location failed'),
|
||||||
description: _t('Unable to look up room ID from server'),
|
description: _t('Unable to look up room ID from server'),
|
||||||
|
@ -403,36 +442,37 @@ export default class RoomDirectory extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onPreviewClick = (ev, room) => {
|
private onPreviewClick = (ev: ButtonEvent, room: IRoom) => {
|
||||||
this.showRoom(room, null, false, true);
|
this.showRoom(room, null, false, true);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
onViewClick = (ev, room) => {
|
private onViewClick = (ev: ButtonEvent, room: IRoom) => {
|
||||||
this.showRoom(room);
|
this.showRoom(room);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
onJoinClick = (ev, room) => {
|
private onJoinClick = (ev: ButtonEvent, room: IRoom) => {
|
||||||
this.showRoom(room, null, true);
|
this.showRoom(room, null, true);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
onCreateRoomClick = room => {
|
private onCreateRoomClick = () => {
|
||||||
this.onFinished();
|
this.onFinished();
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_create_room',
|
action: 'view_create_room',
|
||||||
public: true,
|
public: true,
|
||||||
|
defaultName: this.state.filterString.trim(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
showRoomAlias(alias, autoJoin=false) {
|
private showRoomAlias(alias: string, autoJoin = false) {
|
||||||
this.showRoom(null, alias, autoJoin);
|
this.showRoom(null, alias, autoJoin);
|
||||||
}
|
}
|
||||||
|
|
||||||
showRoom(room, room_alias, autoJoin = false, shouldPeek = false) {
|
private showRoom(room: IRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) {
|
||||||
this.onFinished();
|
this.onFinished();
|
||||||
const payload = {
|
const payload: ActionPayload = {
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
auto_join: autoJoin,
|
auto_join: autoJoin,
|
||||||
should_peek: shouldPeek,
|
should_peek: shouldPeek,
|
||||||
|
@ -449,15 +489,15 @@ export default class RoomDirectory extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!room_alias) {
|
if (!roomAlias) {
|
||||||
room_alias = get_display_alias_for_room(room);
|
roomAlias = getDisplayAliasForRoom(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
payload.oob_data = {
|
payload.oob_data = {
|
||||||
avatarUrl: room.avatar_url,
|
avatarUrl: room.avatar_url,
|
||||||
// XXX: This logic is duplicated from the JS SDK which
|
// XXX: This logic is duplicated from the JS SDK which
|
||||||
// would normally decide what the name is.
|
// would normally decide what the name is.
|
||||||
name: room.name || room_alias || _t('Unnamed room'),
|
name: room.name || roomAlias || _t('Unnamed room'),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.state.roomServer) {
|
if (this.state.roomServer) {
|
||||||
|
@ -471,21 +511,19 @@ export default class RoomDirectory extends React.Component {
|
||||||
// which servers to start querying. However, there's no other way to join rooms in
|
// which servers to start querying. However, there's no other way to join rooms in
|
||||||
// this list without aliases at present, so if roomAlias isn't set here we have no
|
// this list without aliases at present, so if roomAlias isn't set here we have no
|
||||||
// choice but to supply the ID.
|
// choice but to supply the ID.
|
||||||
if (room_alias) {
|
if (roomAlias) {
|
||||||
payload.room_alias = room_alias;
|
payload.room_alias = roomAlias;
|
||||||
} else {
|
} else {
|
||||||
payload.room_id = room.room_id;
|
payload.room_id = room.room_id;
|
||||||
}
|
}
|
||||||
dis.dispatch(payload);
|
dis.dispatch(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoomCells(room) {
|
private createRoomCells(room: IRoom) {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const clientRoom = client.getRoom(room.room_id);
|
const clientRoom = client.getRoom(room.room_id);
|
||||||
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
|
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
|
||||||
const isGuest = client.isGuest();
|
const isGuest = client.isGuest();
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
let previewButton;
|
let previewButton;
|
||||||
let joinOrViewButton;
|
let joinOrViewButton;
|
||||||
|
|
||||||
|
@ -495,20 +533,26 @@ export default class RoomDirectory extends React.Component {
|
||||||
// it is readable, the preview appears as normal.
|
// it is readable, the preview appears as normal.
|
||||||
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
|
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
|
||||||
previewButton = (
|
previewButton = (
|
||||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onPreviewClick(ev, room)}>{_t("Preview")}</AccessibleButton>
|
<AccessibleButton kind="secondary" onClick={(ev) => this.onPreviewClick(ev, room)}>
|
||||||
|
{ _t("Preview") }
|
||||||
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (hasJoinedRoom) {
|
if (hasJoinedRoom) {
|
||||||
joinOrViewButton = (
|
joinOrViewButton = (
|
||||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onViewClick(ev, room)}>{_t("View")}</AccessibleButton>
|
<AccessibleButton kind="secondary" onClick={(ev) => this.onViewClick(ev, room)}>
|
||||||
|
{ _t("View") }
|
||||||
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
} else if (!isGuest) {
|
} else if (!isGuest) {
|
||||||
joinOrViewButton = (
|
joinOrViewButton = (
|
||||||
<AccessibleButton kind="primary" onClick={(ev) => this.onJoinClick(ev, room)}>{_t("Join")}</AccessibleButton>
|
<AccessibleButton kind="primary" onClick={(ev) => this.onJoinClick(ev, room)}>
|
||||||
|
{ _t("Join") }
|
||||||
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = room.name || get_display_alias_for_room(room) || _t('Unnamed room');
|
let name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
|
||||||
if (name.length > MAX_NAME_LENGTH) {
|
if (name.length > MAX_NAME_LENGTH) {
|
||||||
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
|
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
|
||||||
}
|
}
|
||||||
|
@ -531,9 +575,13 @@ export default class RoomDirectory extends React.Component {
|
||||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||||
className="mx_RoomDirectory_roomAvatar"
|
className="mx_RoomDirectory_roomAvatar"
|
||||||
>
|
>
|
||||||
<BaseAvatar width={32} height={32} resizeMethod='crop'
|
<BaseAvatar
|
||||||
name={ name } idName={ name }
|
width={32}
|
||||||
url={ avatarUrl }
|
height={32}
|
||||||
|
resizeMethod='crop'
|
||||||
|
name={name}
|
||||||
|
idName={name}
|
||||||
|
url={avatarUrl}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
<div key={ `${room.room_id}_description` }
|
<div key={ `${room.room_id}_description` }
|
||||||
|
@ -547,7 +595,7 @@ export default class RoomDirectory extends React.Component {
|
||||||
onClick={ (ev) => { ev.stopPropagation(); } }
|
onClick={ (ev) => { ev.stopPropagation(); } }
|
||||||
dangerouslySetInnerHTML={{ __html: topic }}
|
dangerouslySetInnerHTML={{ __html: topic }}
|
||||||
/>
|
/>
|
||||||
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div>
|
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>
|
||||||
</div>,
|
</div>,
|
||||||
<div key={ `${room.room_id}_memberCount` }
|
<div key={ `${room.room_id}_memberCount` }
|
||||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||||
|
@ -576,20 +624,16 @@ export default class RoomDirectory extends React.Component {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
collectScrollPanel = (element) => {
|
private stringLooksLikeId(s: string, fieldType: IFieldType) {
|
||||||
this.scrollPanel = element;
|
|
||||||
};
|
|
||||||
|
|
||||||
_stringLooksLikeId(s, field_type) {
|
|
||||||
let pat = /^#[^\s]+:[^\s]/;
|
let pat = /^#[^\s]+:[^\s]/;
|
||||||
if (field_type && field_type.regexp) {
|
if (fieldType && fieldType.regexp) {
|
||||||
pat = new RegExp(field_type.regexp);
|
pat = new RegExp(fieldType.regexp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pat.test(s);
|
return pat.test(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getFieldsForThirdPartyLocation(userInput, protocol, instance) {
|
private getFieldsForThirdPartyLocation(userInput: string, protocol: IProtocol, instance: IInstance) {
|
||||||
// make an object with the fields specified by that protocol. We
|
// make an object with the fields specified by that protocol. We
|
||||||
// require that the values of all but the last field come from the
|
// require that the values of all but the last field come from the
|
||||||
// instance. The last is the user input.
|
// instance. The last is the user input.
|
||||||
|
@ -605,71 +649,73 @@ export default class RoomDirectory extends React.Component {
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private onFinished = () => {
|
||||||
* called by the parent component when PageUp/Down/etc is pressed.
|
|
||||||
*
|
|
||||||
* We pass it down to the scroll panel.
|
|
||||||
*/
|
|
||||||
handleScrollKey = ev => {
|
|
||||||
if (this.scrollPanel) {
|
|
||||||
this.scrollPanel.handleScrollKey(ev);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onFinished = () => {
|
|
||||||
CountlyAnalytics.instance.trackRoomDirectory(this.startTime);
|
CountlyAnalytics.instance.trackRoomDirectory(this.startTime);
|
||||||
this.props.onFinished();
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
content = this.state.error;
|
content = this.state.error;
|
||||||
} else if (this.state.protocolsLoading) {
|
} else if (this.state.protocolsLoading) {
|
||||||
content = <Loader />;
|
content = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
const cells = (this.state.publicRooms || [])
|
const cells = (this.state.publicRooms || [])
|
||||||
.reduce((cells, room) => cells.concat(this.createRoomCells(room)), [],);
|
.reduce((cells, room) => cells.concat(this.createRoomCells(room)), []);
|
||||||
// we still show the scrollpanel, at least for now, because
|
// we still show the scrollpanel, at least for now, because
|
||||||
// otherwise we don't fetch more because we don't get a fill
|
// otherwise we don't fetch more because we don't get a fill
|
||||||
// request from the scrollpanel because there isn't one
|
// request from the scrollpanel because there isn't one
|
||||||
|
|
||||||
let spinner;
|
let spinner;
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
spinner = <Loader />;
|
spinner = <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let scrollpanel_content;
|
const createNewButton = <>
|
||||||
|
<hr />
|
||||||
|
<AccessibleButton kind="primary" onClick={this.onCreateRoomClick} className="mx_RoomDirectory_newRoom">
|
||||||
|
{ _t("Create new room") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</>;
|
||||||
|
|
||||||
|
let scrollPanelContent;
|
||||||
|
let footer;
|
||||||
if (cells.length === 0 && !this.state.loading) {
|
if (cells.length === 0 && !this.state.loading) {
|
||||||
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
|
footer = <>
|
||||||
|
<h5>{ _t('No results for "%(query)s"', { query: this.state.filterString.trim() }) }</h5>
|
||||||
|
<p>
|
||||||
|
{ _t("Try different words or check for typos. " +
|
||||||
|
"Some results may not be visible as they're private and you need an invite to join them.") }
|
||||||
|
</p>
|
||||||
|
{ createNewButton }
|
||||||
|
</>;
|
||||||
} else {
|
} else {
|
||||||
scrollpanel_content = <div className="mx_RoomDirectory_table">
|
scrollPanelContent = <div className="mx_RoomDirectory_table">
|
||||||
{ cells }
|
{ cells }
|
||||||
</div>;
|
</div>;
|
||||||
|
if (!this.state.loading && !this.nextBatch) {
|
||||||
|
footer = createNewButton;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
content = <ScrollPanel
|
||||||
content = <ScrollPanel ref={this.collectScrollPanel}
|
|
||||||
className="mx_RoomDirectory_tableWrapper"
|
className="mx_RoomDirectory_tableWrapper"
|
||||||
onFillRequest={ this.onFillRequest }
|
onFillRequest={this.onFillRequest}
|
||||||
stickyBottom={false}
|
stickyBottom={false}
|
||||||
startAtBottom={false}
|
startAtBottom={false}
|
||||||
>
|
>
|
||||||
{ scrollpanel_content }
|
{ scrollPanelContent }
|
||||||
{ spinner }
|
{ spinner }
|
||||||
|
{ footer && <div className="mx_RoomDirectory_footer">
|
||||||
|
{ footer }
|
||||||
|
</div> }
|
||||||
</ScrollPanel>;
|
</ScrollPanel>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let listHeader;
|
let listHeader;
|
||||||
if (!this.state.protocolsLoading) {
|
if (!this.state.protocolsLoading) {
|
||||||
const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
|
|
||||||
const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
|
|
||||||
|
|
||||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||||
let instance_expected_field_type;
|
let instanceExpectedFieldType;
|
||||||
if (
|
if (
|
||||||
protocolName &&
|
protocolName &&
|
||||||
this.protocols &&
|
this.protocols &&
|
||||||
|
@ -677,21 +723,27 @@ export default class RoomDirectory extends React.Component {
|
||||||
this.protocols[protocolName].location_fields.length > 0 &&
|
this.protocols[protocolName].location_fields.length > 0 &&
|
||||||
this.protocols[protocolName].field_types
|
this.protocols[protocolName].field_types
|
||||||
) {
|
) {
|
||||||
const last_field = this.protocols[protocolName].location_fields.slice(-1)[0];
|
const lastField = this.protocols[protocolName].location_fields.slice(-1)[0];
|
||||||
instance_expected_field_type = this.protocols[protocolName].field_types[last_field];
|
instanceExpectedFieldType = this.protocols[protocolName].field_types[lastField];
|
||||||
}
|
}
|
||||||
|
|
||||||
let placeholder = _t('Find a room…');
|
let placeholder = _t('Find a room…');
|
||||||
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||||
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer});
|
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {
|
||||||
} else if (instance_expected_field_type) {
|
exampleRoom: "#example:" + this.state.roomServer,
|
||||||
placeholder = instance_expected_field_type.placeholder;
|
});
|
||||||
|
} else if (instanceExpectedFieldType) {
|
||||||
|
placeholder = instanceExpectedFieldType.placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
let showJoinButton = this._stringLooksLikeId(this.state.filterString, instance_expected_field_type);
|
let showJoinButton = this.stringLooksLikeId(this.state.filterString, instanceExpectedFieldType);
|
||||||
if (protocolName) {
|
if (protocolName) {
|
||||||
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
|
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
|
||||||
if (this._getFieldsForThirdPartyLocation(this.state.filterString, this.protocols[protocolName], instance) === null) {
|
if (this.getFieldsForThirdPartyLocation(
|
||||||
|
this.state.filterString,
|
||||||
|
this.protocols[protocolName],
|
||||||
|
instance,
|
||||||
|
) === null) {
|
||||||
showJoinButton = false;
|
showJoinButton = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -723,12 +775,11 @@ export default class RoomDirectory extends React.Component {
|
||||||
}
|
}
|
||||||
const explanation =
|
const explanation =
|
||||||
_t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
|
_t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
|
||||||
{a: sub => {
|
{a: sub => (
|
||||||
return (<AccessibleButton
|
<AccessibleButton kind="secondary" onClick={this.onCreateRoomClick}>
|
||||||
kind="secondary"
|
{ sub }
|
||||||
onClick={this.onCreateRoomClick}
|
</AccessibleButton>
|
||||||
>{sub}</AccessibleButton>);
|
)},
|
||||||
}},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const title = this.state.selectedCommunityId
|
const title = this.state.selectedCommunityId
|
||||||
|
@ -756,6 +807,6 @@ export default class RoomDirectory extends React.Component {
|
||||||
|
|
||||||
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
||||||
// but works with the objects we get from the public room list
|
// but works with the objects we get from the public room list
|
||||||
function get_display_alias_for_room(room) {
|
function getDisplayAliasForRoom(room: IRoom) {
|
||||||
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
|
return room.canonical_alias || room.aliases?.[0] || "";
|
||||||
}
|
}
|
|
@ -46,7 +46,7 @@ import RoomViewStore from '../../stores/RoomViewStore';
|
||||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import {Layout} from "../../settings/Layout";
|
import { Layout } from "../../settings/Layout";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||||
|
@ -54,16 +54,13 @@ import RoomContext from "../../contexts/RoomContext";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { SettingLevel } from "../../settings/SettingLevel";
|
|
||||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||||
import ScrollPanel from "./ScrollPanel";
|
import ScrollPanel from "./ScrollPanel";
|
||||||
import TimelinePanel from "./TimelinePanel";
|
import TimelinePanel from "./TimelinePanel";
|
||||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||||
import ForwardMessage from "../views/rooms/ForwardMessage";
|
|
||||||
import SearchBar from "../views/rooms/SearchBar";
|
import SearchBar from "../views/rooms/SearchBar";
|
||||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||||
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
|
|
||||||
import AuxPanel from "../views/rooms/AuxPanel";
|
import AuxPanel from "../views/rooms/AuxPanel";
|
||||||
import RoomHeader from "../views/rooms/RoomHeader";
|
import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
import { XOR } from "../../@types/common";
|
import { XOR } from "../../@types/common";
|
||||||
|
@ -82,7 +79,9 @@ import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager';
|
||||||
import { objectHasDiff } from "../../utils/objects";
|
import { objectHasDiff } from "../../utils/objects";
|
||||||
import SpaceRoomView from "./SpaceRoomView";
|
import SpaceRoomView from "./SpaceRoomView";
|
||||||
import { IOpts } from "../../createRoom";
|
import { IOpts } from "../../createRoom";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import { omit } from 'lodash';
|
||||||
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function(msg: string) {};
|
let debuglog = function(msg: string) {};
|
||||||
|
@ -136,7 +135,6 @@ export interface IState {
|
||||||
// Whether to highlight the event scrolled to
|
// Whether to highlight the event scrolled to
|
||||||
isInitialEventHighlighted?: boolean;
|
isInitialEventHighlighted?: boolean;
|
||||||
replyToEvent?: MatrixEvent;
|
replyToEvent?: MatrixEvent;
|
||||||
forwardingEvent?: MatrixEvent;
|
|
||||||
numUnreadMessages: number;
|
numUnreadMessages: number;
|
||||||
draggingFile: boolean;
|
draggingFile: boolean;
|
||||||
searching: boolean;
|
searching: boolean;
|
||||||
|
@ -155,8 +153,6 @@ export interface IState {
|
||||||
canPeek: boolean;
|
canPeek: boolean;
|
||||||
showApps: boolean;
|
showApps: boolean;
|
||||||
isPeeking: boolean;
|
isPeeking: boolean;
|
||||||
showingPinned: boolean;
|
|
||||||
showReadReceipts: boolean;
|
|
||||||
showRightPanel: boolean;
|
showRightPanel: boolean;
|
||||||
// error object, as from the matrix client/server API
|
// error object, as from the matrix client/server API
|
||||||
// If we failed to load information about the room,
|
// If we failed to load information about the room,
|
||||||
|
@ -175,6 +171,7 @@ export interface IState {
|
||||||
statusBarVisible: boolean;
|
statusBarVisible: boolean;
|
||||||
// We load this later by asking the js-sdk to suggest a version for us.
|
// We load this later by asking the js-sdk to suggest a version for us.
|
||||||
// This object is the result of Room#getRecommendedVersion()
|
// This object is the result of Room#getRecommendedVersion()
|
||||||
|
|
||||||
upgradeRecommendation?: {
|
upgradeRecommendation?: {
|
||||||
version: string;
|
version: string;
|
||||||
needsUpgrade: boolean;
|
needsUpgrade: boolean;
|
||||||
|
@ -183,6 +180,12 @@ export interface IState {
|
||||||
canReact: boolean;
|
canReact: boolean;
|
||||||
canReply: boolean;
|
canReply: boolean;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
|
lowBandwidth: boolean;
|
||||||
|
showReadReceipts: boolean;
|
||||||
|
showRedactions: boolean;
|
||||||
|
showJoinLeaves: boolean;
|
||||||
|
showAvatarChanges: boolean;
|
||||||
|
showDisplaynameChanges: boolean;
|
||||||
matrixClientIsReady: boolean;
|
matrixClientIsReady: boolean;
|
||||||
showUrlPreview?: boolean;
|
showUrlPreview?: boolean;
|
||||||
e2eStatus?: E2EStatus;
|
e2eStatus?: E2EStatus;
|
||||||
|
@ -200,8 +203,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
private readonly dispatcherRef: string;
|
private readonly dispatcherRef: string;
|
||||||
private readonly roomStoreToken: EventSubscription;
|
private readonly roomStoreToken: EventSubscription;
|
||||||
private readonly rightPanelStoreToken: EventSubscription;
|
private readonly rightPanelStoreToken: EventSubscription;
|
||||||
private readonly showReadReceiptsWatchRef: string;
|
private settingWatchers: string[];
|
||||||
private readonly layoutWatcherRef: string;
|
|
||||||
|
|
||||||
private unmounted = false;
|
private unmounted = false;
|
||||||
private permalinkCreators: Record<string, RoomPermalinkCreator> = {};
|
private permalinkCreators: Record<string, RoomPermalinkCreator> = {};
|
||||||
|
@ -232,8 +234,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
canPeek: false,
|
canPeek: false,
|
||||||
showApps: false,
|
showApps: false,
|
||||||
isPeeking: false,
|
isPeeking: false,
|
||||||
showingPinned: false,
|
|
||||||
showReadReceipts: true,
|
|
||||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||||
joining: false,
|
joining: false,
|
||||||
atEndOfLiveTimeline: true,
|
atEndOfLiveTimeline: true,
|
||||||
|
@ -243,6 +243,12 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
canReact: false,
|
canReact: false,
|
||||||
canReply: false,
|
canReply: false,
|
||||||
layout: SettingsStore.getValue("layout"),
|
layout: SettingsStore.getValue("layout"),
|
||||||
|
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
||||||
|
showReadReceipts: true,
|
||||||
|
showRedactions: true,
|
||||||
|
showJoinLeaves: true,
|
||||||
|
showAvatarChanges: true,
|
||||||
|
showDisplaynameChanges: true,
|
||||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||||
dragCounter: 0,
|
dragCounter: 0,
|
||||||
};
|
};
|
||||||
|
@ -269,9 +275,14 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||||
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||||
|
|
||||||
this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
|
this.settingWatchers = [
|
||||||
this.onReadReceiptsChange);
|
SettingsStore.watchSetting("layout", null, () =>
|
||||||
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange);
|
this.setState({ layout: SettingsStore.getValue("layout") }),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("lowBandwidth", null, () =>
|
||||||
|
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
|
||||||
|
),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWidgetStoreUpdate = () => {
|
private onWidgetStoreUpdate = () => {
|
||||||
|
@ -324,14 +335,45 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
initialEventId: RoomViewStore.getInitialEventId(),
|
initialEventId: RoomViewStore.getInitialEventId(),
|
||||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||||
replyToEvent: RoomViewStore.getQuotingEvent(),
|
replyToEvent: RoomViewStore.getQuotingEvent(),
|
||||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
|
||||||
// we should only peek once we have a ready client
|
// we should only peek once we have a ready client
|
||||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
|
||||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||||
|
showRedactions: SettingsStore.getValue("showRedactions", roomId),
|
||||||
|
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
|
||||||
|
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
|
||||||
|
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
|
||||||
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add watchers for each of the settings we just looked up
|
||||||
|
this.settingWatchers = this.settingWatchers.concat([
|
||||||
|
SettingsStore.watchSetting("showReadReceipts", null, () =>
|
||||||
|
this.setState({
|
||||||
|
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("showRedactions", null, () =>
|
||||||
|
this.setState({
|
||||||
|
showRedactions: SettingsStore.getValue("showRedactions", roomId),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("showJoinLeaves", null, () =>
|
||||||
|
this.setState({
|
||||||
|
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("showAvatarChanges", null, () =>
|
||||||
|
this.setState({
|
||||||
|
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("showDisplaynameChanges", null, () =>
|
||||||
|
this.setState({
|
||||||
|
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
|
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
|
||||||
// Stop peeking because we have joined this room now
|
// Stop peeking because we have joined this room now
|
||||||
this.context.stopPeeking();
|
this.context.stopPeeking();
|
||||||
|
@ -528,7 +570,20 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
return (objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState));
|
const hasPropsDiff = objectHasDiff(this.props, nextProps);
|
||||||
|
|
||||||
|
// React only shallow comparison and we only want to trigger
|
||||||
|
// a component re-render if a room requires an upgrade
|
||||||
|
const newUpgradeRecommendation = nextState.upgradeRecommendation || {}
|
||||||
|
|
||||||
|
const state = omit(this.state, ['upgradeRecommendation']);
|
||||||
|
const newState = omit(nextState, ['upgradeRecommendation'])
|
||||||
|
|
||||||
|
const hasStateDiff =
|
||||||
|
objectHasDiff(state, newState) ||
|
||||||
|
(newUpgradeRecommendation.needsUpgrade === true)
|
||||||
|
|
||||||
|
return hasPropsDiff || hasStateDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
|
@ -627,10 +682,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.showReadReceiptsWatchRef) {
|
|
||||||
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
// cancel any pending calls to the rate_limited_funcs
|
// cancel any pending calls to the rate_limited_funcs
|
||||||
this.updateRoomMembers.cancelPendingCall();
|
this.updateRoomMembers.cancelPendingCall();
|
||||||
|
|
||||||
|
@ -638,7 +689,20 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
// console.log("Tinter.tint from RoomView.unmount");
|
// console.log("Tinter.tint from RoomView.unmount");
|
||||||
// Tinter.tint(); // reset colourscheme
|
// Tinter.tint(); // reset colourscheme
|
||||||
|
|
||||||
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
for (const watcher of this.settingWatchers) {
|
||||||
|
SettingsStore.unwatchSetting(watcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onUserScroll = () => {
|
||||||
|
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: this.state.room.roomId,
|
||||||
|
event_id: this.state.initialEventId,
|
||||||
|
highlighted: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onLayoutChange = () => {
|
private onLayoutChange = () => {
|
||||||
|
@ -797,7 +861,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
// update unread count when scrolled up
|
// update unread count when scrolled up
|
||||||
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
||||||
// no change
|
// no change
|
||||||
} else if (!shouldHideEvent(ev)) {
|
} else if (!shouldHideEvent(ev, this.state)) {
|
||||||
this.setState((state, props) => {
|
this.setState((state, props) => {
|
||||||
return {numUnreadMessages: state.numUnreadMessages + 1};
|
return {numUnreadMessages: state.numUnreadMessages + 1};
|
||||||
});
|
});
|
||||||
|
@ -1114,7 +1178,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
const signUrl = this.props.threepidInvite?.signUrl;
|
const signUrl = this.props.threepidInvite?.signUrl;
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'join_room',
|
action: Action.JoinRoom,
|
||||||
|
roomId: this.getRoomId(),
|
||||||
opts: { inviteSignUrl: signUrl },
|
opts: { inviteSignUrl: signUrl },
|
||||||
_type: "unknown", // TODO: instrumentation
|
_type: "unknown", // TODO: instrumentation
|
||||||
});
|
});
|
||||||
|
@ -1375,13 +1440,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onPinnedClick = () => {
|
|
||||||
const nowShowingPinned = !this.state.showingPinned;
|
|
||||||
const roomId = this.state.room.roomId;
|
|
||||||
this.setState({showingPinned: nowShowingPinned, searching: false});
|
|
||||||
SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onCallPlaced = (type: PlaceCallType) => {
|
private onCallPlaced = (type: PlaceCallType) => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
|
@ -1394,18 +1452,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
dis.dispatch({ action: "open_room_settings" });
|
dis.dispatch({ action: "open_room_settings" });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCancelClick = () => {
|
|
||||||
console.log("updateTint from onCancelClick");
|
|
||||||
this.updateTint();
|
|
||||||
if (this.state.forwardingEvent) {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'forward_event',
|
|
||||||
event: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
dis.fire(Action.FocusComposer);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onAppsClick = () => {
|
private onAppsClick = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "appsDrawer",
|
action: "appsDrawer",
|
||||||
|
@ -1498,7 +1544,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
private onSearchClick = () => {
|
private onSearchClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
searching: !this.state.searching,
|
searching: !this.state.searching,
|
||||||
showingPinned: false,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1511,8 +1556,19 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// jump down to the bottom of this room, where new events are arriving
|
// jump down to the bottom of this room, where new events are arriving
|
||||||
private jumpToLiveTimeline = () => {
|
private jumpToLiveTimeline = () => {
|
||||||
this.messagePanel.jumpToLiveTimeline();
|
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||||
dis.fire(Action.FocusComposer);
|
// If we were viewing a highlighted event, firing view_room without
|
||||||
|
// an event will take care of both clearing the URL fragment and
|
||||||
|
// jumping to the bottom
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: this.state.room.roomId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Otherwise we have to jump manually
|
||||||
|
this.messagePanel.jumpToLiveTimeline();
|
||||||
|
dis.fire(Action.FocusComposer);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// jump up to wherever our read marker is
|
// jump up to wherever our read marker is
|
||||||
|
@ -1585,7 +1641,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
// a maxHeight on the underlying remote video tag.
|
// a maxHeight on the underlying remote video tag.
|
||||||
|
|
||||||
// header + footer + status + give us at least 120px of scrollback at all times.
|
// header + footer + status + give us at least 120px of scrollback at all times.
|
||||||
let auxPanelMaxHeight = window.innerHeight -
|
let auxPanelMaxHeight = UIStore.instance.windowHeight -
|
||||||
(54 + // height of RoomHeader
|
(54 + // height of RoomHeader
|
||||||
36 + // height of the status area
|
36 + // height of the status area
|
||||||
51 + // minimum height of the message compmoser
|
51 + // minimum height of the message compmoser
|
||||||
|
@ -1598,33 +1654,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.setState({auxPanelMaxHeight: auxPanelMaxHeight});
|
this.setState({auxPanelMaxHeight: auxPanelMaxHeight});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onFullscreenClick = () => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'video_fullscreen',
|
|
||||||
fullscreen: true,
|
|
||||||
}, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onMuteAudioClick = () => {
|
|
||||||
const call = this.getCallForRoom();
|
|
||||||
if (!call) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newState = !call.isMicrophoneMuted();
|
|
||||||
call.setMicrophoneMuted(newState);
|
|
||||||
this.forceUpdate(); // TODO: just update the voip buttons
|
|
||||||
};
|
|
||||||
|
|
||||||
private onMuteVideoClick = () => {
|
|
||||||
const call = this.getCallForRoom();
|
|
||||||
if (!call) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newState = !call.isLocalVideoMuted();
|
|
||||||
call.setLocalVideoMuted(newState);
|
|
||||||
this.forceUpdate(); // TODO: just update the voip buttons
|
|
||||||
};
|
|
||||||
|
|
||||||
private onStatusBarVisible = () => {
|
private onStatusBarVisible = () => {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -1856,11 +1885,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let aux = null;
|
let aux = null;
|
||||||
let previewBar;
|
let previewBar;
|
||||||
let hideCancel = false;
|
if (this.state.searching) {
|
||||||
if (this.state.forwardingEvent) {
|
|
||||||
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
|
|
||||||
} else if (this.state.searching) {
|
|
||||||
hideCancel = true; // has own cancel
|
|
||||||
aux = <SearchBar
|
aux = <SearchBar
|
||||||
searchInProgress={this.state.searchInProgress}
|
searchInProgress={this.state.searchInProgress}
|
||||||
onCancelClick={this.onCancelSearchClick}
|
onCancelClick={this.onCancelSearchClick}
|
||||||
|
@ -1869,10 +1894,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
/>;
|
/>;
|
||||||
} else if (showRoomUpgradeBar) {
|
} else if (showRoomUpgradeBar) {
|
||||||
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
||||||
hideCancel = true;
|
|
||||||
} else if (this.state.showingPinned) {
|
|
||||||
hideCancel = true; // has own cancel
|
|
||||||
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
|
|
||||||
} else if (myMembership !== "join") {
|
} else if (myMembership !== "join") {
|
||||||
// We do have a room object for this room, but we're not currently in it.
|
// We do have a room object for this room, but we're not currently in it.
|
||||||
// We may have a 3rd party invite to it.
|
// We may have a 3rd party invite to it.
|
||||||
|
@ -1881,7 +1902,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
inviterName = this.props.oobData.inviterName;
|
inviterName = this.props.oobData.inviterName;
|
||||||
}
|
}
|
||||||
const invitedEmail = this.props.threepidInvite?.toEmail;
|
const invitedEmail = this.props.threepidInvite?.toEmail;
|
||||||
hideCancel = true;
|
|
||||||
previewBar = (
|
previewBar = (
|
||||||
<RoomPreviewBar
|
<RoomPreviewBar
|
||||||
onJoinClick={this.onJoinButtonClicked}
|
onJoinClick={this.onJoinButtonClicked}
|
||||||
|
@ -1999,11 +2019,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
hideMessagePanel = true;
|
hideMessagePanel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldHighlight = this.state.isInitialEventHighlighted;
|
|
||||||
let highlightedEventId = null;
|
let highlightedEventId = null;
|
||||||
if (this.state.forwardingEvent) {
|
if (this.state.isInitialEventHighlighted) {
|
||||||
highlightedEventId = this.state.forwardingEvent.getId();
|
|
||||||
} else if (shouldHighlight) {
|
|
||||||
highlightedEventId = this.state.initialEventId;
|
highlightedEventId = this.state.initialEventId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2028,6 +2045,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
eventId={this.state.initialEventId}
|
eventId={this.state.initialEventId}
|
||||||
eventPixelOffset={this.state.initialEventPixelOffset}
|
eventPixelOffset={this.state.initialEventPixelOffset}
|
||||||
onScroll={this.onMessageListScroll}
|
onScroll={this.onMessageListScroll}
|
||||||
|
onUserScroll={this.onUserScroll}
|
||||||
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
||||||
showUrlPreview = {this.state.showUrlPreview}
|
showUrlPreview = {this.state.showUrlPreview}
|
||||||
className={messagePanelClassNames}
|
className={messagePanelClassNames}
|
||||||
|
@ -2054,6 +2072,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
|
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
|
||||||
numUnreadMessages={this.state.numUnreadMessages}
|
numUnreadMessages={this.state.numUnreadMessages}
|
||||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||||
|
roomId={this.state.roomId}
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2090,8 +2109,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
inRoom={myMembership === 'join'}
|
inRoom={myMembership === 'join'}
|
||||||
onSearchClick={this.onSearchClick}
|
onSearchClick={this.onSearchClick}
|
||||||
onSettingsClick={this.onSettingsClick}
|
onSettingsClick={this.onSettingsClick}
|
||||||
onPinnedClick={this.onPinnedClick}
|
|
||||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
|
||||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||||
e2eStatus={this.state.e2eStatus}
|
e2eStatus={this.state.e2eStatus}
|
||||||
|
|
|
@ -133,6 +133,10 @@ export default class ScrollPanel extends React.Component {
|
||||||
*/
|
*/
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
|
||||||
|
/* onUserScroll: callback which is called when the user interacts with the room timeline
|
||||||
|
*/
|
||||||
|
onUserScroll: PropTypes.func,
|
||||||
|
|
||||||
/* className: classnames to add to the top-level div
|
/* className: classnames to add to the top-level div
|
||||||
*/
|
*/
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
@ -535,21 +539,29 @@ export default class ScrollPanel extends React.Component {
|
||||||
* @param {object} ev the keyboard event
|
* @param {object} ev the keyboard event
|
||||||
*/
|
*/
|
||||||
handleScrollKey = ev => {
|
handleScrollKey = ev => {
|
||||||
|
let isScrolling = false;
|
||||||
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
||||||
switch (roomAction) {
|
switch (roomAction) {
|
||||||
case RoomAction.ScrollUp:
|
case RoomAction.ScrollUp:
|
||||||
this.scrollRelative(-1);
|
this.scrollRelative(-1);
|
||||||
|
isScrolling = true;
|
||||||
break;
|
break;
|
||||||
case RoomAction.RoomScrollDown:
|
case RoomAction.RoomScrollDown:
|
||||||
this.scrollRelative(1);
|
this.scrollRelative(1);
|
||||||
|
isScrolling = true;
|
||||||
break;
|
break;
|
||||||
case RoomAction.JumpToFirstMessage:
|
case RoomAction.JumpToFirstMessage:
|
||||||
this.scrollToTop();
|
this.scrollToTop();
|
||||||
|
isScrolling = true;
|
||||||
break;
|
break;
|
||||||
case RoomAction.JumpToLatestMessage:
|
case RoomAction.JumpToLatestMessage:
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
|
isScrolling = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (isScrolling && this.props.onUserScroll) {
|
||||||
|
this.props.onUserScroll(ev);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Scroll the panel to bring the DOM node with the scroll token
|
/* Scroll the panel to bring the DOM node with the scroll token
|
||||||
|
@ -888,9 +900,8 @@ export default class ScrollPanel extends React.Component {
|
||||||
<AutoHideScrollbar
|
<AutoHideScrollbar
|
||||||
wrappedRef={this._collectScroll}
|
wrappedRef={this._collectScroll}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
className={`mx_ScrollPanel ${this.props.className}`}
|
onWheel={this.props.onUserScroll}
|
||||||
style={this.props.style}
|
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
||||||
>
|
|
||||||
{ this.props.fixedChildren }
|
{ this.props.fixedChildren }
|
||||||
<div className="mx_RoomView_messageListWrapper">
|
<div className="mx_RoomView_messageListWrapper">
|
||||||
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
||||||
|
|
|
@ -76,7 +76,7 @@ export interface ISpaceSummaryEvent {
|
||||||
order?: string;
|
order?: string;
|
||||||
suggested?: boolean;
|
suggested?: boolean;
|
||||||
auto_join?: boolean;
|
auto_join?: boolean;
|
||||||
via?: string;
|
via?: string[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
@ -101,15 +101,13 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
numChildRooms,
|
numChildRooms,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const name = room.name || room.canonical_alias || room.aliases?.[0]
|
const cli = MatrixClientPeg.get();
|
||||||
|
const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null;
|
||||||
|
const name = joinedRoom?.name || room.name || room.canonical_alias || room.aliases?.[0]
|
||||||
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
||||||
|
|
||||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
const cliRoom = cli.getRoom(room.room_id);
|
|
||||||
const myMembership = cliRoom?.getMyMembership();
|
|
||||||
|
|
||||||
const onPreviewClick = (ev: ButtonEvent) => {
|
const onPreviewClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
@ -122,7 +120,7 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
let button;
|
let button;
|
||||||
if (myMembership === "join") {
|
if (joinedRoom) {
|
||||||
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
|
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
|
||||||
{ _t("View") }
|
{ _t("View") }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
@ -146,17 +144,27 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let url: string;
|
let avatar;
|
||||||
if (room.avatar_url) {
|
if (joinedRoom) {
|
||||||
url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20);
|
avatar = <RoomAvatar room={joinedRoom} width={20} height={20} />;
|
||||||
|
} else {
|
||||||
|
avatar = <BaseAvatar
|
||||||
|
name={name}
|
||||||
|
idName={room.room_id}
|
||||||
|
url={room.avatar_url ? mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20) : null}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let description = _t("%(count)s members", { count: room.num_joined_members });
|
let description = _t("%(count)s members", { count: room.num_joined_members });
|
||||||
if (numChildRooms) {
|
if (numChildRooms !== undefined) {
|
||||||
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
|
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
|
||||||
}
|
}
|
||||||
if (room.topic) {
|
|
||||||
description += " · " + room.topic;
|
const topic = joinedRoom?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || room.topic;
|
||||||
|
if (topic) {
|
||||||
|
description += " · " + topic;
|
||||||
}
|
}
|
||||||
|
|
||||||
let suggestedSection;
|
let suggestedSection;
|
||||||
|
@ -167,7 +175,7 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = <React.Fragment>
|
const content = <React.Fragment>
|
||||||
<BaseAvatar name={name} idName={room.room_id} url={url} width={20} height={20} />
|
{ avatar }
|
||||||
<div className="mx_SpaceRoomDirectory_roomTile_name">
|
<div className="mx_SpaceRoomDirectory_roomTile_name">
|
||||||
{ name }
|
{ name }
|
||||||
{ suggestedSection }
|
{ suggestedSection }
|
||||||
|
@ -311,7 +319,7 @@ export const HierarchyLevel = ({
|
||||||
key={roomId}
|
key={roomId}
|
||||||
room={rooms.get(roomId)}
|
room={rooms.get(roomId)}
|
||||||
numChildRooms={Array.from(relations.get(roomId)?.values() || [])
|
numChildRooms={Array.from(relations.get(roomId)?.values() || [])
|
||||||
.filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length}
|
.filter(ev => rooms.has(ev.state_key) && !rooms.get(ev.state_key).room_type).length}
|
||||||
suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
|
suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
|
||||||
selected={selectedMap?.get(spaceId)?.has(roomId)}
|
selected={selectedMap?.get(spaceId)?.has(roomId)}
|
||||||
onViewRoomClick={(autoJoin) => {
|
onViewRoomClick={(autoJoin) => {
|
||||||
|
@ -356,9 +364,9 @@ export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: a
|
||||||
parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev);
|
parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev);
|
||||||
childParentRelations.getOrCreate(ev.state_key, new Set()).add(ev.room_id);
|
childParentRelations.getOrCreate(ev.state_key, new Set()).add(ev.room_id);
|
||||||
}
|
}
|
||||||
if (Array.isArray(ev.content["via"])) {
|
if (Array.isArray(ev.content.via)) {
|
||||||
const set = viaMap.getOrCreate(ev.state_key, new Set());
|
const set = viaMap.getOrCreate(ev.state_key, new Set());
|
||||||
ev.content["via"].forEach(via => set.add(via));
|
ev.content.via.forEach(via => set.add(via));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -429,7 +437,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
if (roomsMap) {
|
if (roomsMap) {
|
||||||
const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length;
|
const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
|
||||||
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
|
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
|
||||||
|
|
||||||
let countsStr;
|
let countsStr;
|
||||||
|
@ -471,8 +479,12 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
try {
|
try {
|
||||||
for (const [parentId, childId] of selectedRelations) {
|
for (const [parentId, childId] of selectedRelations) {
|
||||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
||||||
parentChildMap.get(parentId).get(childId).content = {};
|
parentChildMap.get(parentId).delete(childId);
|
||||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
if (parentChildMap.get(parentId).size > 0) {
|
||||||
|
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||||
|
} else {
|
||||||
|
parentChildMap.delete(parentId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(_t("Failed to remove some rooms. Try again later"));
|
setError(_t("Failed to remove some rooms. Try again later"));
|
||||||
|
@ -508,6 +520,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
setError("Failed to update some suggestions. Try again later");
|
setError("Failed to update some suggestions. Try again later");
|
||||||
}
|
}
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
|
setSelected(new Map());
|
||||||
}}
|
}}
|
||||||
kind="primary_outline"
|
kind="primary_outline"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -28,7 +28,7 @@ import RoomTopic from "../views/elements/RoomTopic";
|
||||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||||
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
|
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
|
||||||
import {useRoomMembers} from "../../hooks/useRoomMembers";
|
import {useRoomMembers} from "../../hooks/useRoomMembers";
|
||||||
import createRoom, {IOpts, Preset} from "../../createRoom";
|
import createRoom, {IOpts} from "../../createRoom";
|
||||||
import Field from "../views/elements/Field";
|
import Field from "../views/elements/Field";
|
||||||
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
||||||
import withValidation from "../views/elements/Validation";
|
import withValidation from "../views/elements/Validation";
|
||||||
|
@ -65,6 +65,7 @@ import dis from "../../dispatcher/dispatcher";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
|
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -417,9 +418,13 @@ const SpaceLanding = ({ space }) => {
|
||||||
{ inviteButton }
|
{ inviteButton }
|
||||||
{ settingsButton }
|
{ settingsButton }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_SpaceRoomView_landing_topic">
|
<RoomTopic room={space}>
|
||||||
<RoomTopic room={space} />
|
{(topic, ref) => (
|
||||||
</div>
|
<div className="mx_SpaceRoomView_landing_topic" ref={ref}>
|
||||||
|
{ topic }
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</RoomTopic>
|
||||||
<SpaceFeedbackPrompt />
|
<SpaceFeedbackPrompt />
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
@ -437,7 +442,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const numFields = 3;
|
const numFields = 3;
|
||||||
const placeholders = [_t("General"), _t("Random"), _t("Support")];
|
const placeholders = [_t("General"), _t("Random"), _t("Support")];
|
||||||
// TODO vary default prefills for "Just Me" spaces
|
|
||||||
const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
|
const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
|
||||||
const fields = new Array(numFields).fill(0).map((_, i) => {
|
const fields = new Array(numFields).fill(0).map((_, i) => {
|
||||||
const name = "roomName" + i;
|
const name = "roomName" + i;
|
||||||
|
@ -584,6 +588,10 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
|
||||||
<h3>{ _t("Me and my teammates") }</h3>
|
<h3>{ _t("Me and my teammates") }</h3>
|
||||||
<div>{ _t("A private space for you and your teammates") }</div>
|
<div>{ _t("A private space for you and your teammates") }</div>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
<div className="mx_SpaceRoomView_betaWarning">
|
||||||
|
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
|
||||||
|
<p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p>
|
||||||
|
</div>
|
||||||
<SpaceFeedbackPrompt />
|
<SpaceFeedbackPrompt />
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
@ -806,7 +814,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
||||||
let suggestedRooms = SpaceStore.instance.suggestedRooms;
|
let suggestedRooms = SpaceStore.instance.suggestedRooms;
|
||||||
if (SpaceStore.instance.activeSpace !== this.props.space) {
|
if (SpaceStore.instance.activeSpace !== this.props.space) {
|
||||||
// the space store has the suggested rooms loaded for a different space, fetch the right ones
|
// the space store has the suggested rooms loaded for a different space, fetch the right ones
|
||||||
suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1)).rooms;
|
suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (suggestedRooms.length) {
|
if (suggestedRooms.length) {
|
||||||
|
@ -814,9 +822,11 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
||||||
defaultDispatcher.dispatch({
|
defaultDispatcher.dispatch({
|
||||||
action: "view_room",
|
action: "view_room",
|
||||||
room_id: room.room_id,
|
room_id: room.room_id,
|
||||||
|
room_alias: room.canonical_alias || room.aliases?.[0],
|
||||||
|
via_servers: room.viaServers,
|
||||||
oobData: {
|
oobData: {
|
||||||
avatarUrl: room.avatar_url,
|
avatarUrl: room.avatar_url,
|
||||||
name: room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"),
|
name: room.name || room.canonical_alias || room.aliases?.[0] || _t("Empty room"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline";
|
||||||
import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
|
import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||||
|
import RoomContext from "../../contexts/RoomContext";
|
||||||
import UserActivity from "../../UserActivity";
|
import UserActivity from "../../UserActivity";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
|
@ -36,7 +37,6 @@ import shouldHideEvent from '../../shouldHideEvent';
|
||||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||||
import {UIFeature} from "../../settings/UIFeature";
|
import {UIFeature} from "../../settings/UIFeature";
|
||||||
import {objectHasDiff} from "../../utils/objects";
|
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
import { arrayFastClone } from "../../utils/arrays";
|
import { arrayFastClone } from "../../utils/arrays";
|
||||||
|
|
||||||
|
@ -94,6 +94,9 @@ class TimelinePanel extends React.Component {
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
|
||||||
|
// callback which is called when the user interacts with the room timeline
|
||||||
|
onUserScroll: PropTypes.func,
|
||||||
|
|
||||||
// callback which is called when the read-up-to mark is updated.
|
// callback which is called when the read-up-to mark is updated.
|
||||||
onReadMarkerUpdated: PropTypes.func,
|
onReadMarkerUpdated: PropTypes.func,
|
||||||
|
|
||||||
|
@ -118,8 +121,13 @@ class TimelinePanel extends React.Component {
|
||||||
|
|
||||||
// which layout to use
|
// which layout to use
|
||||||
layout: LayoutPropType,
|
layout: LayoutPropType,
|
||||||
|
|
||||||
|
// whether to always show timestamps for an event
|
||||||
|
alwaysShowTimestamps: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static contextType = RoomContext;
|
||||||
|
|
||||||
// a map from room id to read marker event timestamp
|
// a map from room id to read marker event timestamp
|
||||||
static roomReadMarkerTsMap = {};
|
static roomReadMarkerTsMap = {};
|
||||||
|
|
||||||
|
@ -258,37 +266,15 @@ class TimelinePanel extends React.Component {
|
||||||
console.warn("Replacing timelineSet on a TimelinePanel - confusion may ensue");
|
console.warn("Replacing timelineSet on a TimelinePanel - confusion may ensue");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newProps.eventId != this.props.eventId) {
|
const differentEventId = newProps.eventId != this.props.eventId;
|
||||||
|
const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId;
|
||||||
|
if (differentEventId || differentHighlightedEventId) {
|
||||||
console.log("TimelinePanel switching to eventId " + newProps.eventId +
|
console.log("TimelinePanel switching to eventId " + newProps.eventId +
|
||||||
" (was " + this.props.eventId + ")");
|
" (was " + this.props.eventId + ")");
|
||||||
return this._initTimeline(newProps);
|
return this._initTimeline(newProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
if (objectHasDiff(this.props, nextProps)) {
|
|
||||||
if (DEBUG) {
|
|
||||||
console.group("Timeline.shouldComponentUpdate: props change");
|
|
||||||
console.log("props before:", this.props);
|
|
||||||
console.log("props after:", nextProps);
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (objectHasDiff(this.state, nextState)) {
|
|
||||||
if (DEBUG) {
|
|
||||||
console.group("Timeline.shouldComponentUpdate: state change");
|
|
||||||
console.log("state before:", this.state);
|
|
||||||
console.log("state after:", nextState);
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
// set a boolean to say we've been unmounted, which any pending
|
// set a boolean to say we've been unmounted, which any pending
|
||||||
// promises can use to throw away their results.
|
// promises can use to throw away their results.
|
||||||
|
@ -1149,9 +1135,8 @@ class TimelinePanel extends React.Component {
|
||||||
arrayFastClone(events)
|
arrayFastClone(events)
|
||||||
.reverse()
|
.reverse()
|
||||||
.forEach(event => {
|
.forEach(event => {
|
||||||
if (event.shouldAttemptDecryption()) {
|
const client = MatrixClientPeg.get();
|
||||||
event.attemptDecryption(MatrixClientPeg.get()._crypto);
|
client.decryptEventIfNeeded(event);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstVisibleEventIndex = this._checkForPreJoinUISI(events);
|
const firstVisibleEventIndex = this._checkForPreJoinUISI(events);
|
||||||
|
@ -1306,7 +1291,7 @@ class TimelinePanel extends React.Component {
|
||||||
|
|
||||||
const shouldIgnore = !!ev.status || // local echo
|
const shouldIgnore = !!ev.status || // local echo
|
||||||
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
|
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
|
||||||
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev);
|
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context);
|
||||||
|
|
||||||
if (isWithoutTile || !node) {
|
if (isWithoutTile || !node) {
|
||||||
// don't start counting if the event should be ignored,
|
// don't start counting if the event should be ignored,
|
||||||
|
@ -1457,10 +1442,11 @@ class TimelinePanel extends React.Component {
|
||||||
ourUserId={MatrixClientPeg.get().credentials.userId}
|
ourUserId={MatrixClientPeg.get().credentials.userId}
|
||||||
stickyBottom={stickyBottom}
|
stickyBottom={stickyBottom}
|
||||||
onScroll={this.onMessageListScroll}
|
onScroll={this.onMessageListScroll}
|
||||||
|
onUserScroll={this.props.onUserScroll}
|
||||||
onFillRequest={this.onMessageListFillRequest}
|
onFillRequest={this.onMessageListFillRequest}
|
||||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||||
isTwelveHour={this.state.isTwelveHour}
|
isTwelveHour={this.state.isTwelveHour}
|
||||||
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
|
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
|
||||||
className={this.props.className}
|
className={this.props.className}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
|
|
@ -55,6 +55,7 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||||
const totalCount = this.state.toasts.length;
|
const totalCount = this.state.toasts.length;
|
||||||
const isStacked = totalCount > 1;
|
const isStacked = totalCount > 1;
|
||||||
let toast;
|
let toast;
|
||||||
|
let containerClasses;
|
||||||
if (totalCount !== 0) {
|
if (totalCount !== 0) {
|
||||||
const topToast = this.state.toasts[0];
|
const topToast = this.state.toasts[0];
|
||||||
const {title, icon, key, component, className, props} = topToast;
|
const {title, icon, key, component, className, props} = topToast;
|
||||||
|
@ -79,16 +80,17 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
||||||
</div>);
|
</div>);
|
||||||
|
|
||||||
|
containerClasses = classNames("mx_ToastContainer", {
|
||||||
|
"mx_ToastContainer_stacked": isStacked,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
return toast
|
||||||
const containerClasses = classNames("mx_ToastContainer", {
|
? (
|
||||||
"mx_ToastContainer_stacked": isStacked,
|
<div className={containerClasses} role="alert">
|
||||||
});
|
{toast}
|
||||||
|
</div>
|
||||||
return (
|
)
|
||||||
<div className={containerClasses} role="alert">
|
: null;
|
||||||
{toast}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,8 @@ import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
|
||||||
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
||||||
import RoomName from "../views/elements/RoomName";
|
import RoomName from "../views/elements/RoomName";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
|
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||||
|
import TooltipButton from "../views/elements/TooltipButton";
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
}
|
}
|
||||||
|
@ -68,6 +69,7 @@ interface IState {
|
||||||
contextMenuPosition: PartialDOMRect;
|
contextMenuPosition: PartialDOMRect;
|
||||||
isDarkTheme: boolean;
|
isDarkTheme: boolean;
|
||||||
selectedSpace?: Room;
|
selectedSpace?: Room;
|
||||||
|
pendingRoomJoin: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.UserMenu")
|
@replaceableComponent("structures.UserMenu")
|
||||||
|
@ -84,6 +86,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
this.state = {
|
this.state = {
|
||||||
contextMenuPosition: null,
|
contextMenuPosition: null,
|
||||||
isDarkTheme: this.isUserOnDarkTheme(),
|
isDarkTheme: this.isUserOnDarkTheme(),
|
||||||
|
pendingRoomJoin: new Set<string>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||||
|
@ -103,6 +106,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
|
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
|
||||||
this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
|
this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
|
||||||
|
MatrixClientPeg.get().on("Room", this.onRoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -114,6 +118,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
if (SettingsStore.getValue("feature_spaces")) {
|
if (SettingsStore.getValue("feature_spaces")) {
|
||||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
||||||
}
|
}
|
||||||
|
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRoom = (room: Room): void => {
|
||||||
|
this.removePendingJoinRoom(room.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onTagStoreUpdate = () => {
|
private onTagStoreUpdate = () => {
|
||||||
|
@ -147,15 +156,39 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onAction = (ev: ActionPayload) => {
|
private onAction = (ev: ActionPayload) => {
|
||||||
if (ev.action !== Action.ToggleUserMenu) return; // not interested
|
switch (ev.action) {
|
||||||
|
case Action.ToggleUserMenu:
|
||||||
if (this.state.contextMenuPosition) {
|
if (this.state.contextMenuPosition) {
|
||||||
this.setState({contextMenuPosition: null});
|
this.setState({contextMenuPosition: null});
|
||||||
} else {
|
} else {
|
||||||
if (this.buttonRef.current) this.buttonRef.current.click();
|
if (this.buttonRef.current) this.buttonRef.current.click();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Action.JoinRoom:
|
||||||
|
this.addPendingJoinRoom(ev.roomId);
|
||||||
|
break;
|
||||||
|
case Action.JoinRoomReady:
|
||||||
|
case Action.JoinRoomError:
|
||||||
|
this.removePendingJoinRoom(ev.roomId);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private addPendingJoinRoom(roomId: string): void {
|
||||||
|
this.setState({
|
||||||
|
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin)
|
||||||
|
.add(roomId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private removePendingJoinRoom(roomId: string): void {
|
||||||
|
if (this.state.pendingRoomJoin.delete(roomId)) {
|
||||||
|
this.setState({
|
||||||
|
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
@ -333,9 +366,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
const mxDomain = MatrixClientPeg.get().getDomain();
|
const mxDomain = MatrixClientPeg.get().getDomain();
|
||||||
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
|
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
|
||||||
if (!hostSignupConfig.domains || validDomains.length > 0) {
|
if (!hostSignupConfig.domains || validDomains.length > 0) {
|
||||||
topSection = <div onClick={this.onCloseMenu}>
|
topSection = <HostSignupAction onClick={this.onCloseMenu} />;
|
||||||
<HostSignupAction />
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -617,6 +648,14 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
{name}
|
{name}
|
||||||
|
{this.state.pendingRoomJoin.size > 0 && (
|
||||||
|
<InlineSpinner>
|
||||||
|
<TooltipButton helpText={_t(
|
||||||
|
"Currently joining %(count)s rooms",
|
||||||
|
{ count: this.state.pendingRoomJoin.size },
|
||||||
|
)} />
|
||||||
|
</InlineSpinner>
|
||||||
|
)}
|
||||||
{dnd}
|
{dnd}
|
||||||
{buttons}
|
{buttons}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -59,6 +59,7 @@ interface IProps {
|
||||||
fallbackHsUrl?: string;
|
fallbackHsUrl?: string;
|
||||||
defaultDeviceDisplayName?: string;
|
defaultDeviceDisplayName?: string;
|
||||||
fragmentAfterLogin?: string;
|
fragmentAfterLogin?: string;
|
||||||
|
defaultUsername?: string;
|
||||||
|
|
||||||
// Called when the user has logged in. Params:
|
// Called when the user has logged in. Params:
|
||||||
// - The object returned by the login API
|
// - The object returned by the login API
|
||||||
|
@ -119,7 +120,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
|
|
||||||
flows: null,
|
flows: null,
|
||||||
|
|
||||||
username: "",
|
username: props.defaultUsername? props.defaultUsername: '',
|
||||||
phoneCountry: null,
|
phoneCountry: null,
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ interface IProps {
|
||||||
is_url?: string;
|
is_url?: string;
|
||||||
session_id: string;
|
session_id: string;
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
}): void;
|
}): string;
|
||||||
// registration shouldn't know or care how login is done.
|
// registration shouldn't know or care how login is done.
|
||||||
onLoginClick(): void;
|
onLoginClick(): void;
|
||||||
onServerConfigChange(config: ValidatedServerConfig): void;
|
onServerConfigChange(config: ValidatedServerConfig): void;
|
||||||
|
@ -223,7 +223,8 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
this.setState({
|
this.setState({
|
||||||
flows: e.data.flows,
|
flows: e.data.flows,
|
||||||
});
|
});
|
||||||
} else if (e.httpStatus === 403 && e.errcode === "M_UNKNOWN") {
|
} else if (e.httpStatus === 403 || e.errcode === "M_FORBIDDEN") {
|
||||||
|
// Check for 403 or M_FORBIDDEN, Synapse used to send 403 M_UNKNOWN but now sends 403 M_FORBIDDEN.
|
||||||
// At this point registration is pretty much disabled, but before we do that let's
|
// At this point registration is pretty much disabled, but before we do that let's
|
||||||
// quickly check to see if the server supports SSO instead. If it does, we'll send
|
// quickly check to see if the server supports SSO instead. If it does, we'll send
|
||||||
// the user off to the login page to figure their account out.
|
// the user off to the login page to figure their account out.
|
||||||
|
@ -467,7 +468,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
let ssoSection;
|
let ssoSection;
|
||||||
if (this.state.ssoFlow) {
|
if (this.state.ssoFlow) {
|
||||||
let continueWithSection;
|
let continueWithSection;
|
||||||
const providers = this.state.ssoFlow["org.matrix.msc2858.identity_providers"] || [];
|
const providers = this.state.ssoFlow.identity_providers || [];
|
||||||
// when there is only a single (or 0) providers we show a wide button with `Continue with X` text
|
// when there is only a single (or 0) providers we show a wide button with `Continue with X` text
|
||||||
if (providers.length > 1) {
|
if (providers.length > 1) {
|
||||||
// i18n: ssoButtons is a placeholder to help translators understand context
|
// i18n: ssoButtons is a placeholder to help translators understand context
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016-2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {createRef} from 'react';
|
import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import classNames from 'classnames';
|
||||||
import classnames from 'classnames';
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -27,6 +25,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import { LocalisedPolicy, Policies } from '../../../Terms';
|
||||||
|
|
||||||
/* This file contains a collection of components which are used by the
|
/* This file contains a collection of components which are used by the
|
||||||
* InteractiveAuth to prompt the user to enter the information needed
|
* InteractiveAuth to prompt the user to enter the information needed
|
||||||
|
@ -74,36 +73,72 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
* focus: set the input focus appropriately in the form.
|
* focus: set the input focus appropriately in the form.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
enum AuthType {
|
||||||
|
Password = "m.login.password",
|
||||||
|
Recaptcha = "m.login.recaptcha",
|
||||||
|
Terms = "m.login.terms",
|
||||||
|
Email = "m.login.email.identity",
|
||||||
|
Msisdn = "m.login.msisdn",
|
||||||
|
Sso = "m.login.sso",
|
||||||
|
SsoUnstable = "org.matrix.login.sso",
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
interface IAuthDict {
|
||||||
|
type?: AuthType;
|
||||||
|
// TODO: Remove `user` once servers support proper UIA
|
||||||
|
// See https://github.com/vector-im/element-web/issues/10312
|
||||||
|
user?: string;
|
||||||
|
identifier?: any;
|
||||||
|
password?: string;
|
||||||
|
response?: string;
|
||||||
|
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||||
|
// See https://github.com/vector-im/element-web/issues/10312
|
||||||
|
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||||
|
threepid_creds?: any;
|
||||||
|
threepidCreds?: any;
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
export const DEFAULT_PHASE = 0;
|
export const DEFAULT_PHASE = 0;
|
||||||
|
|
||||||
@replaceableComponent("views.auth.PasswordAuthEntry")
|
interface IAuthEntryProps {
|
||||||
export class PasswordAuthEntry extends React.Component {
|
matrixClient: MatrixClient;
|
||||||
static LOGIN_TYPE = "m.login.password";
|
loginType: string;
|
||||||
|
authSessionId: string;
|
||||||
|
errorText?: string;
|
||||||
|
// Is the auth logic currently waiting for something to happen?
|
||||||
|
busy?: boolean;
|
||||||
|
onPhaseChange: (phase: number) => void;
|
||||||
|
submitAuthDict: (auth: IAuthDict) => void;
|
||||||
|
}
|
||||||
|
|
||||||
static propTypes = {
|
interface IPasswordAuthEntryState {
|
||||||
matrixClient: PropTypes.object.isRequired,
|
password: string;
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
}
|
||||||
errorText: PropTypes.string,
|
|
||||||
// is the auth logic currently waiting for something to
|
@replaceableComponent("views.auth.PasswordAuthEntry")
|
||||||
// happen?
|
export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswordAuthEntryState> {
|
||||||
busy: PropTypes.bool,
|
static LOGIN_TYPE = AuthType.Password;
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
};
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
password: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
private onSubmit = (e: FormEvent) => {
|
||||||
password: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
_onSubmit = e => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.props.busy) return;
|
if (this.props.busy) return;
|
||||||
|
|
||||||
this.props.submitAuthDict({
|
this.props.submitAuthDict({
|
||||||
type: PasswordAuthEntry.LOGIN_TYPE,
|
type: AuthType.Password,
|
||||||
// TODO: Remove `user` once servers support proper UIA
|
// TODO: Remove `user` once servers support proper UIA
|
||||||
// See https://github.com/vector-im/element-web/issues/10312
|
// See https://github.com/vector-im/element-web/issues/10312
|
||||||
user: this.props.matrixClient.credentials.userId,
|
user: this.props.matrixClient.credentials.userId,
|
||||||
|
@ -115,7 +150,7 @@ export class PasswordAuthEntry extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPasswordFieldChange = ev => {
|
private onPasswordFieldChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
// enable the submit button iff the password is non-empty
|
// enable the submit button iff the password is non-empty
|
||||||
this.setState({
|
this.setState({
|
||||||
password: ev.target.value,
|
password: ev.target.value,
|
||||||
|
@ -123,7 +158,7 @@ export class PasswordAuthEntry extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const passwordBoxClass = classnames({
|
const passwordBoxClass = classNames({
|
||||||
"error": this.props.errorText,
|
"error": this.props.errorText,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -155,7 +190,7 @@ export class PasswordAuthEntry extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{ _t("Confirm your identity by entering your account password below.") }</p>
|
<p>{ _t("Confirm your identity by entering your account password below.") }</p>
|
||||||
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
<form onSubmit={this.onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
||||||
<Field
|
<Field
|
||||||
className={passwordBoxClass}
|
className={passwordBoxClass}
|
||||||
type="password"
|
type="password"
|
||||||
|
@ -163,7 +198,7 @@ export class PasswordAuthEntry extends React.Component {
|
||||||
label={_t('Password')}
|
label={_t('Password')}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
value={this.state.password}
|
value={this.state.password}
|
||||||
onChange={this._onPasswordFieldChange}
|
onChange={this.onPasswordFieldChange}
|
||||||
/>
|
/>
|
||||||
<div className="mx_button_row">
|
<div className="mx_button_row">
|
||||||
{ submitButtonOrSpinner }
|
{ submitButtonOrSpinner }
|
||||||
|
@ -175,26 +210,26 @@ export class PasswordAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.RecaptchaAuthEntry")
|
/* eslint-disable camelcase */
|
||||||
export class RecaptchaAuthEntry extends React.Component {
|
interface IRecaptchaAuthEntryProps extends IAuthEntryProps {
|
||||||
static LOGIN_TYPE = "m.login.recaptcha";
|
stageParams?: {
|
||||||
|
public_key?: string;
|
||||||
static propTypes = {
|
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
|
||||||
stageParams: PropTypes.object.isRequired,
|
|
||||||
errorText: PropTypes.string,
|
|
||||||
busy: PropTypes.bool,
|
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
@replaceableComponent("views.auth.RecaptchaAuthEntry")
|
||||||
|
export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps> {
|
||||||
|
static LOGIN_TYPE = AuthType.Recaptcha;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCaptchaResponse = response => {
|
private onCaptchaResponse = (response: string) => {
|
||||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_submit");
|
CountlyAnalytics.instance.track("onboarding_grecaptcha_submit");
|
||||||
this.props.submitAuthDict({
|
this.props.submitAuthDict({
|
||||||
type: RecaptchaAuthEntry.LOGIN_TYPE,
|
type: AuthType.Recaptcha,
|
||||||
response: response,
|
response: response,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -230,7 +265,7 @@ export class RecaptchaAuthEntry extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CaptchaForm sitePublicKey={sitePublicKey}
|
<CaptchaForm sitePublicKey={sitePublicKey}
|
||||||
onCaptchaResponse={this._onCaptchaResponse}
|
onCaptchaResponse={this.onCaptchaResponse}
|
||||||
/>
|
/>
|
||||||
{ errorSection }
|
{ errorSection }
|
||||||
</div>
|
</div>
|
||||||
|
@ -238,18 +273,28 @@ export class RecaptchaAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.TermsAuthEntry")
|
interface ITermsAuthEntryProps extends IAuthEntryProps {
|
||||||
export class TermsAuthEntry extends React.Component {
|
stageParams?: {
|
||||||
static LOGIN_TYPE = "m.login.terms";
|
policies?: Policies;
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
|
||||||
stageParams: PropTypes.object.isRequired,
|
|
||||||
errorText: PropTypes.string,
|
|
||||||
busy: PropTypes.bool,
|
|
||||||
showContinue: PropTypes.bool,
|
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
showContinue: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocalisedPolicyWithId extends LocalisedPolicy {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITermsAuthEntryState {
|
||||||
|
policies: LocalisedPolicyWithId[];
|
||||||
|
toggledPolicies: {
|
||||||
|
[policy: string]: boolean;
|
||||||
|
};
|
||||||
|
errorText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.auth.TermsAuthEntry")
|
||||||
|
export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITermsAuthEntryState> {
|
||||||
|
static LOGIN_TYPE = AuthType.Terms;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -294,8 +339,11 @@ export class TermsAuthEntry extends React.Component {
|
||||||
|
|
||||||
initToggles[policyId] = false;
|
initToggles[policyId] = false;
|
||||||
|
|
||||||
langPolicy.id = policyId;
|
pickedPolicies.push({
|
||||||
pickedPolicies.push(langPolicy);
|
id: policyId,
|
||||||
|
name: langPolicy.name,
|
||||||
|
url: langPolicy.url,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -311,11 +359,11 @@ export class TermsAuthEntry extends React.Component {
|
||||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||||
}
|
}
|
||||||
|
|
||||||
tryContinue = () => {
|
public tryContinue = () => {
|
||||||
this._trySubmit();
|
this.trySubmit();
|
||||||
};
|
};
|
||||||
|
|
||||||
_togglePolicy(policyId) {
|
private togglePolicy(policyId: string) {
|
||||||
const newToggles = {};
|
const newToggles = {};
|
||||||
for (const policy of this.state.policies) {
|
for (const policy of this.state.policies) {
|
||||||
let checked = this.state.toggledPolicies[policy.id];
|
let checked = this.state.toggledPolicies[policy.id];
|
||||||
|
@ -326,7 +374,7 @@ export class TermsAuthEntry extends React.Component {
|
||||||
this.setState({"toggledPolicies": newToggles});
|
this.setState({"toggledPolicies": newToggles});
|
||||||
}
|
}
|
||||||
|
|
||||||
_trySubmit = () => {
|
private trySubmit = () => {
|
||||||
let allChecked = true;
|
let allChecked = true;
|
||||||
for (const policy of this.state.policies) {
|
for (const policy of this.state.policies) {
|
||||||
const checked = this.state.toggledPolicies[policy.id];
|
const checked = this.state.toggledPolicies[policy.id];
|
||||||
|
@ -334,7 +382,7 @@ export class TermsAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allChecked) {
|
if (allChecked) {
|
||||||
this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
|
this.props.submitAuthDict({type: AuthType.Terms});
|
||||||
CountlyAnalytics.instance.track("onboarding_terms_complete");
|
CountlyAnalytics.instance.track("onboarding_terms_complete");
|
||||||
} else {
|
} else {
|
||||||
this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
|
this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
|
||||||
|
@ -356,7 +404,7 @@ export class TermsAuthEntry extends React.Component {
|
||||||
checkboxes.push(
|
checkboxes.push(
|
||||||
// XXX: replace with StyledCheckbox
|
// XXX: replace with StyledCheckbox
|
||||||
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
||||||
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
|
<input type="checkbox" onChange={() => this.togglePolicy(policy.id)} checked={checked} />
|
||||||
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
|
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
|
||||||
</label>,
|
</label>,
|
||||||
);
|
);
|
||||||
|
@ -375,7 +423,7 @@ export class TermsAuthEntry extends React.Component {
|
||||||
if (this.props.showContinue !== false) {
|
if (this.props.showContinue !== false) {
|
||||||
// XXX: button classes
|
// XXX: button classes
|
||||||
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
|
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
|
||||||
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
|
onClick={this.trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -389,21 +437,18 @@ export class TermsAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
|
interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
|
||||||
export class EmailIdentityAuthEntry extends React.Component {
|
inputs?: {
|
||||||
static LOGIN_TYPE = "m.login.email.identity";
|
emailAddress?: string;
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
matrixClient: PropTypes.object.isRequired,
|
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
|
||||||
authSessionId: PropTypes.string.isRequired,
|
|
||||||
clientSecret: PropTypes.string.isRequired,
|
|
||||||
inputs: PropTypes.object.isRequired,
|
|
||||||
stageState: PropTypes.object.isRequired,
|
|
||||||
fail: PropTypes.func.isRequired,
|
|
||||||
setEmailSid: PropTypes.func.isRequired,
|
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
stageState?: {
|
||||||
|
emailSid: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
|
||||||
|
export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEntryProps> {
|
||||||
|
static LOGIN_TYPE = AuthType.Email;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||||
|
@ -427,7 +472,7 @@ export class EmailIdentityAuthEntry extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="mx_InteractiveAuthEntryComponents_emailWrapper">
|
<div className="mx_InteractiveAuthEntryComponents_emailWrapper">
|
||||||
<p>{ _t("A confirmation email has been sent to %(emailAddress)s",
|
<p>{ _t("A confirmation email has been sent to %(emailAddress)s",
|
||||||
{ emailAddress: (sub) => <b>{ this.props.inputs.emailAddress }</b> },
|
{ emailAddress: <b>{ this.props.inputs.emailAddress }</b> },
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<p>{ _t("Open the link in the email to continue registration.") }</p>
|
<p>{ _t("Open the link in the email to continue registration.") }</p>
|
||||||
|
@ -437,37 +482,44 @@ export class EmailIdentityAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IMsisdnAuthEntryProps extends IAuthEntryProps {
|
||||||
|
inputs: {
|
||||||
|
phoneCountry: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
};
|
||||||
|
clientSecret: string;
|
||||||
|
fail: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMsisdnAuthEntryState {
|
||||||
|
token: string;
|
||||||
|
requestingToken: boolean;
|
||||||
|
errorText: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.MsisdnAuthEntry")
|
@replaceableComponent("views.auth.MsisdnAuthEntry")
|
||||||
export class MsisdnAuthEntry extends React.Component {
|
export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsisdnAuthEntryState> {
|
||||||
static LOGIN_TYPE = "m.login.msisdn";
|
static LOGIN_TYPE = AuthType.Msisdn;
|
||||||
|
|
||||||
static propTypes = {
|
private submitUrl: string;
|
||||||
inputs: PropTypes.shape({
|
private sid: string;
|
||||||
phoneCountry: PropTypes.string,
|
private msisdn: string;
|
||||||
phoneNumber: PropTypes.string,
|
|
||||||
}),
|
|
||||||
fail: PropTypes.func,
|
|
||||||
clientSecret: PropTypes.func,
|
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
|
||||||
matrixClient: PropTypes.object,
|
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
constructor(props) {
|
||||||
token: '',
|
super(props);
|
||||||
requestingToken: false,
|
|
||||||
};
|
this.state = {
|
||||||
|
token: '',
|
||||||
|
requestingToken: false,
|
||||||
|
errorText: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||||
|
|
||||||
this._submitUrl = null;
|
|
||||||
this._sid = null;
|
|
||||||
this._msisdn = null;
|
|
||||||
this._tokenBox = null;
|
|
||||||
|
|
||||||
this.setState({requestingToken: true});
|
this.setState({requestingToken: true});
|
||||||
this._requestMsisdnToken().catch((e) => {
|
this.requestMsisdnToken().catch((e) => {
|
||||||
this.props.fail(e);
|
this.props.fail(e);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.setState({requestingToken: false});
|
this.setState({requestingToken: false});
|
||||||
|
@ -477,26 +529,26 @@ export class MsisdnAuthEntry extends React.Component {
|
||||||
/*
|
/*
|
||||||
* Requests a verification token by SMS.
|
* Requests a verification token by SMS.
|
||||||
*/
|
*/
|
||||||
_requestMsisdnToken() {
|
private requestMsisdnToken(): Promise<void> {
|
||||||
return this.props.matrixClient.requestRegisterMsisdnToken(
|
return this.props.matrixClient.requestRegisterMsisdnToken(
|
||||||
this.props.inputs.phoneCountry,
|
this.props.inputs.phoneCountry,
|
||||||
this.props.inputs.phoneNumber,
|
this.props.inputs.phoneNumber,
|
||||||
this.props.clientSecret,
|
this.props.clientSecret,
|
||||||
1, // TODO: Multiple send attempts?
|
1, // TODO: Multiple send attempts?
|
||||||
).then((result) => {
|
).then((result) => {
|
||||||
this._submitUrl = result.submit_url;
|
this.submitUrl = result.submit_url;
|
||||||
this._sid = result.sid;
|
this.sid = result.sid;
|
||||||
this._msisdn = result.msisdn;
|
this.msisdn = result.msisdn;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onTokenChange = e => {
|
private onTokenChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
token: e.target.value,
|
token: e.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onFormSubmit = async e => {
|
private onFormSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.state.token == '') return;
|
if (this.state.token == '') return;
|
||||||
|
|
||||||
|
@ -506,20 +558,20 @@ export class MsisdnAuthEntry extends React.Component {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result;
|
let result;
|
||||||
if (this._submitUrl) {
|
if (this.submitUrl) {
|
||||||
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
|
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
|
||||||
this._submitUrl, this._sid, this.props.clientSecret, this.state.token,
|
this.submitUrl, this.sid, this.props.clientSecret, this.state.token,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("The registration with MSISDN flow is misconfigured");
|
throw new Error("The registration with MSISDN flow is misconfigured");
|
||||||
}
|
}
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const creds = {
|
const creds = {
|
||||||
sid: this._sid,
|
sid: this.sid,
|
||||||
client_secret: this.props.clientSecret,
|
client_secret: this.props.clientSecret,
|
||||||
};
|
};
|
||||||
this.props.submitAuthDict({
|
this.props.submitAuthDict({
|
||||||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
type: AuthType.Msisdn,
|
||||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||||
// See https://github.com/vector-im/element-web/issues/10312
|
// See https://github.com/vector-im/element-web/issues/10312
|
||||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||||
|
@ -543,7 +595,7 @@ export class MsisdnAuthEntry extends React.Component {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
} else {
|
} else {
|
||||||
const enableSubmit = Boolean(this.state.token);
|
const enableSubmit = Boolean(this.state.token);
|
||||||
const submitClasses = classnames({
|
const submitClasses = classNames({
|
||||||
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
|
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
|
||||||
mx_GeneralButton: true,
|
mx_GeneralButton: true,
|
||||||
});
|
});
|
||||||
|
@ -558,16 +610,16 @@ export class MsisdnAuthEntry extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{ _t("A text message has been sent to %(msisdn)s",
|
<p>{ _t("A text message has been sent to %(msisdn)s",
|
||||||
{ msisdn: <i>{ this._msisdn }</i> },
|
{ msisdn: <i>{ this.msisdn }</i> },
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<p>{ _t("Please enter the code it contains:") }</p>
|
<p>{ _t("Please enter the code it contains:") }</p>
|
||||||
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||||
<form onSubmit={this._onFormSubmit}>
|
<form onSubmit={this.onFormSubmit}>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
|
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
|
||||||
value={this.state.token}
|
value={this.state.token}
|
||||||
onChange={this._onTokenChange}
|
onChange={this.onTokenChange}
|
||||||
aria-label={ _t("Code")}
|
aria-label={ _t("Code")}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
|
@ -584,40 +636,40 @@ export class MsisdnAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.SSOAuthEntry")
|
interface ISSOAuthEntryProps extends IAuthEntryProps {
|
||||||
export class SSOAuthEntry extends React.Component {
|
continueText?: string;
|
||||||
static propTypes = {
|
continueKind?: string;
|
||||||
matrixClient: PropTypes.object.isRequired,
|
onCancel?: () => void;
|
||||||
authSessionId: PropTypes.string.isRequired,
|
}
|
||||||
loginType: PropTypes.string.isRequired,
|
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
|
||||||
errorText: PropTypes.string,
|
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
continueText: PropTypes.string,
|
|
||||||
continueKind: PropTypes.string,
|
|
||||||
onCancel: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
static LOGIN_TYPE = "m.login.sso";
|
interface ISSOAuthEntryState {
|
||||||
static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso";
|
phase: number;
|
||||||
|
attemptFailed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.auth.SSOAuthEntry")
|
||||||
|
export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEntryState> {
|
||||||
|
static LOGIN_TYPE = AuthType.Sso;
|
||||||
|
static UNSTABLE_LOGIN_TYPE = AuthType.SsoUnstable;
|
||||||
|
|
||||||
static PHASE_PREAUTH = 1; // button to start SSO
|
static PHASE_PREAUTH = 1; // button to start SSO
|
||||||
static PHASE_POSTAUTH = 2; // button to confirm SSO completed
|
static PHASE_POSTAUTH = 2; // button to confirm SSO completed
|
||||||
|
|
||||||
_ssoUrl: string;
|
private ssoUrl: string;
|
||||||
|
private popupWindow: Window;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// We actually send the user through fallback auth so we don't have to
|
// We actually send the user through fallback auth so we don't have to
|
||||||
// deal with a redirect back to us, losing application context.
|
// deal with a redirect back to us, losing application context.
|
||||||
this._ssoUrl = props.matrixClient.getFallbackAuthUrl(
|
this.ssoUrl = props.matrixClient.getFallbackAuthUrl(
|
||||||
this.props.loginType,
|
this.props.loginType,
|
||||||
this.props.authSessionId,
|
this.props.authSessionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
this._popupWindow = null;
|
this.popupWindow = null;
|
||||||
window.addEventListener("message", this._onReceiveMessage);
|
window.addEventListener("message", this.onReceiveMessage);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
phase: SSOAuthEntry.PHASE_PREAUTH,
|
phase: SSOAuthEntry.PHASE_PREAUTH,
|
||||||
|
@ -625,44 +677,44 @@ export class SSOAuthEntry extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount() {
|
||||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
|
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener("message", this._onReceiveMessage);
|
window.removeEventListener("message", this.onReceiveMessage);
|
||||||
if (this._popupWindow) {
|
if (this.popupWindow) {
|
||||||
this._popupWindow.close();
|
this.popupWindow.close();
|
||||||
this._popupWindow = null;
|
this.popupWindow = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptFailed = () => {
|
public attemptFailed = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
attemptFailed: true,
|
attemptFailed: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onReceiveMessage = event => {
|
private onReceiveMessage = (event: MessageEvent) => {
|
||||||
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
|
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
|
||||||
if (this._popupWindow) {
|
if (this.popupWindow) {
|
||||||
this._popupWindow.close();
|
this.popupWindow.close();
|
||||||
this._popupWindow = null;
|
this.popupWindow = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onStartAuthClick = () => {
|
private onStartAuthClick = () => {
|
||||||
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost
|
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost
|
||||||
// certainly will need to open the thing in a new tab to avoid losing application
|
// certainly will need to open the thing in a new tab to avoid losing application
|
||||||
// context.
|
// context.
|
||||||
|
|
||||||
this._popupWindow = window.open(this._ssoUrl, "_blank");
|
this.popupWindow = window.open(this.ssoUrl, "_blank");
|
||||||
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
|
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
|
||||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
|
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
|
||||||
};
|
};
|
||||||
|
|
||||||
onConfirmClick = () => {
|
private onConfirmClick = () => {
|
||||||
this.props.submitAuthDict({});
|
this.props.submitAuthDict({});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -716,46 +768,37 @@ export class SSOAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.FallbackAuthEntry")
|
@replaceableComponent("views.auth.FallbackAuthEntry")
|
||||||
export class FallbackAuthEntry extends React.Component {
|
export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
|
||||||
static propTypes = {
|
private popupWindow: Window;
|
||||||
matrixClient: PropTypes.object.isRequired,
|
private fallbackButton = createRef<HTMLAnchorElement>();
|
||||||
authSessionId: PropTypes.string.isRequired,
|
|
||||||
loginType: PropTypes.string.isRequired,
|
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
|
||||||
errorText: PropTypes.string,
|
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// we have to make the user click a button, as browsers will block
|
// we have to make the user click a button, as browsers will block
|
||||||
// the popup if we open it immediately.
|
// the popup if we open it immediately.
|
||||||
this._popupWindow = null;
|
this.popupWindow = null;
|
||||||
window.addEventListener("message", this._onReceiveMessage);
|
window.addEventListener("message", this.onReceiveMessage);
|
||||||
|
|
||||||
this._fallbackButton = createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener("message", this._onReceiveMessage);
|
window.removeEventListener("message", this.onReceiveMessage);
|
||||||
if (this._popupWindow) {
|
if (this.popupWindow) {
|
||||||
this._popupWindow.close();
|
this.popupWindow.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
focus = () => {
|
public focus = () => {
|
||||||
if (this._fallbackButton.current) {
|
if (this.fallbackButton.current) {
|
||||||
this._fallbackButton.current.focus();
|
this.fallbackButton.current.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onShowFallbackClick = e => {
|
private onShowFallbackClick = (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
@ -763,10 +806,10 @@ export class FallbackAuthEntry extends React.Component {
|
||||||
this.props.loginType,
|
this.props.loginType,
|
||||||
this.props.authSessionId,
|
this.props.authSessionId,
|
||||||
);
|
);
|
||||||
this._popupWindow = window.open(url, "_blank");
|
this.popupWindow = window.open(url, "_blank");
|
||||||
};
|
};
|
||||||
|
|
||||||
_onReceiveMessage = event => {
|
private onReceiveMessage = (event: MessageEvent) => {
|
||||||
if (
|
if (
|
||||||
event.data === "authDone" &&
|
event.data === "authDone" &&
|
||||||
event.origin === this.props.matrixClient.getHomeserverUrl()
|
event.origin === this.props.matrixClient.getHomeserverUrl()
|
||||||
|
@ -786,27 +829,31 @@ export class FallbackAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<a href="" ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
<a href="" ref={this.fallbackButton} onClick={this.onShowFallbackClick}>{
|
||||||
|
_t("Start authentication")
|
||||||
|
}</a>
|
||||||
{errorSection}
|
{errorSection}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthEntryComponents = [
|
export default function getEntryComponentForLoginType(loginType: AuthType): typeof React.Component {
|
||||||
PasswordAuthEntry,
|
switch (loginType) {
|
||||||
RecaptchaAuthEntry,
|
case AuthType.Password:
|
||||||
EmailIdentityAuthEntry,
|
return PasswordAuthEntry;
|
||||||
MsisdnAuthEntry,
|
case AuthType.Recaptcha:
|
||||||
TermsAuthEntry,
|
return RecaptchaAuthEntry;
|
||||||
SSOAuthEntry,
|
case AuthType.Email:
|
||||||
];
|
return EmailIdentityAuthEntry;
|
||||||
|
case AuthType.Msisdn:
|
||||||
export default function getEntryComponentForLoginType(loginType) {
|
return MsisdnAuthEntry;
|
||||||
for (const c of AuthEntryComponents) {
|
case AuthType.Terms:
|
||||||
if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) {
|
return TermsAuthEntry;
|
||||||
return c;
|
case AuthType.Sso:
|
||||||
}
|
case AuthType.SsoUnstable:
|
||||||
|
return SSOAuthEntry;
|
||||||
|
default:
|
||||||
|
return FallbackAuthEntry;
|
||||||
}
|
}
|
||||||
return FallbackAuthEntry;
|
|
||||||
}
|
}
|
|
@ -22,6 +22,7 @@ import classNames from 'classnames';
|
||||||
import * as AvatarLogic from '../../../Avatar';
|
import * as AvatarLogic from '../../../Avatar';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||||
import {toPx} from "../../../utils/units";
|
import {toPx} from "../../../utils/units";
|
||||||
|
@ -44,12 +45,12 @@ interface IProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateUrls = (url, urls) => {
|
const calculateUrls = (url, urls, lowBandwidth) => {
|
||||||
// work out the full set of urls to try to load. This is formed like so:
|
// work out the full set of urls to try to load. This is formed like so:
|
||||||
// imageUrls: [ props.url, ...props.urls ]
|
// imageUrls: [ props.url, ...props.urls ]
|
||||||
|
|
||||||
let _urls = [];
|
let _urls = [];
|
||||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
if (!lowBandwidth) {
|
||||||
_urls = urls || [];
|
_urls = urls || [];
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
|
@ -63,7 +64,13 @@ const calculateUrls = (url, urls) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const useImageUrl = ({url, urls}): [string, () => void] => {
|
const useImageUrl = ({url, urls}): [string, () => void] => {
|
||||||
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls));
|
// Since this is a hot code path and the settings store can be slow, we
|
||||||
|
// use the cached lowBandwidth value from the room context if it exists
|
||||||
|
const roomContext = useContext(RoomContext);
|
||||||
|
const lowBandwidth = roomContext ?
|
||||||
|
roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth");
|
||||||
|
|
||||||
|
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth));
|
||||||
const [urlsIndex, setIndex] = useState<number>(0);
|
const [urlsIndex, setIndex] = useState<number>(0);
|
||||||
|
|
||||||
const onError = useCallback(() => {
|
const onError = useCallback(() => {
|
||||||
|
@ -71,7 +78,7 @@ const useImageUrl = ({url, urls}): [string, () => void] => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUrls(calculateUrls(url, urls));
|
setUrls(calculateUrls(url, urls, lowBandwidth));
|
||||||
setIndex(0);
|
setIndex(0);
|
||||||
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,10 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
||||||
if (this.props.room.roomId !== room.roomId) return;
|
if (this.props.room.roomId !== room.roomId) return;
|
||||||
|
|
||||||
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
|
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
|
||||||
this.setState({icon: this.calculateIcon()});
|
const newIcon = this.calculateIcon();
|
||||||
|
if (newIcon !== this.state.icon) {
|
||||||
|
this.setState({icon: newIcon});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,9 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -28,9 +28,11 @@ import Resend from '../../../Resend';
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||||
import { isContentActionable } from '../../../utils/EventUtils';
|
import { isContentActionable } from '../../../utils/EventUtils';
|
||||||
import {MenuItem} from "../../structures/ContextMenu";
|
import { MenuItem } from "../../structures/ContextMenu";
|
||||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
||||||
|
import ForwardDialog from "../dialogs/ForwardDialog";
|
||||||
|
|
||||||
export function canCancel(eventStatus) {
|
export function canCancel(eventStatus) {
|
||||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||||
|
@ -82,7 +84,7 @@ export default class MessageContextMenu extends React.Component {
|
||||||
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
|
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
|
||||||
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
|
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
|
||||||
&& this.props.mxEvent.getType() !== EventType.RoomEncryption;
|
&& this.props.mxEvent.getType() !== EventType.RoomEncryption;
|
||||||
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
|
let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli);
|
||||||
|
|
||||||
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
|
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
|
||||||
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
|
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
|
||||||
|
@ -92,7 +94,7 @@ export default class MessageContextMenu extends React.Component {
|
||||||
|
|
||||||
_isPinned() {
|
_isPinned() {
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||||
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
|
const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
|
||||||
if (!pinnedEvent) return false;
|
if (!pinnedEvent) return false;
|
||||||
const content = pinnedEvent.getContent();
|
const content = pinnedEvent.getContent();
|
||||||
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
||||||
|
@ -156,34 +158,32 @@ export default class MessageContextMenu extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
onForwardClick = () => {
|
onForwardClick = () => {
|
||||||
if (this.props.onCloseDialog) this.props.onCloseDialog();
|
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
|
||||||
dis.dispatch({
|
matrixClient: MatrixClientPeg.get(),
|
||||||
action: 'forward_event',
|
|
||||||
event: this.props.mxEvent,
|
event: this.props.mxEvent,
|
||||||
|
permalinkCreator: this.props.permalinkCreator,
|
||||||
});
|
});
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
onPinClick = () => {
|
onPinClick = () => {
|
||||||
MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '')
|
const cli = MatrixClientPeg.get();
|
||||||
.catch((e) => {
|
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||||
// Intercept the Event Not Found error and fall through the promise chain with no event.
|
const eventId = this.props.mxEvent.getId();
|
||||||
if (e.errcode === "M_NOT_FOUND") return null;
|
|
||||||
throw e;
|
|
||||||
})
|
|
||||||
.then((event) => {
|
|
||||||
const eventIds = (event ? event.pinned : []) || [];
|
|
||||||
if (!eventIds.includes(this.props.mxEvent.getId())) {
|
|
||||||
// Not pinned - add
|
|
||||||
eventIds.push(this.props.mxEvent.getId());
|
|
||||||
} else {
|
|
||||||
// Pinned - remove
|
|
||||||
eventIds.splice(eventIds.indexOf(this.props.mxEvent.getId()), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || [];
|
||||||
cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, '');
|
if (pinnedIds.includes(eventId)) {
|
||||||
|
pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
|
||||||
|
} else {
|
||||||
|
pinnedIds.push(eventId);
|
||||||
|
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||||
|
event_ids: [
|
||||||
|
...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids,
|
||||||
|
eventId,
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
cli.sendStateEvent(this.props.mxEvent.getRoomId(), EventType.RoomPinnedEvents, { pinned: pinnedIds }, "");
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,8 @@ interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
||||||
showUnpin?: boolean;
|
showUnpin?: boolean;
|
||||||
// override delete handler
|
// override delete handler
|
||||||
onDeleteClick?(): void;
|
onDeleteClick?(): void;
|
||||||
|
// override edit handler
|
||||||
|
onEditClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WidgetContextMenu: React.FC<IProps> = ({
|
const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
|
@ -47,6 +49,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
app,
|
app,
|
||||||
userWidget,
|
userWidget,
|
||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
|
onEditClick,
|
||||||
showUnpin,
|
showUnpin,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -89,12 +92,16 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
|
|
||||||
let editButton;
|
let editButton;
|
||||||
if (canModify && WidgetUtils.isManagedByManager(app)) {
|
if (canModify && WidgetUtils.isManagedByManager(app)) {
|
||||||
const onEditClick = () => {
|
const _onEditClick = () => {
|
||||||
WidgetUtils.editWidget(room, app);
|
if (onEditClick) {
|
||||||
|
onEditClick();
|
||||||
|
} else {
|
||||||
|
WidgetUtils.editWidget(room, app);
|
||||||
|
}
|
||||||
onFinished();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
editButton = <IconizedContextMenuOption onClick={onEditClick} label={_t("Edit")} />;
|
editButton = <IconizedContextMenuOption onClick={_onEditClick} label={_t("Edit")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let snapshotButton;
|
let snapshotButton;
|
||||||
|
@ -116,24 +123,29 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
|
|
||||||
let deleteButton;
|
let deleteButton;
|
||||||
if (onDeleteClick || canModify) {
|
if (onDeleteClick || canModify) {
|
||||||
const onDeleteClickDefault = () => {
|
const _onDeleteClick = () => {
|
||||||
// Show delete confirmation dialog
|
if (onDeleteClick) {
|
||||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
onDeleteClick();
|
||||||
title: _t("Delete Widget"),
|
} else {
|
||||||
description: _t(
|
// Show delete confirmation dialog
|
||||||
"Deleting a widget removes it for all users in this room." +
|
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||||
" Are you sure you want to delete this widget?"),
|
title: _t("Delete Widget"),
|
||||||
button: _t("Delete widget"),
|
description: _t(
|
||||||
onFinished: (confirmed) => {
|
"Deleting a widget removes it for all users in this room." +
|
||||||
if (!confirmed) return;
|
" Are you sure you want to delete this widget?"),
|
||||||
WidgetUtils.setRoomWidget(roomId, app.id);
|
button: _t("Delete widget"),
|
||||||
},
|
onFinished: (confirmed) => {
|
||||||
});
|
if (!confirmed) return;
|
||||||
|
WidgetUtils.setRoomWidget(roomId, app.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onFinished();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteButton = <IconizedContextMenuOption
|
deleteButton = <IconizedContextMenuOption
|
||||||
onClick={onDeleteClick || onDeleteClickDefault}
|
onClick={_onDeleteClick}
|
||||||
label={userWidget ? _t("Remove") : _t("Remove for everyone")}
|
label={userWidget ? _t("Remove") : _t("Remove for everyone")}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/Recent
|
||||||
import ProgressBar from "../elements/ProgressBar";
|
import ProgressBar from "../elements/ProgressBar";
|
||||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
|
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
|
@ -74,37 +75,47 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
onFinished,
|
onFinished,
|
||||||
}) => {
|
}) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]);
|
const visibleRooms = useMemo(() => cli.getVisibleRooms().filter(r => r.getMyMembership() === "join"), [cli]);
|
||||||
|
|
||||||
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
||||||
const [progress, setProgress] = useState<number>(null);
|
const [progress, setProgress] = useState<number>(null);
|
||||||
const [error, setError] = useState<Error>(null);
|
const [error, setError] = useState<Error>(null);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const lcQuery = query.toLowerCase();
|
const lcQuery = query.toLowerCase().trim();
|
||||||
|
|
||||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
const existingSubspacesSet = useMemo(() => new Set(SpaceStore.instance.getChildSpaces(space.roomId)), [space]);
|
||||||
const existingSubspacesSet = new Set(existingSubspaces);
|
const existingRoomsSet = useMemo(() => new Set(SpaceStore.instance.getChildRooms(space.roomId)), [space]);
|
||||||
const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId));
|
|
||||||
|
|
||||||
const joinRule = space.getJoinRule();
|
const [spaces, rooms, dms] = useMemo(() => {
|
||||||
const [spaces, rooms, dms] = visibleRooms.reduce((arr, room) => {
|
let rooms = visibleRooms;
|
||||||
if (room.getMyMembership() !== "join") return arr;
|
|
||||||
if (!room.name.toLowerCase().includes(lcQuery)) return arr;
|
|
||||||
|
|
||||||
if (room.isSpaceRoom()) {
|
if (lcQuery) {
|
||||||
if (room !== space && !existingSubspacesSet.has(room)) {
|
const matcher = new QueryMatcher<Room>(visibleRooms, {
|
||||||
arr[0].push(room);
|
keys: ["name"],
|
||||||
}
|
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
|
||||||
} else if (!existingRoomsSet.has(room)) {
|
shouldMatchWordsOnly: false,
|
||||||
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
|
});
|
||||||
arr[1].push(room);
|
|
||||||
} else if (joinRule !== "public") {
|
rooms = matcher.match(lcQuery);
|
||||||
// Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones.
|
|
||||||
arr[2].push(room);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return arr;
|
|
||||||
}, [[], [], []]);
|
const joinRule = space.getJoinRule();
|
||||||
|
return sortRooms(rooms).reduce((arr, room) => {
|
||||||
|
if (room.isSpaceRoom()) {
|
||||||
|
if (room !== space && !existingSubspacesSet.has(room)) {
|
||||||
|
arr[0].push(room);
|
||||||
|
}
|
||||||
|
} else if (!existingRoomsSet.has(room)) {
|
||||||
|
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
|
||||||
|
arr[1].push(room);
|
||||||
|
} else if (joinRule !== "public") {
|
||||||
|
// Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones.
|
||||||
|
arr[2].push(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [[], [], []]);
|
||||||
|
}, [visibleRooms, space, lcQuery, existingRoomsSet, existingSubspacesSet]);
|
||||||
|
|
||||||
const addRooms = async () => {
|
const addRooms = async () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
@ -201,7 +212,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
autoComplete={true}
|
autoComplete={true}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
|
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
|
||||||
{ rooms.length > 0 ? (
|
{ rooms.length > 0 ? (
|
||||||
<div className="mx_AddExistingToSpace_section">
|
<div className="mx_AddExistingToSpace_section">
|
||||||
<h3>{ _t("Rooms") }</h3>
|
<h3>{ _t("Rooms") }</h3>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,27 +15,47 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import withValidation from '../elements/Validation';
|
import withValidation, { IFieldState } from '../elements/Validation';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import {Key} from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import {privateShouldBeEncrypted} from "../../../createRoom";
|
import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
|
||||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Field from "../elements/Field";
|
||||||
|
import RoomAliasField from "../elements/RoomAliasField";
|
||||||
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
|
import BaseDialog from "../dialogs/BaseDialog";
|
||||||
|
import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
defaultPublic?: boolean;
|
||||||
|
defaultName?: string;
|
||||||
|
parentSpace?: Room;
|
||||||
|
onFinished(proceed: boolean, opts?: IOpts): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
isPublic: boolean;
|
||||||
|
isEncrypted: boolean;
|
||||||
|
name: string;
|
||||||
|
topic: string;
|
||||||
|
alias: string;
|
||||||
|
detailsOpen: boolean;
|
||||||
|
noFederate: boolean;
|
||||||
|
nameIsValid: boolean;
|
||||||
|
canChangeEncryption: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.CreateRoomDialog")
|
@replaceableComponent("views.dialogs.CreateRoomDialog")
|
||||||
export default class CreateRoomDialog extends React.Component {
|
export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private nameField = createRef<Field>();
|
||||||
onFinished: PropTypes.func.isRequired,
|
private aliasField = createRef<RoomAliasField>();
|
||||||
defaultPublic: PropTypes.bool,
|
|
||||||
parentSpace: PropTypes.instanceOf(Room),
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -44,7 +64,7 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
isPublic: this.props.defaultPublic || false,
|
isPublic: this.props.defaultPublic || false,
|
||||||
isEncrypted: privateShouldBeEncrypted(),
|
isEncrypted: privateShouldBeEncrypted(),
|
||||||
name: "",
|
name: this.props.defaultName || "",
|
||||||
topic: "",
|
topic: "",
|
||||||
alias: "",
|
alias: "",
|
||||||
detailsOpen: false,
|
detailsOpen: false,
|
||||||
|
@ -53,27 +73,26 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
canChangeEncryption: true,
|
canChangeEncryption: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
MatrixClientPeg.get().doesServerForceEncryptionForPreset("private")
|
MatrixClientPeg.get().doesServerForceEncryptionForPreset(Preset.PrivateChat)
|
||||||
.then(isForced => this.setState({canChangeEncryption: !isForced}));
|
.then(isForced => this.setState({ canChangeEncryption: !isForced }));
|
||||||
}
|
}
|
||||||
|
|
||||||
_roomCreateOptions() {
|
private roomCreateOptions() {
|
||||||
const opts = {};
|
const opts: IOpts = {};
|
||||||
const createOpts = opts.createOpts = {};
|
const createOpts: IOpts["createOpts"] = opts.createOpts = {};
|
||||||
createOpts.name = this.state.name;
|
createOpts.name = this.state.name;
|
||||||
if (this.state.isPublic) {
|
if (this.state.isPublic) {
|
||||||
createOpts.visibility = "public";
|
createOpts.visibility = Visibility.Public;
|
||||||
createOpts.preset = "public_chat";
|
createOpts.preset = Preset.PublicChat;
|
||||||
opts.guestAccess = false;
|
opts.guestAccess = false;
|
||||||
const {alias} = this.state;
|
const { alias } = this.state;
|
||||||
const localPart = alias.substr(1, alias.indexOf(":") - 1);
|
createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1);
|
||||||
createOpts['room_alias_name'] = localPart;
|
|
||||||
}
|
}
|
||||||
if (this.state.topic) {
|
if (this.state.topic) {
|
||||||
createOpts.topic = this.state.topic;
|
createOpts.topic = this.state.topic;
|
||||||
}
|
}
|
||||||
if (this.state.noFederate) {
|
if (this.state.noFederate) {
|
||||||
createOpts.creation_content = {'m.federate': false};
|
createOpts.creation_content = { 'm.federate': false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.state.isPublic) {
|
if (!this.state.isPublic) {
|
||||||
|
@ -98,16 +117,14 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._detailsRef.addEventListener("toggle", this.onDetailsToggled);
|
|
||||||
// move focus to first field when showing dialog
|
// move focus to first field when showing dialog
|
||||||
this._nameFieldRef.focus();
|
this.nameField.current.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._detailsRef.removeEventListener("toggle", this.onDetailsToggled);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyDown = event => {
|
private onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === Key.ENTER) {
|
if (event.key === Key.ENTER) {
|
||||||
this.onOk();
|
this.onOk();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -115,26 +132,26 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onOk = async () => {
|
private onOk = async () => {
|
||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
activeElement.blur();
|
activeElement.blur();
|
||||||
}
|
}
|
||||||
await this._nameFieldRef.validate({allowEmpty: false});
|
await this.nameField.current.validate({allowEmpty: false});
|
||||||
if (this._aliasFieldRef) {
|
if (this.aliasField.current) {
|
||||||
await this._aliasFieldRef.validate({allowEmpty: false});
|
await this.aliasField.current.validate({allowEmpty: false});
|
||||||
}
|
}
|
||||||
// Validation and state updates are async, so we need to wait for them to complete
|
// Validation and state updates are async, so we need to wait for them to complete
|
||||||
// first. Queue a `setState` callback and wait for it to resolve.
|
// first. Queue a `setState` callback and wait for it to resolve.
|
||||||
await new Promise(resolve => this.setState({}, resolve));
|
await new Promise<void>(resolve => this.setState({}, resolve));
|
||||||
if (this.state.nameIsValid && (!this._aliasFieldRef || this._aliasFieldRef.isValid)) {
|
if (this.state.nameIsValid && (!this.aliasField.current || this.aliasField.current.isValid)) {
|
||||||
this.props.onFinished(true, this._roomCreateOptions());
|
this.props.onFinished(true, this.roomCreateOptions());
|
||||||
} else {
|
} else {
|
||||||
let field;
|
let field;
|
||||||
if (!this.state.nameIsValid) {
|
if (!this.state.nameIsValid) {
|
||||||
field = this._nameFieldRef;
|
field = this.nameField.current;
|
||||||
} else if (this._aliasFieldRef && !this._aliasFieldRef.isValid) {
|
} else if (this.aliasField.current && !this.aliasField.current.isValid) {
|
||||||
field = this._aliasFieldRef;
|
field = this.aliasField.current;
|
||||||
}
|
}
|
||||||
if (field) {
|
if (field) {
|
||||||
field.focus();
|
field.focus();
|
||||||
|
@ -143,49 +160,45 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onCancel = () => {
|
private onCancel = () => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
onNameChange = ev => {
|
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({name: ev.target.value});
|
this.setState({ name: ev.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onTopicChange = ev => {
|
private onTopicChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({topic: ev.target.value});
|
this.setState({ topic: ev.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onPublicChange = isPublic => {
|
private onPublicChange = (isPublic: boolean) => {
|
||||||
this.setState({isPublic});
|
this.setState({ isPublic });
|
||||||
};
|
};
|
||||||
|
|
||||||
onEncryptedChange = isEncrypted => {
|
private onEncryptedChange = (isEncrypted: boolean) => {
|
||||||
this.setState({isEncrypted});
|
this.setState({ isEncrypted });
|
||||||
};
|
};
|
||||||
|
|
||||||
onAliasChange = alias => {
|
private onAliasChange = (alias: string) => {
|
||||||
this.setState({alias});
|
this.setState({ alias });
|
||||||
};
|
};
|
||||||
|
|
||||||
onDetailsToggled = ev => {
|
private onDetailsToggled = (ev: SyntheticEvent<HTMLDetailsElement>) => {
|
||||||
this.setState({detailsOpen: ev.target.open});
|
this.setState({ detailsOpen: (ev.target as HTMLDetailsElement).open });
|
||||||
};
|
};
|
||||||
|
|
||||||
onNoFederateChange = noFederate => {
|
private onNoFederateChange = (noFederate: boolean) => {
|
||||||
this.setState({noFederate});
|
this.setState({ noFederate });
|
||||||
};
|
};
|
||||||
|
|
||||||
collectDetailsRef = ref => {
|
private onNameValidate = async (fieldState: IFieldState) => {
|
||||||
this._detailsRef = ref;
|
const result = await CreateRoomDialog.validateRoomName(fieldState);
|
||||||
};
|
|
||||||
|
|
||||||
onNameValidate = async fieldState => {
|
|
||||||
const result = await CreateRoomDialog._validateRoomName(fieldState);
|
|
||||||
this.setState({nameIsValid: result.valid});
|
this.setState({nameIsValid: result.valid});
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
static _validateRoomName = withValidation({
|
private static validateRoomName = withValidation({
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "required",
|
key: "required",
|
||||||
|
@ -196,18 +209,17 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
|
||||||
const Field = sdk.getComponent('views.elements.Field');
|
|
||||||
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
|
|
||||||
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
|
|
||||||
|
|
||||||
let aliasField;
|
let aliasField;
|
||||||
if (this.state.isPublic) {
|
if (this.state.isPublic) {
|
||||||
const domain = MatrixClientPeg.get().getDomain();
|
const domain = MatrixClientPeg.get().getDomain();
|
||||||
aliasField = (
|
aliasField = (
|
||||||
<div className="mx_CreateRoomDialog_aliasContainer">
|
<div className="mx_CreateRoomDialog_aliasContainer">
|
||||||
<RoomAliasField ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
|
<RoomAliasField
|
||||||
|
ref={this.aliasField}
|
||||||
|
onChange={this.onAliasChange}
|
||||||
|
domain={domain}
|
||||||
|
value={this.state.alias}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -270,16 +282,34 @@ export default class CreateRoomDialog extends React.Component {
|
||||||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
|
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<Field ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
|
<Field
|
||||||
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} className="mx_CreateRoomDialog_topic" />
|
ref={this.nameField}
|
||||||
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
|
label={_t('Name')}
|
||||||
|
onChange={this.onNameChange}
|
||||||
|
onValidate={this.onNameValidate}
|
||||||
|
value={this.state.name}
|
||||||
|
className="mx_CreateRoomDialog_name"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label={_t('Topic (optional)')}
|
||||||
|
onChange={this.onTopicChange}
|
||||||
|
value={this.state.topic}
|
||||||
|
className="mx_CreateRoomDialog_topic"
|
||||||
|
/>
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
label={_t("Make this room public")}
|
||||||
|
onChange={this.onPublicChange}
|
||||||
|
value={this.state.isPublic}
|
||||||
|
/>
|
||||||
{ publicPrivateLabel }
|
{ publicPrivateLabel }
|
||||||
{ e2eeSection }
|
{ e2eeSection }
|
||||||
{ aliasField }
|
{ aliasField }
|
||||||
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
|
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">
|
||||||
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>
|
<summary className="mx_CreateRoomDialog_details_summary">
|
||||||
|
{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }
|
||||||
|
</summary>
|
||||||
<LabelledToggleSwitch
|
<LabelledToggleSwitch
|
||||||
label={_t(
|
label={_t(
|
||||||
"Block anyone not part of %(serverName)s from ever joining this room.",
|
"Block anyone not part of %(serverName)s from ever joining this room.",
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2018-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,14 +15,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useState, useEffect} from 'react';
|
import React, { useState, useEffect, ChangeEvent, MouseEvent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PHASE_UNSENT,
|
PHASE_UNSENT,
|
||||||
|
@ -30,27 +30,33 @@ import {
|
||||||
PHASE_DONE,
|
PHASE_DONE,
|
||||||
PHASE_STARTED,
|
PHASE_STARTED,
|
||||||
PHASE_CANCELLED,
|
PHASE_CANCELLED,
|
||||||
|
VerificationRequest,
|
||||||
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
import WidgetStore from "../../../stores/WidgetStore";
|
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
||||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
import {SETTINGS} from "../../../settings/Settings";
|
import { SETTINGS } from "../../../settings/Settings";
|
||||||
import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
|
import SettingsStore, { LEVEL_ORDER } from "../../../settings/SettingsStore";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ErrorDialog from "./ErrorDialog";
|
import ErrorDialog from "./ErrorDialog";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { SettingLevel } from '../../../settings/SettingLevel';
|
||||||
|
|
||||||
class GenericEditor extends React.PureComponent {
|
interface IGenericEditorProps {
|
||||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
interface IGenericEditorState {
|
||||||
super(props);
|
message?: string;
|
||||||
this._onChange = this._onChange.bind(this);
|
[inputId: string]: boolean | string;
|
||||||
this.onBack = this.onBack.bind(this);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onBack() {
|
abstract class GenericEditor<
|
||||||
|
P extends IGenericEditorProps = IGenericEditorProps,
|
||||||
|
S extends IGenericEditorState = IGenericEditorState,
|
||||||
|
> extends React.PureComponent<P, S> {
|
||||||
|
protected onBack = () => {
|
||||||
if (this.state.message) {
|
if (this.state.message) {
|
||||||
this.setState({ message: null });
|
this.setState({ message: null });
|
||||||
} else {
|
} else {
|
||||||
|
@ -58,47 +64,60 @@ class GenericEditor extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onChange(e) {
|
protected onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
// @ts-ignore: Unsure how to convince TS this is okay when the state
|
||||||
|
// type can be extended.
|
||||||
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttons() {
|
protected abstract send();
|
||||||
|
|
||||||
|
protected buttons(): React.ReactNode {
|
||||||
return <div className="mx_Dialog_buttons">
|
return <div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
textInput(id, label) {
|
protected textInput(id: string, label: string): React.ReactNode {
|
||||||
return <Field
|
return <Field
|
||||||
id={id}
|
id={id}
|
||||||
label={label}
|
label={label}
|
||||||
size="42"
|
size={42}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="on"
|
autoComplete="on"
|
||||||
value={this.state[id]}
|
value={this.state[id] as string}
|
||||||
onChange={this._onChange}
|
onChange={this.onChange}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SendCustomEvent extends GenericEditor {
|
interface ISendCustomEventProps extends IGenericEditorProps {
|
||||||
static getLabel() { return _t('Send Custom Event'); }
|
room: Room;
|
||||||
|
forceStateEvent?: boolean;
|
||||||
static propTypes = {
|
forceGeneralEvent?: boolean;
|
||||||
onBack: PropTypes.func.isRequired,
|
inputs?: {
|
||||||
room: PropTypes.instanceOf(Room).isRequired,
|
eventType?: string;
|
||||||
forceStateEvent: PropTypes.bool,
|
stateKey?: string;
|
||||||
forceGeneralEvent: PropTypes.bool,
|
evContent?: string;
|
||||||
inputs: PropTypes.object,
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISendCustomEventState extends IGenericEditorState {
|
||||||
|
isStateEvent: boolean;
|
||||||
|
eventType: string;
|
||||||
|
stateKey: string;
|
||||||
|
evContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendCustomEvent extends GenericEditor<ISendCustomEventProps, ISendCustomEventState> {
|
||||||
|
static getLabel() { return _t('Send Custom Event'); }
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this._send = this._send.bind(this);
|
|
||||||
|
|
||||||
const {eventType, stateKey, evContent} = Object.assign({
|
const {eventType, stateKey, evContent} = Object.assign({
|
||||||
eventType: '',
|
eventType: '',
|
||||||
|
@ -115,7 +134,7 @@ export class SendCustomEvent extends GenericEditor {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
send(content) {
|
private doSend(content: object): Promise<void> {
|
||||||
const cli = this.context;
|
const cli = this.context;
|
||||||
if (this.state.isStateEvent) {
|
if (this.state.isStateEvent) {
|
||||||
return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey);
|
return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey);
|
||||||
|
@ -124,7 +143,7 @@ export class SendCustomEvent extends GenericEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _send() {
|
protected send = async () => {
|
||||||
if (this.state.eventType === '') {
|
if (this.state.eventType === '') {
|
||||||
this.setState({ message: _t('You must specify an event type!') });
|
this.setState({ message: _t('You must specify an event type!') });
|
||||||
return;
|
return;
|
||||||
|
@ -133,7 +152,7 @@ export class SendCustomEvent extends GenericEditor {
|
||||||
let message;
|
let message;
|
||||||
try {
|
try {
|
||||||
const content = JSON.parse(this.state.evContent);
|
const content = JSON.parse(this.state.evContent);
|
||||||
await this.send(content);
|
await this.doSend(content);
|
||||||
message = _t('Event sent!');
|
message = _t('Event sent!');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
||||||
|
@ -147,7 +166,7 @@ export class SendCustomEvent extends GenericEditor {
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{ this.state.message }
|
{ this.state.message }
|
||||||
</div>
|
</div>
|
||||||
{ this._buttons() }
|
{ this.buttons() }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,35 +182,51 @@ export class SendCustomEvent extends GenericEditor {
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
||||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||||
{ showTglFlip && <div style={{float: "right"}}>
|
{ showTglFlip && <div style={{float: "right"}}>
|
||||||
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} />
|
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" />
|
type="checkbox"
|
||||||
|
checked={this.state.isStateEvent}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
<label className="mx_DevTools_tgl-btn"
|
||||||
|
data-tg-off="Event"
|
||||||
|
data-tg-on="State Event"
|
||||||
|
htmlFor="isStateEvent"
|
||||||
|
/>
|
||||||
</div> }
|
</div> }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SendAccountData extends GenericEditor {
|
interface ISendAccountDataProps extends IGenericEditorProps {
|
||||||
static getLabel() { return _t('Send Account Data'); }
|
room: Room;
|
||||||
|
isRoomAccountData: boolean;
|
||||||
static propTypes = {
|
forceMode: boolean;
|
||||||
room: PropTypes.instanceOf(Room).isRequired,
|
inputs?: {
|
||||||
isRoomAccountData: PropTypes.bool,
|
eventType?: string;
|
||||||
forceMode: PropTypes.bool,
|
evContent?: string;
|
||||||
inputs: PropTypes.object,
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISendAccountDataState extends IGenericEditorState {
|
||||||
|
isRoomAccountData: boolean;
|
||||||
|
eventType: string;
|
||||||
|
evContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SendAccountData extends GenericEditor<ISendAccountDataProps, ISendAccountDataState> {
|
||||||
|
static getLabel() { return _t('Send Account Data'); }
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this._send = this._send.bind(this);
|
|
||||||
|
|
||||||
const {eventType, evContent} = Object.assign({
|
const {eventType, evContent} = Object.assign({
|
||||||
eventType: '',
|
eventType: '',
|
||||||
|
@ -206,7 +241,7 @@ class SendAccountData extends GenericEditor {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
send(content) {
|
private doSend(content: object): Promise<void> {
|
||||||
const cli = this.context;
|
const cli = this.context;
|
||||||
if (this.state.isRoomAccountData) {
|
if (this.state.isRoomAccountData) {
|
||||||
return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content);
|
return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content);
|
||||||
|
@ -214,7 +249,7 @@ class SendAccountData extends GenericEditor {
|
||||||
return cli.setAccountData(this.state.eventType, content);
|
return cli.setAccountData(this.state.eventType, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _send() {
|
protected send = async () => {
|
||||||
if (this.state.eventType === '') {
|
if (this.state.eventType === '') {
|
||||||
this.setState({ message: _t('You must specify an event type!') });
|
this.setState({ message: _t('You must specify an event type!') });
|
||||||
return;
|
return;
|
||||||
|
@ -223,7 +258,7 @@ class SendAccountData extends GenericEditor {
|
||||||
let message;
|
let message;
|
||||||
try {
|
try {
|
||||||
const content = JSON.parse(this.state.evContent);
|
const content = JSON.parse(this.state.evContent);
|
||||||
await this.send(content);
|
await this.doSend(content);
|
||||||
message = _t('Event sent!');
|
message = _t('Event sent!');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
||||||
|
@ -237,7 +272,7 @@ class SendAccountData extends GenericEditor {
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{ this.state.message }
|
{ this.state.message }
|
||||||
</div>
|
</div>
|
||||||
{ this._buttons() }
|
{ this.buttons() }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,14 +282,23 @@ class SendAccountData extends GenericEditor {
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
||||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||||
{ !this.state.message && <div style={{float: "right"}}>
|
{ !this.state.message && <div style={{float: "right"}}>
|
||||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} disabled={this.props.forceMode} />
|
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
|
type="checkbox"
|
||||||
|
checked={this.state.isRoomAccountData}
|
||||||
|
disabled={this.props.forceMode}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
<label className="mx_DevTools_tgl-btn"
|
||||||
|
data-tg-off="Account Data"
|
||||||
|
data-tg-on="Room Data"
|
||||||
|
htmlFor="isRoomAccountData"
|
||||||
|
/>
|
||||||
</div> }
|
</div> }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -264,17 +308,22 @@ class SendAccountData extends GenericEditor {
|
||||||
const INITIAL_LOAD_TILES = 20;
|
const INITIAL_LOAD_TILES = 20;
|
||||||
const LOAD_TILES_STEP_SIZE = 50;
|
const LOAD_TILES_STEP_SIZE = 50;
|
||||||
|
|
||||||
class FilteredList extends React.PureComponent {
|
interface IFilteredListProps {
|
||||||
static propTypes = {
|
children: React.ReactElement[];
|
||||||
children: PropTypes.any,
|
query: string;
|
||||||
query: PropTypes.string,
|
onChange: (value: string) => void;
|
||||||
onChange: PropTypes.func,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
static filterChildren(children, query) {
|
interface IFilteredListState {
|
||||||
|
filteredChildren: React.ReactElement[];
|
||||||
|
truncateAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FilteredList extends React.PureComponent<IFilteredListProps, IFilteredListState> {
|
||||||
|
static filterChildren(children: React.ReactElement[], query: string): React.ReactElement[] {
|
||||||
if (!query) return children;
|
if (!query) return children;
|
||||||
const lcQuery = query.toLowerCase();
|
const lcQuery = query.toLowerCase();
|
||||||
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
return children.filter((child) => child.key.toString().toLowerCase().includes(lcQuery));
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -295,27 +344,27 @@ class FilteredList extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showAll = () => {
|
private showAll = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE,
|
truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
createOverflowElement = (overflowCount: number, totalCount: number) => {
|
private createOverflowElement = (overflowCount: number, totalCount: number) => {
|
||||||
return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}>
|
return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}>
|
||||||
{ _t("and %(count)s others...", { count: overflowCount }) }
|
{ _t("and %(count)s others...", { count: overflowCount }) }
|
||||||
</button>;
|
</button>;
|
||||||
};
|
};
|
||||||
|
|
||||||
onQuery = (ev) => {
|
private onQuery = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (this.props.onChange) this.props.onChange(ev.target.value);
|
if (this.props.onChange) this.props.onChange(ev.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
getChildren = (start: number, end: number) => {
|
private getChildren = (start: number, end: number): React.ReactElement[] => {
|
||||||
return this.state.filteredChildren.slice(start, end);
|
return this.state.filteredChildren.slice(start, end);
|
||||||
};
|
};
|
||||||
|
|
||||||
getChildCount = (): number => {
|
private getChildCount = (): number => {
|
||||||
return this.state.filteredChildren.length;
|
return this.state.filteredChildren.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -336,28 +385,31 @@ class FilteredList extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RoomStateExplorer extends React.PureComponent {
|
interface IExplorerProps {
|
||||||
static getLabel() { return _t('Explore Room State'); }
|
room: Room;
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
static propTypes = {
|
interface IRoomStateExplorerState {
|
||||||
onBack: PropTypes.func.isRequired,
|
eventType?: string;
|
||||||
room: PropTypes.instanceOf(Room).isRequired,
|
event?: MatrixEvent;
|
||||||
};
|
editing: boolean;
|
||||||
|
queryEventType: string;
|
||||||
|
queryStateKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateExplorerState> {
|
||||||
|
static getLabel() { return _t('Explore Room State'); }
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
roomStateEvents: Map<string, Map<string, MatrixEvent>>;
|
private roomStateEvents: Map<string, Map<string, MatrixEvent>>;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.roomStateEvents = this.props.room.currentState.events;
|
this.roomStateEvents = this.props.room.currentState.events;
|
||||||
|
|
||||||
this.onBack = this.onBack.bind(this);
|
|
||||||
this.editEv = this.editEv.bind(this);
|
|
||||||
this.onQueryEventType = this.onQueryEventType.bind(this);
|
|
||||||
this.onQueryStateKey = this.onQueryStateKey.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
eventType: null,
|
eventType: null,
|
||||||
event: null,
|
event: null,
|
||||||
|
@ -368,19 +420,19 @@ class RoomStateExplorer extends React.PureComponent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
browseEventType(eventType) {
|
private browseEventType(eventType: string) {
|
||||||
return () => {
|
return () => {
|
||||||
this.setState({ eventType });
|
this.setState({ eventType });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onViewSourceClick(event) {
|
private onViewSourceClick(event: MatrixEvent) {
|
||||||
return () => {
|
return () => {
|
||||||
this.setState({ event });
|
this.setState({ event });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack() {
|
private onBack = () => {
|
||||||
if (this.state.editing) {
|
if (this.state.editing) {
|
||||||
this.setState({ editing: false });
|
this.setState({ editing: false });
|
||||||
} else if (this.state.event) {
|
} else if (this.state.event) {
|
||||||
|
@ -392,15 +444,15 @@ class RoomStateExplorer extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
editEv() {
|
private editEv = () => {
|
||||||
this.setState({ editing: true });
|
this.setState({ editing: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onQueryEventType(filterEventType) {
|
private onQueryEventType = (filterEventType: string) => {
|
||||||
this.setState({ queryEventType: filterEventType });
|
this.setState({ queryEventType: filterEventType });
|
||||||
}
|
}
|
||||||
|
|
||||||
onQueryStateKey(filterStateKey) {
|
private onQueryStateKey = (filterStateKey: string) => {
|
||||||
this.setState({ queryStateKey: filterStateKey });
|
this.setState({ queryStateKey: filterStateKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -472,24 +524,22 @@ class RoomStateExplorer extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountDataExplorer extends React.PureComponent {
|
interface IAccountDataExplorerState {
|
||||||
static getLabel() { return _t('Explore Account Data'); }
|
isRoomAccountData: boolean;
|
||||||
|
event?: MatrixEvent;
|
||||||
|
editing: boolean;
|
||||||
|
queryEventType: string;
|
||||||
|
[inputId: string]: boolean | string;
|
||||||
|
}
|
||||||
|
|
||||||
static propTypes = {
|
class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDataExplorerState> {
|
||||||
onBack: PropTypes.func.isRequired,
|
static getLabel() { return _t('Explore Account Data'); }
|
||||||
room: PropTypes.instanceOf(Room).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.onBack = this.onBack.bind(this);
|
|
||||||
this.editEv = this.editEv.bind(this);
|
|
||||||
this._onChange = this._onChange.bind(this);
|
|
||||||
this.onQueryEventType = this.onQueryEventType.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isRoomAccountData: false,
|
isRoomAccountData: false,
|
||||||
event: null,
|
event: null,
|
||||||
|
@ -499,20 +549,20 @@ class AccountDataExplorer extends React.PureComponent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getData() {
|
private getData(): Record<string, MatrixEvent> {
|
||||||
if (this.state.isRoomAccountData) {
|
if (this.state.isRoomAccountData) {
|
||||||
return this.props.room.accountData;
|
return this.props.room.accountData;
|
||||||
}
|
}
|
||||||
return this.context.store.accountData;
|
return this.context.store.accountData;
|
||||||
}
|
}
|
||||||
|
|
||||||
onViewSourceClick(event) {
|
private onViewSourceClick(event: MatrixEvent) {
|
||||||
return () => {
|
return () => {
|
||||||
this.setState({ event });
|
this.setState({ event });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack() {
|
private onBack = () => {
|
||||||
if (this.state.editing) {
|
if (this.state.editing) {
|
||||||
this.setState({ editing: false });
|
this.setState({ editing: false });
|
||||||
} else if (this.state.event) {
|
} else if (this.state.event) {
|
||||||
|
@ -522,15 +572,15 @@ class AccountDataExplorer extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onChange(e) {
|
private onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
||||||
}
|
}
|
||||||
|
|
||||||
editEv() {
|
private editEv = () => {
|
||||||
this.setState({ editing: true });
|
this.setState({ editing: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onQueryEventType(queryEventType) {
|
private onQueryEventType = (queryEventType: string) => {
|
||||||
this.setState({ queryEventType });
|
this.setState({ queryEventType });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -580,30 +630,39 @@ class AccountDataExplorer extends React.PureComponent {
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||||
{ !this.state.message && <div style={{float: "right"}}>
|
<div style={{float: "right"}}>
|
||||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} />
|
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
|
type="checkbox"
|
||||||
</div> }
|
checked={this.state.isRoomAccountData}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
<label className="mx_DevTools_tgl-btn"
|
||||||
|
data-tg-off="Account Data"
|
||||||
|
data-tg-on="Room Data"
|
||||||
|
htmlFor="isRoomAccountData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServersInRoomList extends React.PureComponent {
|
interface IServersInRoomListState {
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServersInRoomList extends React.PureComponent<IExplorerProps, IServersInRoomListState> {
|
||||||
static getLabel() { return _t('View Servers in Room'); }
|
static getLabel() { return _t('View Servers in Room'); }
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
onBack: PropTypes.func.isRequired,
|
|
||||||
room: PropTypes.instanceOf(Room).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
|
private servers: React.ReactElement[];
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const room = this.props.room;
|
const room = this.props.room;
|
||||||
const servers = new Set();
|
const servers = new Set<string>();
|
||||||
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
|
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
|
||||||
this.servers = Array.from(servers).map(s =>
|
this.servers = Array.from(servers).map(s =>
|
||||||
<button key={s} className="mx_DevTools_ServersInRoomList_button">
|
<button key={s} className="mx_DevTools_ServersInRoomList_button">
|
||||||
|
@ -615,7 +674,7 @@ class ServersInRoomList extends React.PureComponent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onQuery = (query) => {
|
private onQuery = (query: string) => {
|
||||||
this.setState({ query });
|
this.setState({ query });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -642,7 +701,10 @@ const PHASE_MAP = {
|
||||||
[PHASE_CANCELLED]: "cancelled",
|
[PHASE_CANCELLED]: "cancelled",
|
||||||
};
|
};
|
||||||
|
|
||||||
function VerificationRequest({txnId, request}) {
|
const VerificationRequestExplorer: React.FC<{
|
||||||
|
txnId: string;
|
||||||
|
request: VerificationRequest;
|
||||||
|
}> = ({txnId, request}) => {
|
||||||
const [, updateState] = useState();
|
const [, updateState] = useState();
|
||||||
const [timeout, setRequestTimeout] = useState(request.timeout);
|
const [timeout, setRequestTimeout] = useState(request.timeout);
|
||||||
|
|
||||||
|
@ -679,7 +741,7 @@ function VerificationRequest({txnId, request}) {
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
class VerificationExplorer extends React.Component {
|
class VerificationExplorer extends React.PureComponent<IExplorerProps> {
|
||||||
static getLabel() {
|
static getLabel() {
|
||||||
return _t("Verification Requests");
|
return _t("Verification Requests");
|
||||||
}
|
}
|
||||||
|
@ -687,7 +749,7 @@ class VerificationExplorer extends React.Component {
|
||||||
/* Ensure this.context is the cli */
|
/* Ensure this.context is the cli */
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
onNewRequest = () => {
|
private onNewRequest = () => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -704,13 +766,13 @@ class VerificationExplorer extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const cli = this.context;
|
const cli = this.context;
|
||||||
const room = this.props.room;
|
const room = this.props.room;
|
||||||
const inRoomChannel = cli._crypto._inRoomVerificationRequests;
|
const inRoomChannel = cli.crypto._inRoomVerificationRequests;
|
||||||
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
|
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
|
||||||
|
|
||||||
return (<div>
|
return (<div>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
|
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
|
||||||
<VerificationRequest txnId={txnId} request={request} key={txnId} />,
|
<VerificationRequestExplorer txnId={txnId} request={request} key={txnId} />,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
|
@ -720,7 +782,12 @@ class VerificationExplorer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WidgetExplorer extends React.Component {
|
interface IWidgetExplorerState {
|
||||||
|
query: string;
|
||||||
|
editWidget?: IApp;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WidgetExplorer extends React.Component<IExplorerProps, IWidgetExplorerState> {
|
||||||
static getLabel() {
|
static getLabel() {
|
||||||
return _t("Active Widgets");
|
return _t("Active Widgets");
|
||||||
}
|
}
|
||||||
|
@ -734,19 +801,19 @@ class WidgetExplorer extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onWidgetStoreUpdate = () => {
|
private onWidgetStoreUpdate = () => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
onQueryChange = (query) => {
|
private onQueryChange = (query: string) => {
|
||||||
this.setState({query});
|
this.setState({query});
|
||||||
};
|
};
|
||||||
|
|
||||||
onEditWidget = (widget) => {
|
private onEditWidget = (widget: IApp) => {
|
||||||
this.setState({editWidget: widget});
|
this.setState({editWidget: widget});
|
||||||
};
|
};
|
||||||
|
|
||||||
onBack = () => {
|
private onBack = () => {
|
||||||
const widgets = WidgetStore.instance.getApps(this.props.room.roomId);
|
const widgets = WidgetStore.instance.getApps(this.props.room.roomId);
|
||||||
if (this.state.editWidget && widgets.includes(this.state.editWidget)) {
|
if (this.state.editWidget && widgets.includes(this.state.editWidget)) {
|
||||||
this.setState({editWidget: null});
|
this.setState({editWidget: null});
|
||||||
|
@ -769,8 +836,11 @@ class WidgetExplorer extends React.Component {
|
||||||
const editWidget = this.state.editWidget;
|
const editWidget = this.state.editWidget;
|
||||||
const widgets = WidgetStore.instance.getApps(room.roomId);
|
const widgets = WidgetStore.instance.getApps(room.roomId);
|
||||||
if (editWidget && widgets.includes(editWidget)) {
|
if (editWidget && widgets.includes(editWidget)) {
|
||||||
const allState = Array.from(Array.from(room.currentState.events.values()).map(e => e.values()))
|
const allState = Array.from(
|
||||||
.reduce((p, c) => {p.push(...c); return p;}, []);
|
Array.from(room.currentState.events.values()).map((e: Map<string, MatrixEvent>) => {
|
||||||
|
return e.values();
|
||||||
|
}),
|
||||||
|
).reduce((p, c) => { p.push(...c); return p; }, []);
|
||||||
const stateEv = allState.find(ev => ev.getId() === editWidget.eventId);
|
const stateEv = allState.find(ev => ev.getId() === editWidget.eventId);
|
||||||
if (!stateEv) { // "should never happen"
|
if (!stateEv) { // "should never happen"
|
||||||
return <div>
|
return <div>
|
||||||
|
@ -811,7 +881,15 @@ class WidgetExplorer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsExplorer extends React.Component {
|
interface ISettingsExplorerState {
|
||||||
|
query: string;
|
||||||
|
editSetting?: string;
|
||||||
|
viewSetting?: string;
|
||||||
|
explicitValues?: string;
|
||||||
|
explicitRoomValues?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExplorerState> {
|
||||||
static getLabel() {
|
static getLabel() {
|
||||||
return _t("Settings Explorer");
|
return _t("Settings Explorer");
|
||||||
}
|
}
|
||||||
|
@ -829,19 +907,19 @@ class SettingsExplorer extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onQueryChange = (ev) => {
|
private onQueryChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({query: ev.target.value});
|
this.setState({query: ev.target.value});
|
||||||
};
|
};
|
||||||
|
|
||||||
onExplValuesEdit = (ev) => {
|
private onExplValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
this.setState({explicitValues: ev.target.value});
|
this.setState({explicitValues: ev.target.value});
|
||||||
};
|
};
|
||||||
|
|
||||||
onExplRoomValuesEdit = (ev) => {
|
private onExplRoomValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
this.setState({explicitRoomValues: ev.target.value});
|
this.setState({explicitRoomValues: ev.target.value});
|
||||||
};
|
};
|
||||||
|
|
||||||
onBack = () => {
|
private onBack = () => {
|
||||||
if (this.state.editSetting) {
|
if (this.state.editSetting) {
|
||||||
this.setState({editSetting: null});
|
this.setState({editSetting: null});
|
||||||
} else if (this.state.viewSetting) {
|
} else if (this.state.viewSetting) {
|
||||||
|
@ -851,12 +929,12 @@ class SettingsExplorer extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onViewClick = (ev, settingId) => {
|
private onViewClick = (ev: MouseEvent, settingId: string) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.setState({viewSetting: settingId});
|
this.setState({viewSetting: settingId});
|
||||||
};
|
};
|
||||||
|
|
||||||
onEditClick = (ev, settingId) => {
|
private onEditClick = (ev: MouseEvent, settingId: string) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.setState({
|
this.setState({
|
||||||
editSetting: settingId,
|
editSetting: settingId,
|
||||||
|
@ -865,7 +943,7 @@ class SettingsExplorer extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSaveClick = async () => {
|
private onSaveClick = async () => {
|
||||||
try {
|
try {
|
||||||
const settingId = this.state.editSetting;
|
const settingId = this.state.editSetting;
|
||||||
const parsedExplicit = JSON.parse(this.state.explicitValues);
|
const parsedExplicit = JSON.parse(this.state.explicitValues);
|
||||||
|
@ -874,7 +952,7 @@ class SettingsExplorer extends React.Component {
|
||||||
console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
|
console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
|
||||||
try {
|
try {
|
||||||
const val = parsedExplicit[level];
|
const val = parsedExplicit[level];
|
||||||
await SettingsStore.setValue(settingId, null, level, val);
|
await SettingsStore.setValue(settingId, null, level as SettingLevel, val);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
|
@ -884,7 +962,7 @@ class SettingsExplorer extends React.Component {
|
||||||
console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
|
console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
|
||||||
try {
|
try {
|
||||||
const val = parsedExplicitRoom[level];
|
const val = parsedExplicitRoom[level];
|
||||||
await SettingsStore.setValue(settingId, roomId, level, val);
|
await SettingsStore.setValue(settingId, roomId, level as SettingLevel, val);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
|
@ -901,7 +979,7 @@ class SettingsExplorer extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderSettingValue(val) {
|
private renderSettingValue(val: any): string {
|
||||||
// Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us
|
// Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us
|
||||||
const toStringTypes = ['boolean', 'number'];
|
const toStringTypes = ['boolean', 'number'];
|
||||||
if (toStringTypes.includes(typeof(val))) {
|
if (toStringTypes.includes(typeof(val))) {
|
||||||
|
@ -911,7 +989,7 @@ class SettingsExplorer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderExplicitSettingValues(setting, roomId) {
|
private renderExplicitSettingValues(setting: string, roomId: string): string {
|
||||||
const vals = {};
|
const vals = {};
|
||||||
for (const level of LEVEL_ORDER) {
|
for (const level of LEVEL_ORDER) {
|
||||||
try {
|
try {
|
||||||
|
@ -926,7 +1004,7 @@ class SettingsExplorer extends React.Component {
|
||||||
return JSON.stringify(vals, null, 4);
|
return JSON.stringify(vals, null, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCanEditLevel(roomId, level) {
|
private renderCanEditLevel(roomId: string, level: SettingLevel): React.ReactNode {
|
||||||
const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
|
const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
|
||||||
const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
|
const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
|
||||||
return <td className={className}><code>{canEdit.toString()}</code></td>;
|
return <td className={className}><code>{canEdit.toString()}</code></td>;
|
||||||
|
@ -1062,27 +1140,37 @@ class SettingsExplorer extends React.Component {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{_t("Value:")}
|
{_t("Value:")}
|
||||||
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting))}</code>
|
<code>{this.renderSettingValue(
|
||||||
|
SettingsStore.getValue(this.state.viewSetting),
|
||||||
|
)}</code>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{_t("Value in this room:")}
|
{_t("Value in this room:")}
|
||||||
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting, room.roomId))}</code>
|
<code>{this.renderSettingValue(
|
||||||
|
SettingsStore.getValue(this.state.viewSetting, room.roomId),
|
||||||
|
)}</code>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{_t("Values at explicit levels:")}
|
{_t("Values at explicit levels:")}
|
||||||
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, null)}</code></pre>
|
<pre><code>{this.renderExplicitSettingValues(
|
||||||
|
this.state.viewSetting, null,
|
||||||
|
)}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{_t("Values at explicit levels in this room:")}
|
{_t("Values at explicit levels in this room:")}
|
||||||
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, room.roomId)}</code></pre>
|
<pre><code>{this.renderExplicitSettingValues(
|
||||||
|
this.state.viewSetting, room.roomId,
|
||||||
|
)}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{_t("Edit Values")}</button>
|
<button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{
|
||||||
|
_t("Edit Values")
|
||||||
|
}</button>
|
||||||
<button onClick={this.onBack}>{_t("Back")}</button>
|
<button onClick={this.onBack}>{_t("Back")}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1091,7 +1179,11 @@ class SettingsExplorer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Entries = [
|
type DevtoolsDialogEntry = React.JSXElementConstructor<any> & {
|
||||||
|
getLabel: () => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Entries: DevtoolsDialogEntry[] = [
|
||||||
SendCustomEvent,
|
SendCustomEvent,
|
||||||
RoomStateExplorer,
|
RoomStateExplorer,
|
||||||
SendAccountData,
|
SendAccountData,
|
||||||
|
@ -1102,43 +1194,36 @@ const Entries = [
|
||||||
SettingsExplorer,
|
SettingsExplorer,
|
||||||
];
|
];
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.DevtoolsDialog")
|
interface IProps {
|
||||||
export default class DevtoolsDialog extends React.PureComponent {
|
roomId: string;
|
||||||
static propTypes = {
|
onFinished: (finished: boolean) => void;
|
||||||
roomId: PropTypes.string.isRequired,
|
}
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
mode?: DevtoolsDialogEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.dialogs.DevtoolsDialog")
|
||||||
|
export default class DevtoolsDialog extends React.PureComponent<IProps, IState> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.onBack = this.onBack.bind(this);
|
|
||||||
this.onCancel = this.onCancel.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
mode: null,
|
mode: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
private setMode(mode: DevtoolsDialogEntry) {
|
||||||
this._unmounted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_setMode(mode) {
|
|
||||||
return () => {
|
return () => {
|
||||||
this.setState({ mode });
|
this.setState({ mode });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack() {
|
private onBack = () => {
|
||||||
if (this.prevMode) {
|
this.setState({ mode: null });
|
||||||
this.setState({ mode: this.prevMode });
|
|
||||||
this.prevMode = null;
|
|
||||||
} else {
|
|
||||||
this.setState({ mode: null });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCancel() {
|
private onCancel = () => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1165,7 +1250,7 @@ export default class DevtoolsDialog extends React.PureComponent {
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{ Entries.map((Entry) => {
|
{ Entries.map((Entry) => {
|
||||||
const label = Entry.getLabel();
|
const label = Entry.getLabel();
|
||||||
const onClick = this._setMode(Entry);
|
const onClick = this.setMode(Entry);
|
||||||
return <button className={classes} key={label} onClick={onClick}>{ label }</button>;
|
return <button className={classes} key={label} onClick={onClick}>{ label }</button>;
|
||||||
}) }
|
}) }
|
||||||
</div>
|
</div>
|
247
src/components/views/dialogs/ForwardDialog.tsx
Normal file
247
src/components/views/dialogs/ForwardDialog.tsx
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Robin Townsend <robin@robin.town>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT 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, {useMemo, useState, useEffect} from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||||
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
|
import {_t} from "../../../languageHandler";
|
||||||
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings";
|
||||||
|
import {UIFeature} from "../../../settings/UIFeature";
|
||||||
|
import {Layout} from "../../../settings/Layout";
|
||||||
|
import {IDialogProps} from "./IDialogProps";
|
||||||
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import {avatarUrlForUser} from "../../../Avatar";
|
||||||
|
import EventTile from "../rooms/EventTile";
|
||||||
|
import SearchBox from "../../structures/SearchBox";
|
||||||
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
|
import {Alignment} from '../elements/Tooltip';
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
|
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
|
||||||
|
import NotificationBadge from "../rooms/NotificationBadge";
|
||||||
|
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||||
|
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||||
|
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||||
|
|
||||||
|
const AVATAR_SIZE = 30;
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
matrixClient: MatrixClient;
|
||||||
|
// The event to forward
|
||||||
|
event: MatrixEvent;
|
||||||
|
// We need a permalink creator for the source room to pass through to EventTile
|
||||||
|
// in case the event is a reply (even though the user can't get at the link)
|
||||||
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IEntryProps {
|
||||||
|
room: Room;
|
||||||
|
event: MatrixEvent;
|
||||||
|
matrixClient: MatrixClient;
|
||||||
|
onFinished(success: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SendState {
|
||||||
|
CanSend,
|
||||||
|
Sending,
|
||||||
|
Sent,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinished }) => {
|
||||||
|
const [sendState, setSendState] = useState<SendState>(SendState.CanSend);
|
||||||
|
|
||||||
|
const jumpToRoom = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "view_room",
|
||||||
|
room_id: room.roomId,
|
||||||
|
});
|
||||||
|
onFinished(true);
|
||||||
|
};
|
||||||
|
const send = async () => {
|
||||||
|
setSendState(SendState.Sending);
|
||||||
|
try {
|
||||||
|
await cli.sendEvent(room.roomId, event.getType(), event.getContent());
|
||||||
|
setSendState(SendState.Sent);
|
||||||
|
} catch (e) {
|
||||||
|
setSendState(SendState.Failed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let className;
|
||||||
|
let disabled = false;
|
||||||
|
let title;
|
||||||
|
let icon;
|
||||||
|
if (sendState === SendState.CanSend) {
|
||||||
|
className = "mx_ForwardList_canSend";
|
||||||
|
if (room.maySendMessage()) {
|
||||||
|
title = _t("Send");
|
||||||
|
} else {
|
||||||
|
disabled = true;
|
||||||
|
title = _t("You don't have permission to do this");
|
||||||
|
}
|
||||||
|
} else if (sendState === SendState.Sending) {
|
||||||
|
className = "mx_ForwardList_sending";
|
||||||
|
disabled = true;
|
||||||
|
title = _t("Sending");
|
||||||
|
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
|
||||||
|
} else if (sendState === SendState.Sent) {
|
||||||
|
className = "mx_ForwardList_sent";
|
||||||
|
disabled = true;
|
||||||
|
title = _t("Sent");
|
||||||
|
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
|
||||||
|
} else {
|
||||||
|
className = "mx_ForwardList_sendFailed";
|
||||||
|
disabled = true;
|
||||||
|
title = _t("Failed to send");
|
||||||
|
icon = <NotificationBadge
|
||||||
|
notification={StaticNotificationState.RED_EXCLAMATION}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_ForwardList_entry">
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_ForwardList_roomButton"
|
||||||
|
onClick={jumpToRoom}
|
||||||
|
title={_t("Open link")}
|
||||||
|
yOffset={-20}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
>
|
||||||
|
<DecoratedRoomAvatar room={room} avatarSize={32} />
|
||||||
|
<span className="mx_ForwardList_entry_name">{ room.name }</span>
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
|
||||||
|
className={`mx_ForwardList_sendButton ${className}`}
|
||||||
|
onClick={send}
|
||||||
|
disabled={disabled}
|
||||||
|
title={title}
|
||||||
|
yOffset={-20}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
>
|
||||||
|
<div className="mx_ForwardList_sendLabel">{ _t("Send") }</div>
|
||||||
|
{ icon }
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => {
|
||||||
|
const userId = cli.getUserId();
|
||||||
|
const [profileInfo, setProfileInfo] = useState<any>({});
|
||||||
|
useEffect(() => {
|
||||||
|
cli.getProfileInfo(userId).then(info => setProfileInfo(info));
|
||||||
|
}, [cli, userId]);
|
||||||
|
|
||||||
|
// For the message preview we fake the sender as ourselves
|
||||||
|
const mockEvent = new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: userId,
|
||||||
|
content: event.getContent(),
|
||||||
|
unsigned: {
|
||||||
|
age: 97,
|
||||||
|
},
|
||||||
|
event_id: "$9999999999999999999999999999999999999999999",
|
||||||
|
room_id: event.getRoomId(),
|
||||||
|
});
|
||||||
|
mockEvent.sender = {
|
||||||
|
name: profileInfo.displayname || userId,
|
||||||
|
userId,
|
||||||
|
getAvatarUrl: (..._) => {
|
||||||
|
return avatarUrlForUser(
|
||||||
|
{ avatarUrl: profileInfo.avatar_url },
|
||||||
|
AVATAR_SIZE, AVATAR_SIZE, "crop",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getMxcAvatarUrl: () => profileInfo.avatar_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const lcQuery = query.toLowerCase();
|
||||||
|
|
||||||
|
const spacesEnabled = useFeatureEnabled("feature_spaces");
|
||||||
|
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
|
||||||
|
const previewLayout = useSettingValue<Layout>("layout");
|
||||||
|
|
||||||
|
let rooms = useMemo(() => sortRooms(
|
||||||
|
cli.getVisibleRooms().filter(
|
||||||
|
room => room.getMyMembership() === "join" &&
|
||||||
|
!(spacesEnabled && room.isSpaceRoom()),
|
||||||
|
),
|
||||||
|
), [cli, spacesEnabled]);
|
||||||
|
|
||||||
|
if (lcQuery) {
|
||||||
|
rooms = new QueryMatcher<Room>(rooms, {
|
||||||
|
keys: ["name"],
|
||||||
|
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
|
||||||
|
shouldMatchWordsOnly: false,
|
||||||
|
}).match(lcQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BaseDialog
|
||||||
|
title={_t("Forward message")}
|
||||||
|
className="mx_ForwardDialog"
|
||||||
|
contentId="mx_ForwardList"
|
||||||
|
onFinished={onFinished}
|
||||||
|
fixedWidth={false}
|
||||||
|
>
|
||||||
|
<h3>{ _t("Message preview") }</h3>
|
||||||
|
<div className={classnames("mx_ForwardDialog_preview", {
|
||||||
|
"mx_IRCLayout": previewLayout == Layout.IRC,
|
||||||
|
"mx_GroupLayout": previewLayout == Layout.Group,
|
||||||
|
})}>
|
||||||
|
<EventTile
|
||||||
|
mxEvent={mockEvent}
|
||||||
|
layout={previewLayout}
|
||||||
|
enableFlair={flairEnabled}
|
||||||
|
permalinkCreator={permalinkCreator}
|
||||||
|
as="div"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div className="mx_ForwardList" id="mx_ForwardList">
|
||||||
|
<SearchBox
|
||||||
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
|
placeholder={_t("Search for rooms or people")}
|
||||||
|
onSearch={setQuery}
|
||||||
|
autoComplete={true}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
<AutoHideScrollbar className="mx_ForwardList_content">
|
||||||
|
{ rooms.length > 0 ? (
|
||||||
|
<div className="mx_ForwardList_results">
|
||||||
|
{ rooms.map(room =>
|
||||||
|
<Entry
|
||||||
|
key={room.roomId}
|
||||||
|
room={room}
|
||||||
|
event={event}
|
||||||
|
matrixClient={cli}
|
||||||
|
onFinished={onFinished}
|
||||||
|
/>,
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
) : <span className="mx_ForwardList_noResults">
|
||||||
|
{ _t("No results") }
|
||||||
|
</span> }
|
||||||
|
</AutoHideScrollbar>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ForwardDialog;
|
|
@ -15,5 +15,5 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface IDialogProps {
|
export interface IDialogProps {
|
||||||
onFinished: (bool) => void;
|
onFinished(...args: any): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {createRef} from 'react';
|
import React, { createRef } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import {_t, _td} from "../../../languageHandler";
|
import {_t, _td} from "../../../languageHandler";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
|
@ -31,7 +33,6 @@ import Modal from "../../../Modal";
|
||||||
import {humanizeTime} from "../../../utils/humanize";
|
import {humanizeTime} from "../../../utils/humanize";
|
||||||
import createRoom, {
|
import createRoom, {
|
||||||
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
||||||
IInvite3PID,
|
|
||||||
} from "../../../createRoom";
|
} from "../../../createRoom";
|
||||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||||
import {Key} from "../../../Keyboard";
|
import {Key} from "../../../Keyboard";
|
||||||
|
@ -47,10 +48,25 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
import {getAddressType} from "../../../UserAddress";
|
import {getAddressType} from "../../../UserAddress";
|
||||||
|
import BaseAvatar from '../avatars/BaseAvatar';
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import { compare } from '../../../utils/strings';
|
||||||
|
import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
import { copyPlaintext, selectText } from "../../../utils/strings";
|
||||||
|
import * as ContextMenu from "../../structures/ContextMenu";
|
||||||
|
import { toRightOf } from "../../structures/ContextMenu";
|
||||||
|
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
||||||
|
|
||||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
interface IRecentUser {
|
||||||
|
userId: string,
|
||||||
|
user: RoomMember,
|
||||||
|
lastActive: number,
|
||||||
|
}
|
||||||
|
|
||||||
export const KIND_DM = "dm";
|
export const KIND_DM = "dm";
|
||||||
export const KIND_INVITE = "invite";
|
export const KIND_INVITE = "invite";
|
||||||
export const KIND_CALL_TRANSFER = "call_transfer";
|
export const KIND_CALL_TRANSFER = "call_transfer";
|
||||||
|
@ -61,43 +77,41 @@ const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is c
|
||||||
// This is the interface that is expected by various components in this file. It is a bit
|
// This is the interface that is expected by various components in this file. It is a bit
|
||||||
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||||
// for 3PIDs/email addresses.
|
// for 3PIDs/email addresses.
|
||||||
//
|
abstract class Member {
|
||||||
// XXX: We should use TypeScript interfaces instead of this weird "abstract" class.
|
|
||||||
class Member {
|
|
||||||
/**
|
/**
|
||||||
* The display name of this Member. For users this should be their profile's display
|
* The display name of this Member. For users this should be their profile's display
|
||||||
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
||||||
*/
|
*/
|
||||||
get name(): string { throw new Error("Member class not implemented"); }
|
public abstract get name(): string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of this Member. For users this should be their user ID. For 3PIDs this should
|
* The ID of this Member. For users this should be their user ID. For 3PIDs this should
|
||||||
* be the 3PID address (email).
|
* be the 3PID address (email).
|
||||||
*/
|
*/
|
||||||
get userId(): string { throw new Error("Member class not implemented"); }
|
public abstract get userId(): string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the MXC URL of this Member's avatar. For users this should be their profile's
|
* Gets the MXC URL of this Member's avatar. For users this should be their profile's
|
||||||
* avatar MXC URL or null if none set. For 3PIDs this should always be null.
|
* avatar MXC URL or null if none set. For 3PIDs this should always be null.
|
||||||
*/
|
*/
|
||||||
getMxcAvatarUrl(): string { throw new Error("Member class not implemented"); }
|
public abstract getMxcAvatarUrl(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DirectoryMember extends Member {
|
class DirectoryMember extends Member {
|
||||||
_userId: string;
|
private readonly _userId: string;
|
||||||
_displayName: string;
|
private readonly displayName: string;
|
||||||
_avatarUrl: string;
|
private readonly avatarUrl: string;
|
||||||
|
|
||||||
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
||||||
super();
|
super();
|
||||||
this._userId = userDirResult.user_id;
|
this._userId = userDirResult.user_id;
|
||||||
this._displayName = userDirResult.display_name;
|
this.displayName = userDirResult.display_name;
|
||||||
this._avatarUrl = userDirResult.avatar_url;
|
this.avatarUrl = userDirResult.avatar_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// These next class members are for the Member interface
|
// These next class members are for the Member interface
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return this._displayName || this._userId;
|
return this.displayName || this._userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
get userId(): string {
|
get userId(): string {
|
||||||
|
@ -105,32 +119,32 @@ class DirectoryMember extends Member {
|
||||||
}
|
}
|
||||||
|
|
||||||
getMxcAvatarUrl(): string {
|
getMxcAvatarUrl(): string {
|
||||||
return this._avatarUrl;
|
return this.avatarUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThreepidMember extends Member {
|
class ThreepidMember extends Member {
|
||||||
_id: string;
|
private readonly id: string;
|
||||||
|
|
||||||
constructor(id: string) {
|
constructor(id: string) {
|
||||||
super();
|
super();
|
||||||
this._id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a getter that would be falsey on all other implementations. Until we have
|
// This is a getter that would be falsey on all other implementations. Until we have
|
||||||
// better type support in the react-sdk we can use this trick to determine the kind
|
// better type support in the react-sdk we can use this trick to determine the kind
|
||||||
// of 3PID we're dealing with, if any.
|
// of 3PID we're dealing with, if any.
|
||||||
get isEmail(): boolean {
|
get isEmail(): boolean {
|
||||||
return this._id.includes('@');
|
return this.id.includes('@');
|
||||||
}
|
}
|
||||||
|
|
||||||
// These next class members are for the Member interface
|
// These next class members are for the Member interface
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return this._id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get userId(): string {
|
get userId(): string {
|
||||||
return this._id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMxcAvatarUrl(): string {
|
getMxcAvatarUrl(): string {
|
||||||
|
@ -140,11 +154,11 @@ class ThreepidMember extends Member {
|
||||||
|
|
||||||
interface IDMUserTileProps {
|
interface IDMUserTileProps {
|
||||||
member: RoomMember;
|
member: RoomMember;
|
||||||
onRemove: (RoomMember) => any;
|
onRemove(member: RoomMember): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||||
_onRemove = (e) => {
|
private onRemove = (e) => {
|
||||||
// Stop the browser from highlighting text
|
// Stop the browser from highlighting text
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -153,9 +167,6 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
|
||||||
|
|
||||||
const avatarSize = 20;
|
const avatarSize = 20;
|
||||||
const avatar = this.props.member.isEmail
|
const avatar = this.props.member.isEmail
|
||||||
? <img
|
? <img
|
||||||
|
@ -177,7 +188,7 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||||
closeButton = (
|
closeButton = (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className='mx_InviteDialog_userTile_remove'
|
className='mx_InviteDialog_userTile_remove'
|
||||||
onClick={this._onRemove}
|
onClick={this.onRemove}
|
||||||
>
|
>
|
||||||
<img src={require("../../../../res/img/icon-pill-remove.svg")}
|
<img src={require("../../../../res/img/icon-pill-remove.svg")}
|
||||||
alt={_t('Remove')} width={8} height={8}
|
alt={_t('Remove')} width={8} height={8}
|
||||||
|
@ -201,13 +212,13 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||||
interface IDMRoomTileProps {
|
interface IDMRoomTileProps {
|
||||||
member: RoomMember;
|
member: RoomMember;
|
||||||
lastActiveTs: number;
|
lastActiveTs: number;
|
||||||
onToggle: (RoomMember) => any;
|
onToggle(member: RoomMember): void;
|
||||||
highlightWord: string;
|
highlightWord: string;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||||
_onClick = (e) => {
|
private onClick = (e) => {
|
||||||
// Stop the browser from highlighting text
|
// Stop the browser from highlighting text
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -215,7 +226,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||||
this.props.onToggle(this.props.member);
|
this.props.onToggle(this.props.member);
|
||||||
};
|
};
|
||||||
|
|
||||||
_highlightName(str: string) {
|
private highlightName(str: string) {
|
||||||
if (!this.props.highlightWord) return str;
|
if (!this.props.highlightWord) return str;
|
||||||
|
|
||||||
// We convert things to lowercase for index searching, but pull substrings from
|
// We convert things to lowercase for index searching, but pull substrings from
|
||||||
|
@ -252,8 +263,6 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
|
||||||
|
|
||||||
let timestamp = null;
|
let timestamp = null;
|
||||||
if (this.props.lastActiveTs) {
|
if (this.props.lastActiveTs) {
|
||||||
const humanTs = humanizeTime(this.props.lastActiveTs);
|
const humanTs = humanizeTime(this.props.lastActiveTs);
|
||||||
|
@ -291,13 +300,13 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||||
|
|
||||||
const caption = this.props.member.isEmail
|
const caption = this.props.member.isEmail
|
||||||
? _t("Invite by email")
|
? _t("Invite by email")
|
||||||
: this._highlightName(this.props.member.userId);
|
: this.highlightName(this.props.member.userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mx_InviteDialog_roomTile' onClick={this._onClick}>
|
<div className='mx_InviteDialog_roomTile' onClick={this.onClick}>
|
||||||
{stackedAvatar}
|
{stackedAvatar}
|
||||||
<span className="mx_InviteDialog_roomTile_nameStack">
|
<span className="mx_InviteDialog_roomTile_nameStack">
|
||||||
<div className='mx_InviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</div>
|
<div className='mx_InviteDialog_roomTile_name'>{this.highlightName(this.props.member.name)}</div>
|
||||||
<div className='mx_InviteDialog_roomTile_userId'>{caption}</div>
|
<div className='mx_InviteDialog_roomTile_userId'>{caption}</div>
|
||||||
</span>
|
</span>
|
||||||
{timestamp}
|
{timestamp}
|
||||||
|
@ -308,7 +317,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||||
|
|
||||||
interface IInviteDialogProps {
|
interface IInviteDialogProps {
|
||||||
// Takes an array of user IDs/emails to invite.
|
// Takes an array of user IDs/emails to invite.
|
||||||
onFinished: (toInvite?: string[]) => any;
|
onFinished: (toInvite?: string[]) => void;
|
||||||
|
|
||||||
// The kind of invite being performed. Assumed to be KIND_DM if
|
// The kind of invite being performed. Assumed to be KIND_DM if
|
||||||
// not provided.
|
// not provided.
|
||||||
|
@ -349,8 +358,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
initialText: "",
|
initialText: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
_debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
private closeCopiedTooltip: () => void;
|
||||||
_editorRef: any = null;
|
private debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
||||||
|
private editorRef = createRef<HTMLInputElement>();
|
||||||
|
private unmounted = false;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -378,7 +389,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
filterText: this.props.initialText,
|
filterText: this.props.initialText,
|
||||||
recents: InviteDialog.buildRecents(alreadyInvited),
|
recents: InviteDialog.buildRecents(alreadyInvited),
|
||||||
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||||
suggestions: this._buildSuggestions(alreadyInvited),
|
suggestions: this.buildSuggestions(alreadyInvited),
|
||||||
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
||||||
serverResultsMixin: [],
|
serverResultsMixin: [],
|
||||||
threepidResultsMixin: [],
|
threepidResultsMixin: [],
|
||||||
|
@ -390,21 +401,26 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
busy: false,
|
busy: false,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._editorRef = createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.initialText) {
|
if (this.props.initialText) {
|
||||||
this._updateSuggestions(this.props.initialText);
|
this.updateSuggestions(this.props.initialText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unmounted = true;
|
||||||
|
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
|
||||||
|
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
|
||||||
|
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
private onConsultFirstChange = (ev) => {
|
private onConsultFirstChange = (ev) => {
|
||||||
this.setState({consultFirst: ev.target.checked});
|
this.setState({consultFirst: ev.target.checked});
|
||||||
}
|
}
|
||||||
|
|
||||||
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] {
|
public static buildRecents(excludedTargetIds: Set<string>): IRecentUser[] {
|
||||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||||
|
|
||||||
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
|
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
|
||||||
|
@ -467,7 +483,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
return recents;
|
return recents;
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember}[] {
|
private buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember}[] {
|
||||||
const maxConsideredMembers = 200;
|
const maxConsideredMembers = 200;
|
||||||
const joinedRooms = MatrixClientPeg.get().getRooms()
|
const joinedRooms = MatrixClientPeg.get().getRooms()
|
||||||
.filter(r => r.getMyMembership() === 'join' && r.getJoinedMemberCount() <= maxConsideredMembers);
|
.filter(r => r.getMyMembership() === 'join' && r.getJoinedMemberCount() <= maxConsideredMembers);
|
||||||
|
@ -574,7 +590,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
members.sort((a, b) => {
|
members.sort((a, b) => {
|
||||||
if (a.score === b.score) {
|
if (a.score === b.score) {
|
||||||
if (a.numRooms === b.numRooms) {
|
if (a.numRooms === b.numRooms) {
|
||||||
return a.member.userId.localeCompare(b.member.userId);
|
return compare(a.member.userId, b.member.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.numRooms - a.numRooms;
|
return b.numRooms - a.numRooms;
|
||||||
|
@ -585,7 +601,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
||||||
}
|
}
|
||||||
|
|
||||||
_shouldAbortAfterInviteError(result): boolean {
|
private shouldAbortAfterInviteError(result): boolean {
|
||||||
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
|
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
|
||||||
if (failedUsers.length > 0) {
|
if (failedUsers.length > 0) {
|
||||||
console.log("Failed to invite users: ", result);
|
console.log("Failed to invite users: ", result);
|
||||||
|
@ -600,7 +616,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_convertFilter(): Member[] {
|
private convertFilter(): Member[] {
|
||||||
// Check to see if there's anything to convert first
|
// Check to see if there's anything to convert first
|
||||||
if (!this.state.filterText || !this.state.filterText.includes('@')) return this.state.targets || [];
|
if (!this.state.filterText || !this.state.filterText.includes('@')) return this.state.targets || [];
|
||||||
|
|
||||||
|
@ -617,10 +633,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
return newTargets;
|
return newTargets;
|
||||||
}
|
}
|
||||||
|
|
||||||
_startDm = async () => {
|
private startDm = async () => {
|
||||||
this.setState({busy: true});
|
this.setState({busy: true});
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const targets = this._convertFilter();
|
const targets = this.convertFilter();
|
||||||
const targetIds = targets.map(t => t.userId);
|
const targetIds = targets.map(t => t.userId);
|
||||||
|
|
||||||
// Check if there is already a DM with these people and reuse it if possible.
|
// Check if there is already a DM with these people and reuse it if possible.
|
||||||
|
@ -694,11 +710,11 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_inviteUsers = async () => {
|
private inviteUsers = async () => {
|
||||||
const startTime = CountlyAnalytics.getTimestamp();
|
const startTime = CountlyAnalytics.getTimestamp();
|
||||||
this.setState({busy: true});
|
this.setState({busy: true});
|
||||||
this._convertFilter();
|
this.convertFilter();
|
||||||
const targets = this._convertFilter();
|
const targets = this.convertFilter();
|
||||||
const targetIds = targets.map(t => t.userId);
|
const targetIds = targets.map(t => t.userId);
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
@ -715,7 +731,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
try {
|
try {
|
||||||
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
||||||
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
||||||
if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too
|
if (!this.shouldAbortAfterInviteError(result)) { // handles setting error message too
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -749,9 +765,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_transferCall = async () => {
|
private transferCall = async () => {
|
||||||
this._convertFilter();
|
this.convertFilter();
|
||||||
const targets = this._convertFilter();
|
const targets = this.convertFilter();
|
||||||
const targetIds = targets.map(t => t.userId);
|
const targetIds = targets.map(t => t.userId);
|
||||||
if (targetIds.length > 1) {
|
if (targetIds.length > 1) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -790,26 +806,26 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onKeyDown = (e) => {
|
private onKeyDown = (e) => {
|
||||||
if (this.state.busy) return;
|
if (this.state.busy) return;
|
||||||
const value = e.target.value.trim();
|
const value = e.target.value.trim();
|
||||||
const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey;
|
const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey;
|
||||||
if (!value && this.state.targets.length > 0 && e.key === Key.BACKSPACE && !hasModifiers) {
|
if (!value && this.state.targets.length > 0 && e.key === Key.BACKSPACE && !hasModifiers) {
|
||||||
// when the field is empty and the user hits backspace remove the right-most target
|
// when the field is empty and the user hits backspace remove the right-most target
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this._removeMember(this.state.targets[this.state.targets.length - 1]);
|
this.removeMember(this.state.targets[this.state.targets.length - 1]);
|
||||||
} else if (value && e.key === Key.ENTER && !hasModifiers) {
|
} else if (value && e.key === Key.ENTER && !hasModifiers) {
|
||||||
// when the user hits enter with something in their field try to convert it
|
// when the user hits enter with something in their field try to convert it
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this._convertFilter();
|
this.convertFilter();
|
||||||
} else if (value && e.key === Key.SPACE && !hasModifiers && value.includes("@") && !value.includes(" ")) {
|
} else if (value && e.key === Key.SPACE && !hasModifiers && value.includes("@") && !value.includes(" ")) {
|
||||||
// when the user hits space and their input looks like an e-mail/MXID then try to convert it
|
// when the user hits space and their input looks like an e-mail/MXID then try to convert it
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this._convertFilter();
|
this.convertFilter();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_updateSuggestions = async (term) => {
|
private updateSuggestions = async (term) => {
|
||||||
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
|
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
|
||||||
if (term !== this.state.filterText) {
|
if (term !== this.state.filterText) {
|
||||||
// Discard the results - we were probably too slow on the server-side to make
|
// Discard the results - we were probably too slow on the server-side to make
|
||||||
|
@ -918,30 +934,30 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_updateFilter = (e) => {
|
private updateFilter = (e) => {
|
||||||
const term = e.target.value;
|
const term = e.target.value;
|
||||||
this.setState({filterText: term});
|
this.setState({filterText: term});
|
||||||
|
|
||||||
// Debounce server lookups to reduce spam. We don't clear the existing server
|
// Debounce server lookups to reduce spam. We don't clear the existing server
|
||||||
// results because they might still be vaguely accurate, likewise for races which
|
// results because they might still be vaguely accurate, likewise for races which
|
||||||
// could happen here.
|
// could happen here.
|
||||||
if (this._debounceTimer) {
|
if (this.debounceTimer) {
|
||||||
clearTimeout(this._debounceTimer);
|
clearTimeout(this.debounceTimer);
|
||||||
}
|
}
|
||||||
this._debounceTimer = setTimeout(() => {
|
this.debounceTimer = setTimeout(() => {
|
||||||
this._updateSuggestions(term);
|
this.updateSuggestions(term);
|
||||||
}, 150); // 150ms debounce (human reaction time + some)
|
}, 150); // 150ms debounce (human reaction time + some)
|
||||||
};
|
};
|
||||||
|
|
||||||
_showMoreRecents = () => {
|
private showMoreRecents = () => {
|
||||||
this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN});
|
this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN});
|
||||||
};
|
};
|
||||||
|
|
||||||
_showMoreSuggestions = () => {
|
private showMoreSuggestions = () => {
|
||||||
this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN});
|
this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN});
|
||||||
};
|
};
|
||||||
|
|
||||||
_toggleMember = (member: Member) => {
|
private toggleMember = (member: Member) => {
|
||||||
if (!this.state.busy) {
|
if (!this.state.busy) {
|
||||||
let filterText = this.state.filterText;
|
let filterText = this.state.filterText;
|
||||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||||
|
@ -954,13 +970,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
this.setState({targets, filterText});
|
this.setState({targets, filterText});
|
||||||
|
|
||||||
if (this._editorRef && this._editorRef.current) {
|
if (this.editorRef && this.editorRef.current) {
|
||||||
this._editorRef.current.focus();
|
this.editorRef.current.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_removeMember = (member: Member) => {
|
private removeMember = (member: Member) => {
|
||||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||||
const idx = targets.indexOf(member);
|
const idx = targets.indexOf(member);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
|
@ -968,12 +984,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
this.setState({targets});
|
this.setState({targets});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._editorRef && this._editorRef.current) {
|
if (this.editorRef && this.editorRef.current) {
|
||||||
this._editorRef.current.focus();
|
this.editorRef.current.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPaste = async (e) => {
|
private onPaste = async (e) => {
|
||||||
if (this.state.filterText) {
|
if (this.state.filterText) {
|
||||||
// if the user has already typed something, just let them
|
// if the user has already typed something, just let them
|
||||||
// paste normally.
|
// paste normally.
|
||||||
|
@ -1027,6 +1043,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
failed.push(address);
|
failed.push(address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.unmounted) return;
|
||||||
|
|
||||||
if (failed.length > 0) {
|
if (failed.length > 0) {
|
||||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
|
@ -1043,17 +1060,17 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
this.setState({targets: [...this.state.targets, ...toAdd]});
|
this.setState({targets: [...this.state.targets, ...toAdd]});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onClickInputArea = (e) => {
|
private onClickInputArea = (e) => {
|
||||||
// Stop the browser from highlighting text
|
// Stop the browser from highlighting text
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (this._editorRef && this._editorRef.current) {
|
if (this.editorRef && this.editorRef.current) {
|
||||||
this._editorRef.current.focus();
|
this.editorRef.current.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onUseDefaultIdentityServerClick = (e) => {
|
private onUseDefaultIdentityServerClick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Update the IS in account data. Actually using it may trigger terms.
|
// Update the IS in account data. Actually using it may trigger terms.
|
||||||
|
@ -1062,21 +1079,21 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
this.setState({canUseIdentityServer: true, tryingIdentityServer: false});
|
this.setState({canUseIdentityServer: true, tryingIdentityServer: false});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onManageSettingsClick = (e) => {
|
private onManageSettingsClick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dis.fire(Action.ViewUserSettings);
|
dis.fire(Action.ViewUserSettings);
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCommunityInviteClick = (e) => {
|
private onCommunityInviteClick = (e) => {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderSection(kind: "recents"|"suggestions") {
|
private renderSection(kind: "recents"|"suggestions") {
|
||||||
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
||||||
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
||||||
const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this);
|
const showMoreFn = kind === 'recents' ? this.showMoreRecents.bind(this) : this.showMoreSuggestions.bind(this);
|
||||||
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
||||||
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
||||||
let sectionSubname = null;
|
let sectionSubname = null;
|
||||||
|
@ -1156,7 +1173,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
member={r.user}
|
member={r.user}
|
||||||
lastActiveTs={lastActive(r)}
|
lastActiveTs={lastActive(r)}
|
||||||
key={r.userId}
|
key={r.userId}
|
||||||
onToggle={this._toggleMember}
|
onToggle={this.toggleMember}
|
||||||
highlightWord={this.state.filterText}
|
highlightWord={this.state.filterText}
|
||||||
isSelected={this.state.targets.some(t => t.userId === r.userId)}
|
isSelected={this.state.targets.some(t => t.userId === r.userId)}
|
||||||
/>
|
/>
|
||||||
|
@ -1171,32 +1188,32 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderEditor() {
|
private renderEditor() {
|
||||||
const targets = this.state.targets.map(t => (
|
const targets = this.state.targets.map(t => (
|
||||||
<DMUserTile member={t} onRemove={!this.state.busy && this._removeMember} key={t.userId} />
|
<DMUserTile member={t} onRemove={!this.state.busy && this.removeMember} key={t.userId} />
|
||||||
));
|
));
|
||||||
const input = (
|
const input = (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
onKeyDown={this._onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onChange={this._updateFilter}
|
onChange={this.updateFilter}
|
||||||
value={this.state.filterText}
|
value={this.state.filterText}
|
||||||
ref={this._editorRef}
|
ref={this.editorRef}
|
||||||
onPaste={this._onPaste}
|
onPaste={this.onPaste}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
disabled={this.state.busy}
|
disabled={this.state.busy}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className='mx_InviteDialog_editor' onClick={this._onClickInputArea}>
|
<div className='mx_InviteDialog_editor' onClick={this.onClickInputArea}>
|
||||||
{targets}
|
{targets}
|
||||||
{input}
|
{input}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderIdentityServerWarning() {
|
private renderIdentityServerWarning() {
|
||||||
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer ||
|
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer ||
|
||||||
!SettingsStore.getValue(UIFeature.IdentityServer)
|
!SettingsStore.getValue(UIFeature.IdentityServer)
|
||||||
) {
|
) {
|
||||||
|
@ -1214,8 +1231,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: sub => <a href="#" onClick={this._onUseDefaultIdentityServerClick}>{sub}</a>,
|
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{sub}</a>,
|
||||||
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||||
},
|
},
|
||||||
)}</div>
|
)}</div>
|
||||||
);
|
);
|
||||||
|
@ -1225,13 +1242,32 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
"Use an identity server to invite by email. " +
|
"Use an identity server to invite by email. " +
|
||||||
"Manage in <settings>Settings</settings>.",
|
"Manage in <settings>Settings</settings>.",
|
||||||
{}, {
|
{}, {
|
||||||
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||||
},
|
},
|
||||||
)}</div>
|
)}</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async onLinkClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
selectText(e.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onCopyClick = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const target = e.target; // copy target before we go async and React throws it away
|
||||||
|
|
||||||
|
const successful = await copyPlaintext(makeUserPermalink(MatrixClientPeg.get().getUserId()));
|
||||||
|
const buttonRect = target.getBoundingClientRect();
|
||||||
|
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||||
|
...toRightOf(buttonRect, 2),
|
||||||
|
message: successful ? _t("Copied!") : _t("Failed to copy"),
|
||||||
|
});
|
||||||
|
// Drop a reference to this close handler for componentWillUnmount
|
||||||
|
this.closeCopiedTooltip = target.onmouseleave = close;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||||
|
@ -1242,12 +1278,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
spinner = <Spinner w={20} h={20} />;
|
spinner = <Spinner w={20} h={20} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
let helpText;
|
let helpText;
|
||||||
let buttonText;
|
let buttonText;
|
||||||
let goButtonFn;
|
let goButtonFn;
|
||||||
let consultSection;
|
let extraSection;
|
||||||
|
let footer;
|
||||||
let keySharingWarning = <span />;
|
let keySharingWarning = <span />;
|
||||||
|
|
||||||
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
||||||
|
@ -1298,7 +1334,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="link"
|
kind="link"
|
||||||
onClick={this._onCommunityInviteClick}
|
onClick={this.onCommunityInviteClick}
|
||||||
>{sub}</AccessibleButton>
|
>{sub}</AccessibleButton>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -1309,7 +1345,27 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
buttonText = _t("Go");
|
buttonText = _t("Go");
|
||||||
goButtonFn = this._startDm;
|
goButtonFn = this.startDm;
|
||||||
|
extraSection = <div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
|
||||||
|
<span>{ _t("Some suggestions may be hidden for privacy.") }</span>
|
||||||
|
<p>{ _t("If you can't see who you’re looking for, send them your invite link below.") }</p>
|
||||||
|
</div>;
|
||||||
|
const link = makeUserPermalink(MatrixClientPeg.get().getUserId());
|
||||||
|
footer = <div className="mx_InviteDialog_footer">
|
||||||
|
<h3>{ _t("Or send invite link") }</h3>
|
||||||
|
<div className="mx_InviteDialog_footer_link">
|
||||||
|
<a href={link} onClick={this.onLinkClick}>
|
||||||
|
{ link }
|
||||||
|
</a>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
title={_t("Copy")}
|
||||||
|
onClick={this.onCopyClick}
|
||||||
|
className="mx_InviteDialog_footer_link_copy"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
} else if (this.props.kind === KIND_INVITE) {
|
} else if (this.props.kind === KIND_INVITE) {
|
||||||
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
||||||
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
|
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
|
||||||
|
@ -1348,7 +1404,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonText = _t("Invite");
|
buttonText = _t("Invite");
|
||||||
goButtonFn = this._inviteUsers;
|
goButtonFn = this.inviteUsers;
|
||||||
|
|
||||||
if (cli.isRoomEncrypted(this.props.roomId)) {
|
if (cli.isRoomEncrypted(this.props.roomId)) {
|
||||||
const room = cli.getRoom(this.props.roomId);
|
const room = cli.getRoom(this.props.roomId);
|
||||||
|
@ -1370,8 +1426,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
||||||
title = _t("Transfer");
|
title = _t("Transfer");
|
||||||
buttonText = _t("Transfer");
|
buttonText = _t("Transfer");
|
||||||
goButtonFn = this._transferCall;
|
goButtonFn = this.transferCall;
|
||||||
consultSection = <div>
|
footer = <div>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
||||||
{_t("Consult first")}
|
{_t("Consult first")}
|
||||||
|
@ -1385,7 +1441,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
|| (this.state.filterText && this.state.filterText.includes('@'));
|
|| (this.state.filterText && this.state.filterText.includes('@'));
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className='mx_InviteDialog'
|
className={classNames("mx_InviteDialog", {
|
||||||
|
mx_InviteDialog_hasFooter: !!footer,
|
||||||
|
})}
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={title}
|
title={title}
|
||||||
|
@ -1393,7 +1451,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
<div className='mx_InviteDialog_content'>
|
<div className='mx_InviteDialog_content'>
|
||||||
<p className='mx_InviteDialog_helpText'>{helpText}</p>
|
<p className='mx_InviteDialog_helpText'>{helpText}</p>
|
||||||
<div className='mx_InviteDialog_addressBar'>
|
<div className='mx_InviteDialog_addressBar'>
|
||||||
{this._renderEditor()}
|
{this.renderEditor()}
|
||||||
<div className='mx_InviteDialog_buttonAndSpinner'>
|
<div className='mx_InviteDialog_buttonAndSpinner'>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="primary"
|
kind="primary"
|
||||||
|
@ -1407,13 +1465,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{keySharingWarning}
|
{keySharingWarning}
|
||||||
{this._renderIdentityServerWarning()}
|
{this.renderIdentityServerWarning()}
|
||||||
<div className='error'>{this.state.errorText}</div>
|
<div className='error'>{this.state.errorText}</div>
|
||||||
<div className='mx_InviteDialog_userSections'>
|
<div className='mx_InviteDialog_userSections'>
|
||||||
{this._renderSection('recents')}
|
{this.renderSection('recents')}
|
||||||
{this._renderSection('suggestions')}
|
{this.renderSection('suggestions')}
|
||||||
|
{extraSection}
|
||||||
</div>
|
</div>
|
||||||
{consultSection}
|
{footer}
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
|
|
|
@ -159,7 +159,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
||||||
stickyBottom={false}
|
stickyBottom={false}
|
||||||
startAtBottom={false}
|
startAtBottom={false}
|
||||||
>
|
>
|
||||||
<ul className="mx_MessageEditHistoryDialog_edits mx_MessagePanel_alwaysShowTimestamps">{this._renderEdits()}</ul>
|
<ul className="mx_MessageEditHistoryDialog_edits">{this._renderEdits()}</ul>
|
||||||
</ScrollPanel>);
|
</ScrollPanel>);
|
||||||
}
|
}
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import TabbedView, {Tab} from "../../structures/TabbedView";
|
import TabbedView, {Tab} from "../../structures/TabbedView";
|
||||||
import {_t, _td} from "../../../languageHandler";
|
import {_t, _td} from "../../../languageHandler";
|
||||||
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
|
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
|
||||||
|
@ -39,31 +38,36 @@ export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
|
||||||
export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
|
export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
|
||||||
export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
|
export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
roomId: string;
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
initialTabId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.RoomSettingsDialog")
|
@replaceableComponent("views.dialogs.RoomSettingsDialog")
|
||||||
export default class RoomSettingsDialog extends React.Component {
|
export default class RoomSettingsDialog extends React.Component<IProps> {
|
||||||
static propTypes = {
|
private dispatcherRef: string;
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount() {
|
||||||
this._dispatcherRef = dis.register(this._onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
if (this._dispatcherRef) dis.unregister(this._dispatcherRef);
|
if (this.dispatcherRef) {
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAction = (payload) => {
|
private onAction = (payload): void => {
|
||||||
// When view changes below us, close the room settings
|
// When view changes below us, close the room settings
|
||||||
// whilst the modal is open this can only be triggered when someone hits Leave Room
|
// whilst the modal is open this can only be triggered when someone hits Leave Room
|
||||||
if (payload.action === 'view_home_page') {
|
if (payload.action === 'view_home_page') {
|
||||||
this.props.onFinished();
|
this.props.onFinished(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_getTabs() {
|
private getTabs(): Tab[] {
|
||||||
const tabs = [];
|
const tabs: Tab[] = [];
|
||||||
|
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
ROOM_GENERAL_TAB,
|
ROOM_GENERAL_TAB,
|
||||||
|
@ -123,7 +127,10 @@ export default class RoomSettingsDialog extends React.Component {
|
||||||
title={_t("Room Settings - %(roomName)s", {roomName})}
|
title={_t("Room Settings - %(roomName)s", {roomName})}
|
||||||
>
|
>
|
||||||
<div className='mx_SettingsDialog_content'>
|
<div className='mx_SettingsDialog_content'>
|
||||||
<TabbedView tabs={this._getTabs()} />
|
<TabbedView
|
||||||
|
tabs={this.getTabs()}
|
||||||
|
initialTabId={this.props.initialTabId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
|
@ -30,7 +30,6 @@ import ToggleSwitch from "../elements/ToggleSwitch";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import {allSettled} from "../../../utils/promise";
|
|
||||||
import {useDispatcher} from "../../../hooks/useDispatcher";
|
import {useDispatcher} from "../../../hooks/useDispatcher";
|
||||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||||
|
|
||||||
|
@ -74,9 +73,13 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
if (avatarChanged) {
|
if (avatarChanged) {
|
||||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
|
if (newAvatar) {
|
||||||
url: await cli.uploadContent(newAvatar),
|
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
|
||||||
}, ""));
|
url: await cli.uploadContent(newAvatar),
|
||||||
|
}, ""));
|
||||||
|
} else {
|
||||||
|
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameChanged) {
|
if (nameChanged) {
|
||||||
|
@ -91,7 +94,7 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
||||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
|
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await allSettled(promises);
|
const results = await Promise.allSettled(promises);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
const failures = results.filter(r => r.status === "rejected");
|
const failures = results.filter(r => r.status === "rejected");
|
||||||
if (failures.length > 0) {
|
if (failures.length > 0) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2016, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,39 +15,44 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import {instanceForInstanceId} from '../../../utils/DirectoryUtils';
|
import { instanceForInstanceId } from '../../../utils/DirectoryUtils';
|
||||||
import {
|
import {
|
||||||
|
ChevronFace,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
useContextMenu,
|
|
||||||
ContextMenuButton,
|
ContextMenuButton,
|
||||||
MenuItemRadio,
|
|
||||||
MenuItem,
|
|
||||||
MenuGroup,
|
MenuGroup,
|
||||||
|
MenuItem,
|
||||||
|
MenuItemRadio,
|
||||||
|
useContextMenu,
|
||||||
} from "../../structures/ContextMenu";
|
} from "../../structures/ContextMenu";
|
||||||
import {_t} from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import {useSettingValue} from "../../../hooks/useSettings";
|
import { useSettingValue } from "../../../hooks/useSettings";
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import withValidation from "../elements/Validation";
|
import withValidation from "../elements/Validation";
|
||||||
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
import TextInputDialog from "../dialogs/TextInputDialog";
|
||||||
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
import { compare } from "../../../utils/strings";
|
||||||
|
|
||||||
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
||||||
|
|
||||||
const SETTING_NAME = "room_directory_servers";
|
const SETTING_NAME = "room_directory_servers";
|
||||||
|
|
||||||
const inPlaceOf = (elementRect) => ({
|
const inPlaceOf = (elementRect: Pick<DOMRect, "right" | "top">) => ({
|
||||||
right: window.innerWidth - elementRect.right,
|
right: UIStore.instance.windowWidth - elementRect.right,
|
||||||
top: elementRect.top,
|
top: elementRect.top,
|
||||||
chevronOffset: 0,
|
chevronOffset: 0,
|
||||||
chevronFace: "none",
|
chevronFace: ChevronFace.None,
|
||||||
});
|
});
|
||||||
|
|
||||||
const validServer = withValidation({
|
const validServer = withValidation<undefined, { error?: MatrixError }>({
|
||||||
deriveData: async ({ value }) => {
|
deriveData: async ({ value }) => {
|
||||||
try {
|
try {
|
||||||
// check if we can successfully load this server's room directory
|
// check if we can successfully load this server's room directory
|
||||||
|
@ -78,15 +82,49 @@ const validServer = withValidation({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
export interface IFieldType {
|
||||||
|
regexp: string;
|
||||||
|
placeholder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IInstance {
|
||||||
|
desc: string;
|
||||||
|
icon?: string;
|
||||||
|
fields: object;
|
||||||
|
network_id: string;
|
||||||
|
// XXX: this is undocumented but we rely on it.
|
||||||
|
// we inject a fake entry with a symbolic instance_id.
|
||||||
|
instance_id: string | symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProtocol {
|
||||||
|
user_fields: string[];
|
||||||
|
location_fields: string[];
|
||||||
|
icon: string;
|
||||||
|
field_types: Record<string, IFieldType>;
|
||||||
|
instances: IInstance[];
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
export type Protocols = Record<string, IProtocol>;
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
protocols: Protocols;
|
||||||
|
selectedServerName: string;
|
||||||
|
selectedInstanceId: string | symbol;
|
||||||
|
onOptionChange(server: string, instanceId?: string | symbol): void;
|
||||||
|
}
|
||||||
|
|
||||||
// This dropdown sources homeservers from three places:
|
// This dropdown sources homeservers from three places:
|
||||||
// + your currently connected homeserver
|
// + your currently connected homeserver
|
||||||
// + homeservers in config.json["roomDirectory"]
|
// + homeservers in config.json["roomDirectory"]
|
||||||
// + homeservers in SettingsStore["room_directory_servers"]
|
// + homeservers in SettingsStore["room_directory_servers"]
|
||||||
// if a server exists in multiple, only keep the top-most entry.
|
// if a server exists in multiple, only keep the top-most entry.
|
||||||
|
|
||||||
const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, selectedInstanceId}) => {
|
const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, selectedInstanceId }: IProps) => {
|
||||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||||
const _userDefinedServers = useSettingValue(SETTING_NAME);
|
const _userDefinedServers: string[] = useSettingValue(SETTING_NAME);
|
||||||
const [userDefinedServers, _setUserDefinedServers] = useState(_userDefinedServers);
|
const [userDefinedServers, _setUserDefinedServers] = useState(_userDefinedServers);
|
||||||
|
|
||||||
const handlerFactory = (server, instanceId) => {
|
const handlerFactory = (server, instanceId) => {
|
||||||
|
@ -98,7 +136,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
|
|
||||||
const setUserDefinedServers = servers => {
|
const setUserDefinedServers = servers => {
|
||||||
_setUserDefinedServers(servers);
|
_setUserDefinedServers(servers);
|
||||||
SettingsStore.setValue(SETTING_NAME, null, "account", servers);
|
SettingsStore.setValue(SETTING_NAME, null, SettingLevel.ACCOUNT, servers);
|
||||||
};
|
};
|
||||||
// keep local echo up to date with external changes
|
// keep local echo up to date with external changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -112,7 +150,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
const roomDirectory = config.roomDirectory || {};
|
const roomDirectory = config.roomDirectory || {};
|
||||||
|
|
||||||
const hsName = MatrixClientPeg.getHomeserverName();
|
const hsName = MatrixClientPeg.getHomeserverName();
|
||||||
const configServers = new Set(roomDirectory.servers);
|
const configServers = new Set<string>(roomDirectory.servers);
|
||||||
|
|
||||||
// configured servers take preference over user-defined ones, if one occurs in both ignore the latter one.
|
// configured servers take preference over user-defined ones, if one occurs in both ignore the latter one.
|
||||||
const removableServers = new Set(userDefinedServers.filter(s => !configServers.has(s) && s !== hsName));
|
const removableServers = new Set(userDefinedServers.filter(s => !configServers.has(s) && s !== hsName));
|
||||||
|
@ -136,15 +174,21 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
// add a fake protocol with the ALL_ROOMS symbol
|
// add a fake protocol with the ALL_ROOMS symbol
|
||||||
protocolsList.push({
|
protocolsList.push({
|
||||||
instances: [{
|
instances: [{
|
||||||
|
fields: [],
|
||||||
|
network_id: "",
|
||||||
instance_id: ALL_ROOMS,
|
instance_id: ALL_ROOMS,
|
||||||
desc: _t("All rooms"),
|
desc: _t("All rooms"),
|
||||||
}],
|
}],
|
||||||
|
location_fields: [],
|
||||||
|
user_fields: [],
|
||||||
|
field_types: {},
|
||||||
|
icon: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protocolsList.forEach(({instances=[]}) => {
|
protocolsList.forEach(({instances=[]}) => {
|
||||||
[...instances].sort((b, a) => {
|
[...instances].sort((b, a) => {
|
||||||
return a.desc.localeCompare(b.desc);
|
return compare(a.desc, b.desc);
|
||||||
}).forEach(({desc, instance_id: instanceId}) => {
|
}).forEach(({desc, instance_id: instanceId}) => {
|
||||||
entries.push(
|
entries.push(
|
||||||
<MenuItemRadio
|
<MenuItemRadio
|
||||||
|
@ -172,7 +216,6 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
if (removableServers.has(server)) {
|
if (removableServers.has(server)) {
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
closeMenu();
|
closeMenu();
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
||||||
const {finished} = Modal.createTrackedDialog("Network Dropdown", "Remove server", QuestionDialog, {
|
const {finished} = Modal.createTrackedDialog("Network Dropdown", "Remove server", QuestionDialog, {
|
||||||
title: _t("Are you sure?"),
|
title: _t("Are you sure?"),
|
||||||
description: _t("Are you sure you want to remove <b>%(serverName)s</b>", {
|
description: _t("Are you sure you want to remove <b>%(serverName)s</b>", {
|
||||||
|
@ -191,7 +234,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
setUserDefinedServers(servers.filter(s => s !== server));
|
setUserDefinedServers(servers.filter(s => s !== server));
|
||||||
|
|
||||||
// the selected server is being removed, reset to our HS
|
// the selected server is being removed, reset to our HS
|
||||||
if (serverSelected === server) {
|
if (serverSelected) {
|
||||||
onOptionChange(hsName, undefined);
|
onOptionChange(hsName, undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -223,7 +266,6 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
|
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
closeMenu();
|
closeMenu();
|
||||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
|
||||||
const { finished } = Modal.createTrackedDialog("Network Dropdown", "Add a new server", TextInputDialog, {
|
const { finished } = Modal.createTrackedDialog("Network Dropdown", "Add a new server", TextInputDialog, {
|
||||||
title: _t("Add a new server"),
|
title: _t("Add a new server"),
|
||||||
description: _t("Enter the name of a new server you want to explore."),
|
description: _t("Enter the name of a new server you want to explore."),
|
||||||
|
@ -284,9 +326,4 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
NetworkDropdown.propTypes = {
|
|
||||||
onOptionChange: PropTypes.func.isRequired,
|
|
||||||
protocols: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NetworkDropdown;
|
export default NetworkDropdown;
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton from "./AccessibleButton";
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip, {Alignment} from './Tooltip';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
|
@ -28,6 +28,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
tooltipClassName?: string;
|
tooltipClassName?: string;
|
||||||
forceHide?: boolean;
|
forceHide?: boolean;
|
||||||
yOffset?: number;
|
yOffset?: number;
|
||||||
|
alignment?: Alignment;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -66,14 +67,15 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, ...props} = this.props;
|
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, ...props} = this.props;
|
||||||
|
|
||||||
const tip = this.state.hover ? <Tooltip
|
const tip = this.state.hover ? <Tooltip
|
||||||
className="mx_AccessibleTooltipButton_container"
|
className="mx_AccessibleTooltipButton_container"
|
||||||
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
||||||
label={tooltip || title}
|
label={tooltip || title}
|
||||||
yOffset={yOffset}
|
yOffset={yOffset}
|
||||||
/> : <div />;
|
alignment={alignment}
|
||||||
|
/> : null;
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -47,9 +47,14 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
// The key used for PersistedElement
|
// The key used for PersistedElement
|
||||||
this._persistKey = getPersistKey(this.props.app.id);
|
this._persistKey = getPersistKey(this.props.app.id);
|
||||||
this._sgWidget = new StopGapWidget(this.props);
|
try {
|
||||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
this._sgWidget = new StopGapWidget(this.props);
|
||||||
this._sgWidget.on("ready", this._onWidgetReady);
|
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||||
|
this._sgWidget.on("ready", this._onWidgetReady);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to construct widget", e);
|
||||||
|
this._sgWidget = null;
|
||||||
|
}
|
||||||
this.iframe = null; // ref to the iframe (callback style)
|
this.iframe = null; // ref to the iframe (callback style)
|
||||||
|
|
||||||
this.state = this._getNewState(props);
|
this.state = this._getNewState(props);
|
||||||
|
@ -97,7 +102,7 @@ export default class AppTile extends React.Component {
|
||||||
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this._persistKey);
|
||||||
this._sgWidget.stop();
|
if (this._sgWidget) this._sgWidget.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ hasPermissionToLoad });
|
this.setState({ hasPermissionToLoad });
|
||||||
|
@ -117,7 +122,7 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// Only fetch IM token on mount if we're showing and have permission to load
|
// Only fetch IM token on mount if we're showing and have permission to load
|
||||||
if (this.state.hasPermissionToLoad) {
|
if (this._sgWidget && this.state.hasPermissionToLoad) {
|
||||||
this._startWidget();
|
this._startWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,10 +151,15 @@ export default class AppTile extends React.Component {
|
||||||
if (this._sgWidget) {
|
if (this._sgWidget) {
|
||||||
this._sgWidget.stop();
|
this._sgWidget.stop();
|
||||||
}
|
}
|
||||||
this._sgWidget = new StopGapWidget(newProps);
|
try {
|
||||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
this._sgWidget = new StopGapWidget(newProps);
|
||||||
this._sgWidget.on("ready", this._onWidgetReady);
|
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||||
this._startWidget();
|
this._sgWidget.on("ready", this._onWidgetReady);
|
||||||
|
this._startWidget();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to construct widget", e);
|
||||||
|
this._sgWidget = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_startWidget() {
|
_startWidget() {
|
||||||
|
@ -161,7 +171,7 @@ export default class AppTile extends React.Component {
|
||||||
_iframeRefChange = (ref) => {
|
_iframeRefChange = (ref) => {
|
||||||
this.iframe = ref;
|
this.iframe = ref;
|
||||||
if (ref) {
|
if (ref) {
|
||||||
this._sgWidget.start(ref);
|
if (this._sgWidget) this._sgWidget.start(ref);
|
||||||
} else {
|
} else {
|
||||||
this._resetWidget(this.props);
|
this._resetWidget(this.props);
|
||||||
}
|
}
|
||||||
|
@ -209,7 +219,7 @@ export default class AppTile extends React.Component {
|
||||||
// Delete the widget from the persisted store for good measure.
|
// Delete the widget from the persisted store for good measure.
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this._persistKey);
|
||||||
|
|
||||||
this._sgWidget.stop({forceDestroy: true});
|
if (this._sgWidget) this._sgWidget.stop({forceDestroy: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWidgetPrepared = () => {
|
_onWidgetPrepared = () => {
|
||||||
|
@ -340,7 +350,13 @@ export default class AppTile extends React.Component {
|
||||||
<Spinner message={_t("Loading...")} />
|
<Spinner message={_t("Loading...")} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (!this.state.hasPermissionToLoad) {
|
if (this._sgWidget === null) {
|
||||||
|
appTileBody = (
|
||||||
|
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
||||||
|
<AppWarning errorMsg={_t("Error loading Widget")} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (!this.state.hasPermissionToLoad) {
|
||||||
// only possible for room widgets, can assert this.props.room here
|
// only possible for room widgets, can assert this.props.room here
|
||||||
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
|
@ -364,7 +380,7 @@ export default class AppTile extends React.Component {
|
||||||
if (this.isMixedContent()) {
|
if (this.isMixedContent()) {
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
||||||
<AppWarning errorMsg="Error - Mixed content" />
|
<AppWarning errorMsg={_t("Error - Mixed content")} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -417,6 +433,8 @@ export default class AppTile extends React.Component {
|
||||||
onFinished={this._closeContextMenu}
|
onFinished={this._closeContextMenu}
|
||||||
showUnpin={!this.props.userWidget}
|
showUnpin={!this.props.userWidget}
|
||||||
userWidget={this.props.userWidget}
|
userWidget={this.props.userWidget}
|
||||||
|
onEditClick={this.props.onEditClick}
|
||||||
|
onDeleteClick={this.props.onDeleteClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import ICanvasEffect from '../../../effects/ICanvasEffect';
|
import ICanvasEffect from '../../../effects/ICanvasEffect';
|
||||||
import {CHAT_EFFECTS} from '../../../effects'
|
import { CHAT_EFFECTS } from '../../../effects'
|
||||||
|
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomWidth: number;
|
roomWidth: number;
|
||||||
|
@ -45,8 +46,8 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const resize = () => {
|
const resize = () => {
|
||||||
if (canvasRef.current) {
|
if (canvasRef.current && canvasRef.current?.height !== UIStore.instance.windowHeight) {
|
||||||
canvasRef.current.height = window.innerHeight;
|
canvasRef.current.height = UIStore.instance.windowHeight;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onAction = (payload: { action: string }) => {
|
const onAction = (payload: { action: string }) => {
|
||||||
|
@ -58,12 +59,12 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
|
||||||
}
|
}
|
||||||
const dispatcherRef = dis.register(onAction);
|
const dispatcherRef = dis.register(onAction);
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
canvas.height = window.innerHeight;
|
canvas.height = UIStore.instance.windowHeight;
|
||||||
window.addEventListener('resize', resize, true);
|
UIStore.instance.on(UI_EVENTS.Resize, resize);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
dis.unregister(dispatcherRef);
|
dis.unregister(dispatcherRef);
|
||||||
window.removeEventListener('resize', resize);
|
UIStore.instance.off(UI_EVENTS.Resize, resize);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
|
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
|
||||||
for (const effect in currentEffects) {
|
for (const effect in currentEffects) {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue