Merge branch 'develop' into gsouquet/fix-backdrop-filter
|
@ -1,16 +0,0 @@
|
|||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
||||
|
||||
src/Markdown.js
|
||||
src/NodeAnimator.js
|
||||
src/components/structures/RoomDirectory.js
|
||||
src/components/views/rooms/MemberList.js
|
||||
src/ratelimitedfunc.js
|
||||
src/utils/DMRoomMap.js
|
||||
src/utils/MultiInviter.js
|
||||
test/components/structures/MessagePanel-test.js
|
||||
test/components/views/dialogs/InteractiveAuthDialog-test.js
|
||||
test/mock-clock.js
|
||||
src/component-index.js
|
||||
test/end-to-end-tests/node_modules/
|
||||
test/end-to-end-tests/element/
|
||||
test/end-to-end-tests/synapse/
|
63
.eslintrc.js
|
@ -1,7 +1,9 @@
|
|||
module.exports = {
|
||||
extends: ["matrix-org", "matrix-org/react-legacy"],
|
||||
parser: "babel-eslint",
|
||||
|
||||
plugins: ["matrix-org"],
|
||||
extends: [
|
||||
"plugin:matrix-org/babel",
|
||||
"plugin:matrix-org/react",
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
|
@ -15,35 +17,60 @@ module.exports = {
|
|||
"prefer-promise-reject-errors": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
"quotes": "off",
|
||||
},
|
||||
"no-extra-boolean-cast": "off",
|
||||
|
||||
// Bind or arrow functions in props causes performance issues (but we
|
||||
// currently use them in some places).
|
||||
// It's disabled here, but we should using it sparingly.
|
||||
"react/jsx-no-bind": "off",
|
||||
"react/jsx-key": ["error"],
|
||||
|
||||
"no-restricted-properties": [
|
||||
"error",
|
||||
...buildRestrictedPropertiesOptions(
|
||||
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
|
||||
"Use UIStore to access window dimensions instead.",
|
||||
),
|
||||
...buildRestrictedPropertiesOptions(
|
||||
["*.mxcUrlToHttp", "*.getHttpUriForMxc"],
|
||||
"Use Media helper instead to centralise access for customisation.",
|
||||
),
|
||||
],
|
||||
},
|
||||
overrides: [{
|
||||
"files": ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
|
||||
"extends": ["matrix-org/ts"],
|
||||
"rules": {
|
||||
files: [
|
||||
"src/**/*.{ts,tsx}",
|
||||
"test/**/*.{ts,tsx}",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/typescript",
|
||||
"plugin:matrix-org/react",
|
||||
],
|
||||
rules: {
|
||||
// Things we do that break the ideal style
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"quotes": "off",
|
||||
"no-extra-boolean-cast": "off",
|
||||
|
||||
// Remove Babel things manually due to override limitations
|
||||
"@babel/no-invalid-this": ["off"],
|
||||
|
||||
// We're okay being explicit at the moment
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
// We disable this while we're transitioning
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// We'd rather not do this but we do
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
|
||||
"quotes": "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(".");
|
||||
let [object, property] = prop.split(".");
|
||||
if (object === "*") {
|
||||
object = undefined;
|
||||
}
|
||||
return {
|
||||
object,
|
||||
property,
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
[include]
|
||||
src/**/*.js
|
||||
test/**/*.js
|
||||
|
||||
[ignore]
|
||||
node_modules/
|
171
CHANGELOG.md
|
@ -1,3 +1,174 @@
|
|||
Changes in [3.25.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0) (2021-07-05)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0-rc.1...v3.25.0)
|
||||
|
||||
* Remove reminescent references to the tinter
|
||||
[\#6316](https://github.com/matrix-org/matrix-react-sdk/pull/6316)
|
||||
* Update to released version of js-sdk
|
||||
|
||||
Changes in [3.25.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0-rc.1) (2021-06-29)
|
||||
===============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0...v3.25.0-rc.1)
|
||||
|
||||
* Update to js-sdk v12.0.1-rc.1
|
||||
* Translations update from Weblate
|
||||
[\#6286](https://github.com/matrix-org/matrix-react-sdk/pull/6286)
|
||||
* Fix back button on user info card after clicking a permalink
|
||||
[\#6277](https://github.com/matrix-org/matrix-react-sdk/pull/6277)
|
||||
* Group ACLs with MELS
|
||||
[\#6280](https://github.com/matrix-org/matrix-react-sdk/pull/6280)
|
||||
* Fix editState not getting passed through
|
||||
[\#6282](https://github.com/matrix-org/matrix-react-sdk/pull/6282)
|
||||
* Migrate message context menu to IconizedContextMenu
|
||||
[\#5671](https://github.com/matrix-org/matrix-react-sdk/pull/5671)
|
||||
* Improve audio recording performance
|
||||
[\#6240](https://github.com/matrix-org/matrix-react-sdk/pull/6240)
|
||||
* Fix multiple timeline panels handling composer and edit events
|
||||
[\#6278](https://github.com/matrix-org/matrix-react-sdk/pull/6278)
|
||||
* Let m.notice messages mark a room as unread
|
||||
[\#6281](https://github.com/matrix-org/matrix-react-sdk/pull/6281)
|
||||
* Removes the override on the Bubble Container
|
||||
[\#5953](https://github.com/matrix-org/matrix-react-sdk/pull/5953)
|
||||
* Fix IRC layout regressions
|
||||
[\#6193](https://github.com/matrix-org/matrix-react-sdk/pull/6193)
|
||||
* Fix trashcan.svg by exporting it with its viewbox
|
||||
[\#6248](https://github.com/matrix-org/matrix-react-sdk/pull/6248)
|
||||
* Fix tiny scrollbar dot on chrome/electron in Forward Dialog
|
||||
[\#6276](https://github.com/matrix-org/matrix-react-sdk/pull/6276)
|
||||
* Upgrade puppeteer to use newer version of Chrome
|
||||
[\#6268](https://github.com/matrix-org/matrix-react-sdk/pull/6268)
|
||||
* Make toast dismiss button less prominent
|
||||
[\#6275](https://github.com/matrix-org/matrix-react-sdk/pull/6275)
|
||||
* Encrypt the voice message file if needed
|
||||
[\#6269](https://github.com/matrix-org/matrix-react-sdk/pull/6269)
|
||||
* Fix hyper-precise presence
|
||||
[\#6270](https://github.com/matrix-org/matrix-react-sdk/pull/6270)
|
||||
* Fix issues around private spaces, including previewable
|
||||
[\#6265](https://github.com/matrix-org/matrix-react-sdk/pull/6265)
|
||||
* Make _pinned messages_ in `m.room.pinned_events` event clickable
|
||||
[\#6257](https://github.com/matrix-org/matrix-react-sdk/pull/6257)
|
||||
* Fix space avatar management layout being broken
|
||||
[\#6266](https://github.com/matrix-org/matrix-react-sdk/pull/6266)
|
||||
* Convert EntityTile, MemberTile and PresenceLabel to TS
|
||||
[\#6251](https://github.com/matrix-org/matrix-react-sdk/pull/6251)
|
||||
* Fix UserInfo not working when rendered without a room
|
||||
[\#6260](https://github.com/matrix-org/matrix-react-sdk/pull/6260)
|
||||
* Update membership reason handling, including leave reason displaying
|
||||
[\#6253](https://github.com/matrix-org/matrix-react-sdk/pull/6253)
|
||||
* Consolidate types with js-sdk changes
|
||||
[\#6220](https://github.com/matrix-org/matrix-react-sdk/pull/6220)
|
||||
* Fix edit history modal
|
||||
[\#6258](https://github.com/matrix-org/matrix-react-sdk/pull/6258)
|
||||
* Convert MemberList to TS
|
||||
[\#6249](https://github.com/matrix-org/matrix-react-sdk/pull/6249)
|
||||
* Fix two PRs duplicating the css attribute
|
||||
[\#6259](https://github.com/matrix-org/matrix-react-sdk/pull/6259)
|
||||
* Improve invite error messages in InviteDialog for room invites
|
||||
[\#6201](https://github.com/matrix-org/matrix-react-sdk/pull/6201)
|
||||
* Fix invite dialog being cut off when it has limited results
|
||||
[\#6256](https://github.com/matrix-org/matrix-react-sdk/pull/6256)
|
||||
* Fix pinning event in a room which hasn't had events pinned in before
|
||||
[\#6255](https://github.com/matrix-org/matrix-react-sdk/pull/6255)
|
||||
* Allow modal widget buttons to be disabled when the modal opens
|
||||
[\#6178](https://github.com/matrix-org/matrix-react-sdk/pull/6178)
|
||||
* Decrease e2e shield fill mask size so that it doesn't overlap
|
||||
[\#6250](https://github.com/matrix-org/matrix-react-sdk/pull/6250)
|
||||
* Dial Pad UI bug fixes
|
||||
[\#5786](https://github.com/matrix-org/matrix-react-sdk/pull/5786)
|
||||
* Simple handling of mid-call output changes
|
||||
[\#6247](https://github.com/matrix-org/matrix-react-sdk/pull/6247)
|
||||
* Improve ForwardDialog performance by using TruncatedList
|
||||
[\#6228](https://github.com/matrix-org/matrix-react-sdk/pull/6228)
|
||||
* Fix dependency and lockfile mismatch
|
||||
[\#6246](https://github.com/matrix-org/matrix-react-sdk/pull/6246)
|
||||
* Improve room directory click behaviour
|
||||
[\#6234](https://github.com/matrix-org/matrix-react-sdk/pull/6234)
|
||||
* Fix keyboard accessibility of the space panel
|
||||
[\#6239](https://github.com/matrix-org/matrix-react-sdk/pull/6239)
|
||||
* Add ways to manage addresses for Spaces
|
||||
[\#6151](https://github.com/matrix-org/matrix-react-sdk/pull/6151)
|
||||
* Hide communities invites and the community autocompleter when Spaces on
|
||||
[\#6244](https://github.com/matrix-org/matrix-react-sdk/pull/6244)
|
||||
* Convert bunch of files to TS
|
||||
[\#6241](https://github.com/matrix-org/matrix-react-sdk/pull/6241)
|
||||
* Open local addresses section by default when there are no existing local
|
||||
addresses
|
||||
[\#6179](https://github.com/matrix-org/matrix-react-sdk/pull/6179)
|
||||
* Allow reordering of the space panel via Drag and Drop
|
||||
[\#6137](https://github.com/matrix-org/matrix-react-sdk/pull/6137)
|
||||
* Replace drag and drop mechanism in communities with something simpler
|
||||
[\#6134](https://github.com/matrix-org/matrix-react-sdk/pull/6134)
|
||||
* EventTilePreview fixes
|
||||
[\#6000](https://github.com/matrix-org/matrix-react-sdk/pull/6000)
|
||||
* Upgrade @types/react and @types/react-dom
|
||||
[\#6233](https://github.com/matrix-org/matrix-react-sdk/pull/6233)
|
||||
* Fix type error in the SpaceStore
|
||||
[\#6242](https://github.com/matrix-org/matrix-react-sdk/pull/6242)
|
||||
* Add experimental options to the Spaces beta
|
||||
[\#6199](https://github.com/matrix-org/matrix-react-sdk/pull/6199)
|
||||
* Consolidate types with js-sdk changes
|
||||
[\#6215](https://github.com/matrix-org/matrix-react-sdk/pull/6215)
|
||||
* Fix branch matching for Buildkite
|
||||
[\#6236](https://github.com/matrix-org/matrix-react-sdk/pull/6236)
|
||||
* Migrate SearchBar to TypeScript
|
||||
[\#6230](https://github.com/matrix-org/matrix-react-sdk/pull/6230)
|
||||
* Add support to keyboard shortcuts dialog for [digits]
|
||||
[\#6088](https://github.com/matrix-org/matrix-react-sdk/pull/6088)
|
||||
* Fix modal opening race condition
|
||||
[\#6238](https://github.com/matrix-org/matrix-react-sdk/pull/6238)
|
||||
* Deprecate FormButton in favour of AccessibleButton
|
||||
[\#6229](https://github.com/matrix-org/matrix-react-sdk/pull/6229)
|
||||
* Add PR template
|
||||
[\#6216](https://github.com/matrix-org/matrix-react-sdk/pull/6216)
|
||||
* Prefer canonical aliases while autocompleting rooms
|
||||
[\#6222](https://github.com/matrix-org/matrix-react-sdk/pull/6222)
|
||||
* Fix quote button
|
||||
[\#6232](https://github.com/matrix-org/matrix-react-sdk/pull/6232)
|
||||
* Restore branch matching support for GitHub Actions e2e tests
|
||||
[\#6224](https://github.com/matrix-org/matrix-react-sdk/pull/6224)
|
||||
* Fix View Source accessing renamed private field on MatrixEvent
|
||||
[\#6225](https://github.com/matrix-org/matrix-react-sdk/pull/6225)
|
||||
* Fix ConfirmUserActionDialog returning an input field rather than text
|
||||
[\#6219](https://github.com/matrix-org/matrix-react-sdk/pull/6219)
|
||||
* Revert "Partially restore immutable event objects at the rendering layer"
|
||||
[\#6221](https://github.com/matrix-org/matrix-react-sdk/pull/6221)
|
||||
* Add jq to e2e tests Dockerfile
|
||||
[\#6218](https://github.com/matrix-org/matrix-react-sdk/pull/6218)
|
||||
* Partially restore immutable event objects at the rendering layer
|
||||
[\#6196](https://github.com/matrix-org/matrix-react-sdk/pull/6196)
|
||||
* Update MSC number references for voice messages
|
||||
[\#6197](https://github.com/matrix-org/matrix-react-sdk/pull/6197)
|
||||
* Fix phase enum usage in JS modules as well
|
||||
[\#6214](https://github.com/matrix-org/matrix-react-sdk/pull/6214)
|
||||
* Migrate some dialogs to TypeScript
|
||||
[\#6185](https://github.com/matrix-org/matrix-react-sdk/pull/6185)
|
||||
* Typescript fixes due to MatrixEvent being TSified
|
||||
[\#6208](https://github.com/matrix-org/matrix-react-sdk/pull/6208)
|
||||
* Allow click-to-ping, quote & emoji picker for edit composer too
|
||||
[\#5858](https://github.com/matrix-org/matrix-react-sdk/pull/5858)
|
||||
* Add call silencing
|
||||
[\#6082](https://github.com/matrix-org/matrix-react-sdk/pull/6082)
|
||||
* Fix types in SlashCommands
|
||||
[\#6207](https://github.com/matrix-org/matrix-react-sdk/pull/6207)
|
||||
* Benchmark multiple common user scenario
|
||||
[\#6190](https://github.com/matrix-org/matrix-react-sdk/pull/6190)
|
||||
* Fix forward dialog message preview display names
|
||||
[\#6204](https://github.com/matrix-org/matrix-react-sdk/pull/6204)
|
||||
* Remove stray bullet point in reply preview
|
||||
[\#6206](https://github.com/matrix-org/matrix-react-sdk/pull/6206)
|
||||
* Stop requesting null next replies from the server
|
||||
[\#6203](https://github.com/matrix-org/matrix-react-sdk/pull/6203)
|
||||
* Fix soft crash caused by a broken shouldComponentUpdate
|
||||
[\#6202](https://github.com/matrix-org/matrix-react-sdk/pull/6202)
|
||||
* Keep composer reply when scrolling away from a highlighted event
|
||||
[\#6200](https://github.com/matrix-org/matrix-react-sdk/pull/6200)
|
||||
* Cache virtual/native room mappings when they're created
|
||||
[\#6194](https://github.com/matrix-org/matrix-react-sdk/pull/6194)
|
||||
* Disable comment-on-alert
|
||||
[\#6191](https://github.com/matrix-org/matrix-react-sdk/pull/6191)
|
||||
* Bump postcss from 7.0.35 to 7.0.36
|
||||
[\#6195](https://github.com/matrix-org/matrix-react-sdk/pull/6195)
|
||||
|
||||
Changes in [3.24.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0) (2021-06-21)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0-rc.1...v3.24.0)
|
||||
|
|
|
@ -10,7 +10,6 @@ module.exports = {
|
|||
],
|
||||
}],
|
||||
"@babel/preset-typescript",
|
||||
"@babel/preset-flow",
|
||||
"@babel/preset-react",
|
||||
],
|
||||
"plugins": [
|
||||
|
@ -19,7 +18,6 @@ module.exports = {
|
|||
"@babel/plugin-proposal-numeric-separator",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
"@babel/plugin-transform-flow-comments",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-runtime",
|
||||
],
|
||||
|
|
22
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.24.0",
|
||||
"version": "3.25.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -45,7 +45,7 @@
|
|||
"start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"",
|
||||
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
|
||||
"lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
|
||||
"lint:js": "eslint --max-warnings 0 src test",
|
||||
"lint:types": "tsc --noEmit --jsx react",
|
||||
"lint:style": "stylelint 'res/css/**/*.scss'",
|
||||
"test": "jest",
|
||||
|
@ -54,7 +54,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/commonmark": "^0.27.4",
|
||||
"await-lock": "^2.1.0",
|
||||
"blurhash": "^1.1.3",
|
||||
"browser-encrypt-attachment": "^0.3.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"cheerio": "^1.0.0-rc.9",
|
||||
|
@ -79,7 +81,7 @@
|
|||
"katex": "^0.12.0",
|
||||
"linkifyjs": "^2.1.9",
|
||||
"lodash": "^4.17.20",
|
||||
"matrix-js-sdk": "12.0.0",
|
||||
"matrix-js-sdk": "12.0.1",
|
||||
"matrix-widget-api": "^0.1.0-beta.15",
|
||||
"minimist": "^1.2.5",
|
||||
"opus-recorder": "^8.0.3",
|
||||
|
@ -105,16 +107,16 @@
|
|||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/parser": "^7.12.11",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-decorators": "^7.12.12",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.12.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-transform-flow-comments": "^7.12.1",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-flow": "^7.12.1",
|
||||
"@babel/preset-react": "^7.12.10",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
|
@ -140,18 +142,16 @@
|
|||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/sanitize-html": "^2.3.1",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.14.0",
|
||||
"@typescript-eslint/parser": "^4.14.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||
"@typescript-eslint/parser": "^4.17.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"chokidar": "^3.5.1",
|
||||
"concurrently": "^5.3.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"eslint": "7.18.0",
|
||||
"eslint-config-matrix-org": "^0.2.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-flowtype": "^5.2.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"glob": "^7.1.6",
|
||||
|
|
|
@ -37,6 +37,11 @@
|
|||
@import "./structures/_ViewSource.scss";
|
||||
@import "./structures/auth/_CompleteSecurity.scss";
|
||||
@import "./structures/auth/_Login.scss";
|
||||
@import "./views/audio_messages/_AudioPlayer.scss";
|
||||
@import "./views/audio_messages/_PlayPauseButton.scss";
|
||||
@import "./views/audio_messages/_PlaybackContainer.scss";
|
||||
@import "./views/audio_messages/_SeekBar.scss";
|
||||
@import "./views/audio_messages/_Waveform.scss";
|
||||
@import "./views/auth/_AuthBody.scss";
|
||||
@import "./views/auth/_AuthButtons.scss";
|
||||
@import "./views/auth/_AuthFooter.scss";
|
||||
|
@ -52,7 +57,6 @@
|
|||
@import "./views/avatars/_BaseAvatar.scss";
|
||||
@import "./views/avatars/_DecoratedRoomAvatar.scss";
|
||||
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
||||
@import "./views/avatars/_PulsedAvatar.scss";
|
||||
@import "./views/avatars/_WidgetAvatar.scss";
|
||||
@import "./views/beta/_BetaCard.scss";
|
||||
@import "./views/context_menus/_CallContextMenu.scss";
|
||||
|
@ -165,6 +169,7 @@
|
|||
@import "./views/messages/_MTextBody.scss";
|
||||
@import "./views/messages/_MVideoBody.scss";
|
||||
@import "./views/messages/_MVoiceMessageBody.scss";
|
||||
@import "./views/messages/_MediaBody.scss";
|
||||
@import "./views/messages/_MessageActionBar.scss";
|
||||
@import "./views/messages/_MessageTimestamp.scss";
|
||||
@import "./views/messages/_MjolnirBody.scss";
|
||||
|
@ -253,9 +258,6 @@
|
|||
@import "./views/toasts/_AnalyticsToast.scss";
|
||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||
@import "./views/verification/_VerificationShowSas.scss";
|
||||
@import "./views/voice_messages/_PlayPauseButton.scss";
|
||||
@import "./views/voice_messages/_PlaybackContainer.scss";
|
||||
@import "./views/voice_messages/_Waveform.scss";
|
||||
@import "./views/voip/_CallContainer.scss";
|
||||
@import "./views/voip/_CallView.scss";
|
||||
@import "./views/voip/_CallViewForRoom.scss";
|
||||
|
|
|
@ -323,7 +323,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_GroupView_featuredThing .mx_BaseAvatar {
|
||||
/* To prevent misalignment with mx_TintableSvg (in addButton) */
|
||||
/* To prevent misalignment with img (in addButton) */
|
||||
vertical-align: initial;
|
||||
}
|
||||
|
||||
|
|
|
@ -111,6 +111,29 @@ $roomListCollapsedWidth: 68px;
|
|||
}
|
||||
}
|
||||
|
||||
.mx_LeftPanel_dialPadButton {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background-color: $roomlist-button-bg-color;
|
||||
position: relative;
|
||||
margin-left: 8px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
mask-image: url('$(res)/img/element-icons/call/dialpad.svg');
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
background: $secondary-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_LeftPanel_exploreButton {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
@ -185,6 +208,12 @@ $roomListCollapsedWidth: 68px;
|
|||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.mx_LeftPanel_dialPadButton {
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.mx_LeftPanel_exploreButton {
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
|
|
|
@ -121,23 +121,51 @@ $pulse-color: $pinned-unread-color;
|
|||
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
|
||||
animation: mx_RightPanel_indicator_pulse 2s infinite;
|
||||
animation-iteration-count: 1;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: scale(1);
|
||||
transform-origin: center center;
|
||||
animation-name: mx_RightPanel_indicator_pulse_shadow;
|
||||
animation-duration: inherit;
|
||||
animation-iteration-count: inherit;
|
||||
border-radius: 50%;
|
||||
background: rgba($pulse-color, 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);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mx_RightPanel_indicator_pulse_shadow {
|
||||
0% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(2.2);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ limitations under the License.
|
|||
|
||||
.mx_AccessibleButton {
|
||||
padding: 5px 10px;
|
||||
padding-left: 28px; // 16px for the icon, 2px margin to text, 10px regular padding
|
||||
padding-left: 30px; // 18px for the icon, 2px margin to text, 10px regular padding
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
|
@ -128,13 +128,14 @@ limitations under the License.
|
|||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
top: 50%; // text sizes are dynamic
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&.mx_RoomStatusBar_unsentCancelAllBtn::before {
|
||||
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||
width: 12px;
|
||||
height: 16px;
|
||||
top: calc(50% - 8px); // text sizes are dynamic
|
||||
}
|
||||
|
||||
&.mx_RoomStatusBar_unsentResendAllBtn {
|
||||
|
@ -142,9 +143,6 @@ limitations under the License.
|
|||
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/element-icons/retry.svg');
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
top: calc(50% - 9px); // text sizes are dynamic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,14 +57,15 @@ limitations under the License.
|
|||
|
||||
@keyframes mx_RoomView_fileDropTarget_image_animation {
|
||||
from {
|
||||
width: 0px;
|
||||
transform: scaleX(0);
|
||||
}
|
||||
to {
|
||||
width: 32px;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomView_fileDropTarget_image {
|
||||
width: 32px;
|
||||
animation: mx_RoomView_fileDropTarget_image_animation;
|
||||
animation-duration: 0.5s;
|
||||
margin-bottom: 16px;
|
||||
|
|
|
@ -134,12 +134,15 @@ limitations under the License.
|
|||
.mx_Toast_buttons {
|
||||
float: right;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
min-width: 96px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton + .mx_AccessibleButton {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Toast_description {
|
||||
|
|
68
res/css/views/audio_messages/_AudioPlayer.scss
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
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_AudioPlayer_container {
|
||||
padding: 16px 12px 12px 12px;
|
||||
max-width: 267px; // use max to make the control fit in the files/pinned panels
|
||||
|
||||
.mx_AudioPlayer_primaryContainer {
|
||||
display: flex;
|
||||
|
||||
.mx_PlayPauseButton {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.mx_AudioPlayer_mediaInfo {
|
||||
flex: 1;
|
||||
overflow: hidden; // makes the ellipsis on the file name work
|
||||
|
||||
& > * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_AudioPlayer_mediaName {
|
||||
color: $primary-fg-color;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-15px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding-bottom: 4px; // mimics the line-height differences in the Figma
|
||||
}
|
||||
|
||||
.mx_AudioPlayer_byline {
|
||||
font-size: $font-12px;
|
||||
line-height: $font-12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AudioPlayer_seek {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.mx_SeekBar {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mx_Clock {
|
||||
width: $font-42px; // we're not using a monospace font, so fake it
|
||||
min-width: $font-42px; // for flexbox
|
||||
padding-left: 4px; // isolate from seek bar
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,6 +18,8 @@ limitations under the License.
|
|||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: 32px; // for when the button is used in a flexbox
|
||||
min-height: 32px; // for when the button is used in a flexbox
|
||||
border-radius: 32px;
|
||||
background-color: $voice-playback-button-bg-color;
|
||||
|
|
@ -22,25 +22,24 @@ limitations under the License.
|
|||
// 7px top and bottom for visual design. 12px left & right, but the waveform (right)
|
||||
// has a 1px padding on it that we want to account for.
|
||||
padding: 7px 12px 7px 11px;
|
||||
background-color: $voice-record-waveform-bg-color;
|
||||
border-radius: 12px;
|
||||
|
||||
// Cheat at alignment a bit
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
color: $voice-record-waveform-fg-color;
|
||||
font-size: $font-14px;
|
||||
line-height: $font-24px;
|
||||
contain: content;
|
||||
|
||||
.mx_Waveform {
|
||||
.mx_Waveform_bar {
|
||||
background-color: $voice-record-waveform-incomplete-fg-color;
|
||||
height: 100%;
|
||||
/* Variable set by a JS component */
|
||||
transform: scaleY(max(0.05, var(--barHeight)));
|
||||
|
||||
&.mx_Waveform_bar_100pct {
|
||||
// Small animation to remove the mechanical feel of progress
|
||||
transition: background-color 250ms ease;
|
||||
background-color: $voice-record-waveform-fg-color;
|
||||
background-color: $message-body-panel-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
103
res/css/views/audio_messages/_SeekBar.scss
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// CSS inspiration from:
|
||||
// * https://www.w3schools.com/howto/howto_js_rangeslider.asp
|
||||
// * https://stackoverflow.com/a/28283806
|
||||
// * https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/
|
||||
|
||||
.mx_SeekBar {
|
||||
// Dev note: we deliberately do not have the -ms-track (and friends) selectors because we don't
|
||||
// need to support IE.
|
||||
|
||||
appearance: none; // default style override
|
||||
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: $quaternary-fg-color;
|
||||
outline: none; // remove blue selection border
|
||||
position: relative; // for before+after pseudo elements later on
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
appearance: none; // default style override
|
||||
|
||||
// Dev note: This needs to be duplicated with the -moz-range-thumb selector
|
||||
// because otherwise Edge (webkit) will fail to see the styles and just refuse
|
||||
// to apply them.
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 8px;
|
||||
background-color: $tertiary-fg-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 8px;
|
||||
background-color: $tertiary-fg-color;
|
||||
cursor: pointer;
|
||||
|
||||
// Firefox adds a border on the thumb
|
||||
border: none;
|
||||
}
|
||||
|
||||
// This is for webkit support, but we can't limit the functionality of it to just webkit
|
||||
// browsers. Firefox responds to webkit-prefixed values now, which means we can't use media
|
||||
// or support queries to selectively apply the rule. An upside is that this CSS doesn't work
|
||||
// in firefox, so it's just wasted CPU/GPU time.
|
||||
&::before { // ::before to ensure it ends up under the thumb
|
||||
content: '';
|
||||
background-color: $tertiary-fg-color;
|
||||
|
||||
// Absolute positioning to ensure it overlaps with the existing bar
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
// Sizing to match the bar
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
|
||||
// And finally dynamic width without overly hurting the rendering engine.
|
||||
transform-origin: 0 100%;
|
||||
transform: scaleX(var(--fillTo));
|
||||
}
|
||||
|
||||
// This is firefox's built-in support for the above, with 100% less hacks.
|
||||
&::-moz-range-progress {
|
||||
background-color: $tertiary-fg-color;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
// Increase clickable area for the slider (approximately same size as browser default)
|
||||
// We do it this way to keep the same padding and margins of the element, avoiding margin math.
|
||||
// Source: https://front-back.com/expand-clickable-areas-for-a-better-touch-experience/
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
bottom: -6px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
|
@ -110,24 +110,52 @@ $dot-size: 12px;
|
|||
width: $dot-size;
|
||||
transform: scale(1);
|
||||
background: rgba($pulse-color, 1);
|
||||
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
|
||||
animation: mx_Beta_bluePulse 2s infinite;
|
||||
animation-iteration-count: 20;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: scale(1);
|
||||
transform-origin: center center;
|
||||
animation-name: mx_Beta_bluePulse_shadow;
|
||||
animation-duration: inherit;
|
||||
animation-iteration-count: inherit;
|
||||
border-radius: 50%;
|
||||
background: rgba($pulse-color, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mx_Beta_bluePulse {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mx_Beta_bluePulse_shadow {
|
||||
0% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(2.2);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2021 Michael Weimann <mail@michael-weimann.eu>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,16 +16,69 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_MessageContextMenu {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.mx_MessageContextMenu_field {
|
||||
.mx_IconizedContextMenu_icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
padding: 3px 6px 3px 6px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
background: $primary-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MessageContextMenu_field.mx_MessageContextMenu_fieldSet {
|
||||
font-weight: bold;
|
||||
.mx_MessageContextMenu_iconCollapse::before {
|
||||
mask-image: url('$(res)/img/element-icons/message/chevron-up.svg');
|
||||
}
|
||||
|
||||
.mx_MessageContextMenu_iconReport::before {
|
||||
mask-image: url('$(res)/img/element-icons/warning-badge.svg');
|
||||
}
|
||||
|
||||
.mx_MessageContextMenu_iconLink::before {
|
||||
mask-image: url('$(res)/img/element-icons/link.svg');
|
||||
}
|
||||
|
||||
.mx_MessageContextMenu_iconPermalink::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/share.svg');
|
||||
}
|
||||
|
||||
.mx_MessageContextMenu_iconUnhidePreview::before {
|
||||
mask-image: url('$(res)/img/element-icons/settings/appearance.svg');
|
||||
}
|
||||
|
||||
.mx_MessageContextMenu_iconForward::before {
|
||||
mask-image: url('$(res)/img/element-icons/message/fwd.svg');
|
||||
}
|
||||
|
||||
.mx_MessageContextMenu_iconRedact::before {
|
||||
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||
}
|
||||
|
||||
.mx_MessageContextMenu_iconResend::before {
|
||||
mask-image: url('$(res)/img/element-icons/retry.svg');
|
||||
}
|
||||
|
||||
.mx_MessageContextMenu_iconSource::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/format-bar/code.svg');
|
||||
}
|
||||
|
||||
.mx_MessageContextMenu_iconQuote::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/format-bar/quote.svg');
|
||||
}
|
||||
|
||||
.mx_MessageContextMenu_iconPin::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
||||
}
|
||||
|
||||
.mx_MessageContextMenu_iconUnpin::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ limitations under the License.
|
|||
> .mx_ForwardDialog_preview {
|
||||
max-height: 30%;
|
||||
flex-shrink: 0;
|
||||
overflow: scroll;
|
||||
overflow-y: auto;
|
||||
|
||||
div {
|
||||
pointer-events: none;
|
||||
|
|
|
@ -28,6 +28,7 @@ limitations under the License.
|
|||
left: 0;
|
||||
top: 2px; // alignment
|
||||
background-image: url("$(res)/img/element-icons/warning-badge.svg");
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.mx_AccessSecretStorageDialog_reset_link {
|
||||
|
|
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
$timelineImageBorderRadius: 4px;
|
||||
|
||||
.mx_MImageBody {
|
||||
display: block;
|
||||
margin-right: 34px;
|
||||
|
@ -25,7 +27,11 @@ limitations under the License.
|
|||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: 4px;
|
||||
border-radius: $timelineImageBorderRadius;
|
||||
|
||||
> canvas {
|
||||
border-radius: $timelineImageBorderRadius;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MImageBody_thumbnail_container {
|
||||
|
@ -43,7 +49,7 @@ limitations under the License.
|
|||
top: 50%;
|
||||
}
|
||||
|
||||
// Inner img and TintableSvg should be centered around 0, 0
|
||||
// Inner img should be centered around 0, 0
|
||||
.mx_MImageBody_thumbnail_spinner > * {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
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.
|
||||
|
@ -14,17 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_PulsedAvatar {
|
||||
@keyframes shadow-pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0px rgba($accent-color, 0.2);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 6px rgba($accent-color, 0);
|
||||
}
|
||||
// A "media body" is any file upload looking thing, apart from images and videos (they
|
||||
// have unique styles).
|
||||
|
||||
.mx_MediaBody {
|
||||
background-color: $message-body-panel-bg-color;
|
||||
border-radius: 12px;
|
||||
|
||||
color: $message-body-panel-fg-color;
|
||||
font-size: $font-14px;
|
||||
line-height: $font-24px;
|
||||
}
|
||||
|
||||
img {
|
||||
animation: shadow-pulse 1s infinite;
|
||||
}
|
||||
}
|
|
@ -17,4 +17,9 @@ limitations under the License.
|
|||
.mx_TextualEvent {
|
||||
opacity: 0.5;
|
||||
overflow-y: hidden;
|
||||
|
||||
a {
|
||||
color: $accent-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ limitations under the License.
|
|||
.mx_cryptoEvent_buttons {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.mx_cryptoEvent_state {
|
||||
|
|
|
@ -29,6 +29,7 @@ $irc-line-height: $font-18px;
|
|||
// timestamps are links which shouldn't be underlined
|
||||
> a {
|
||||
text-decoration: none;
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
|
@ -49,18 +50,6 @@ $irc-line-height: $font-18px;
|
|||
}
|
||||
}
|
||||
|
||||
> .mx_SenderProfile {
|
||||
order: 2;
|
||||
flex-shrink: 0;
|
||||
width: var(--name-width);
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
|
@ -173,27 +162,37 @@ $irc-line-height: $font-18px;
|
|||
border-left: 0;
|
||||
}
|
||||
|
||||
.mx_SenderProfile_hover {
|
||||
background-color: $primary-bg-color;
|
||||
overflow: hidden;
|
||||
.mx_SenderProfile {
|
||||
width: var(--name-width);
|
||||
display: flex;
|
||||
order: 2;
|
||||
flex-shrink: 0;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
> .mx_SenderProfile_displayName {
|
||||
width: 100%;
|
||||
text-align: end;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: var(--name-width);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
> .mx_SenderProfile_mxid {
|
||||
visibility: collapse;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SenderProfile:hover {
|
||||
justify-content: flex-start;
|
||||
overflow: visible;
|
||||
z-index: 10;
|
||||
|
||||
> .mx_SenderProfile_displayName {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.mx_SenderProfile_hover:hover {
|
||||
overflow: visible;
|
||||
width: max(auto, 100%);
|
||||
z-index: 10;
|
||||
> .mx_SenderProfile_mxid {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ReplyThread {
|
||||
|
@ -201,16 +200,7 @@ $irc-line-height: $font-18px;
|
|||
.mx_SenderProfile {
|
||||
width: unset;
|
||||
max-width: var(--name-width);
|
||||
}
|
||||
|
||||
.mx_SenderProfile_hover {
|
||||
background: transparent;
|
||||
|
||||
> span {
|
||||
> .mx_SenderProfile_displayName {
|
||||
min-width: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_emote {
|
||||
|
|
|
@ -36,10 +36,10 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_VoiceRecordComposerTile_delete {
|
||||
width: 14px; // w&h are size of icon
|
||||
height: 18px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
vertical-align: middle;
|
||||
margin-right: 11px; // distance from left edge of waveform container (container has some margin too)
|
||||
margin-right: 8px; // distance from left edge of waveform container (container has some margin too)
|
||||
background-color: $voice-record-icon-color;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
|
|
|
@ -73,7 +73,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton {
|
||||
.mx_AccessibleButton_hasKind {
|
||||
padding: 8px 22px;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
|
|
3
res/img/element-icons/call/dialpad.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="12" height="18" viewBox="0 0 12 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 14.25C5.175 14.25 4.5 14.925 4.5 15.75C4.5 16.575 5.175 17.25 6 17.25C6.825 17.25 7.5 16.575 7.5 15.75C7.5 14.925 6.825 14.25 6 14.25ZM1.5 0.75C0.675 0.75 0 1.425 0 2.25C0 3.075 0.675 3.75 1.5 3.75C2.325 3.75 3 3.075 3 2.25C3 1.425 2.325 0.75 1.5 0.75ZM1.5 5.25C0.675 5.25 0 5.925 0 6.75C0 7.575 0.675 8.25 1.5 8.25C2.325 8.25 3 7.575 3 6.75C3 5.925 2.325 5.25 1.5 5.25ZM1.5 9.75C0.675 9.75 0 10.425 0 11.25C0 12.075 0.675 12.75 1.5 12.75C2.325 12.75 3 12.075 3 11.25C3 10.425 2.325 9.75 1.5 9.75ZM10.5 3.75C11.325 3.75 12 3.075 12 2.25C12 1.425 11.325 0.75 10.5 0.75C9.675 0.75 9 1.425 9 2.25C9 3.075 9.675 3.75 10.5 3.75ZM6 9.75C5.175 9.75 4.5 10.425 4.5 11.25C4.5 12.075 5.175 12.75 6 12.75C6.825 12.75 7.5 12.075 7.5 11.25C7.5 10.425 6.825 9.75 6 9.75ZM10.5 9.75C9.675 9.75 9 10.425 9 11.25C9 12.075 9.675 12.75 10.5 12.75C11.325 12.75 12 12.075 12 11.25C12 10.425 11.325 9.75 10.5 9.75ZM10.5 5.25C9.675 5.25 9 5.925 9 6.75C9 7.575 9.675 8.25 10.5 8.25C11.325 8.25 12 7.575 12 6.75C12 5.925 11.325 5.25 10.5 5.25ZM6 5.25C5.175 5.25 4.5 5.925 4.5 6.75C4.5 7.575 5.175 8.25 6 8.25C6.825 8.25 7.5 7.575 7.5 6.75C7.5 5.925 6.825 5.25 6 5.25ZM6 0.75C5.175 0.75 4.5 1.425 4.5 2.25C4.5 3.075 5.175 3.75 6 3.75C6.825 3.75 7.5 3.075 7.5 2.25C7.5 1.425 6.825 0.75 6 0.75Z" fill="#737D8C"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
res/img/element-icons/message/chevron-up.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-up"><polyline points="18 15 12 9 6 15"></polyline></svg>
|
After Width: | Height: | Size: 268 B |
1
res/img/element-icons/message/corner-up-right.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-up-right"><polyline points="15 14 20 9 15 4"></polyline><path d="M4 20v-7a4 4 0 0 1 4-4h12"></path></svg>
|
After Width: | Height: | Size: 316 B |
3
res/img/element-icons/message/fwd.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.9454 4.27941C10.653 3.98601 10.6539 3.51114 10.9472 3.21875C11.2406 2.92637 11.7155 2.92719 12.0079 3.22059L15.5312 6.75612C15.8229 7.0488 15.8229 7.52226 15.5312 7.81494L12.0079 11.3505C11.7155 11.6439 11.2407 11.6447 10.9473 11.3523C10.6539 11.06 10.653 10.5851 10.9454 10.2917L13.2292 8H6.36588C4.95064 8 3.75282 9.20272 3.75282 10.75C3.75282 12.2973 4.95064 13.5 6.36588 13.5H7.93524C8.34945 13.5 8.68524 13.8358 8.68524 14.25C8.68524 14.6642 8.34945 15 7.93524 15H6.36588C4.06634 15 2.25282 13.0687 2.25282 10.75C2.25282 8.43128 4.06634 6.5 6.36588 6.5H13.1583L10.9454 4.27941Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 755 B |
1
res/img/element-icons/message/link.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
After Width: | Height: | Size: 371 B |
1
res/img/element-icons/message/repeat.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-repeat"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>
|
After Width: | Height: | Size: 392 B |
1
res/img/element-icons/message/share.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-share"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line></svg>
|
After Width: | Height: | Size: 364 B |
|
@ -1,3 +1,3 @@
|
|||
<svg width="12" height="17" viewBox="0 0 12 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.857143 14.5C0.857143 15.4491 1.62857 16.5 2.57143 16.5H9.42857C10.3714 16.5 11.1429 15.2542 11.1429 14.3051V5.67692C11.1429 4.72781 10.3714 3.95128 9.42857 3.95128H2.57143C1.62857 3.95128 0.857143 4.72781 0.857143 5.67692V14.5ZM11.1429 1.36282H9L8.39143 0.750218C8.23714 0.59491 8.01429 0.5 7.79143 0.5H4.20857C3.98571 0.5 3.76286 0.59491 3.60857 0.750218L3 1.36282H0.857143C0.385714 1.36282 0 1.75109 0 2.22564C0 2.70019 0.385714 3.08846 0.857143 3.08846H11.1429C11.6143 3.08846 12 2.70019 12 2.22564C12 1.75109 11.6143 1.36282 11.1429 1.36282Z" fill="#737D8C"/>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 19C6 20.1 6.9 21 8 21H16C17.1 21 18 20.1 18 19V9C18 7.9 17.1 7 16 7H8C6.9 7 6 7.9 6 9V19ZM18 4H15.5L14.79 3.29C14.61 3.11 14.35 3 14.09 3H9.91C9.65 3 9.39 3.11 9.21 3.29L8.5 4H6C5.45 4 5 4.45 5 5C5 5.55 5.45 6 6 6H18C18.55 6 19 5.55 19 5C19 4.45 18.55 4 18 4Z" fill="#8D99A5"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 679 B After Width: | Height: | Size: 397 B |
|
@ -1,5 +1,32 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="8" fill="#FF4B55"/>
|
||||
<rect x="7" y="3" width="2" height="6" rx="1" fill="white"/>
|
||||
<rect x="7" y="11" width="2" height="2" rx="1" fill="white"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg8"
|
||||
version="1.1"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24">
|
||||
<metadata
|
||||
id="metadata14">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs12" />
|
||||
<path
|
||||
id="path2"
|
||||
d="M 12 2 C 6.47715 2 2 6.47715 2 12 C 2 17.5228 6.47715 22 12 22 C 17.5228 22 22 17.5228 22 12 C 22 6.47715 17.5228 2 12 2 z M 11.880859 5.5039062 C 12.720859 5.4439063 13.470547 6.0746875 13.560547 6.9296875 L 13.560547 7.1699219 L 13.080078 13.169922 C 13.035078 13.724922 12.570625 14.144531 12.015625 14.144531 L 11.925781 14.144531 C 11.400781 14.099531 10.996172 13.694922 10.951172 13.169922 L 10.470703 7.1699219 C 10.395703 6.3149219 11.025859 5.5639064 11.880859 5.5039062 z M 12 15.763672 C 12.729 15.763672 13.320312 16.354884 13.320312 17.083984 C 13.320313 17.812984 12.729 18.404297 12 18.404297 C 11.271 18.404297 10.679688 17.812984 10.679688 17.083984 C 10.679688 16.354884 11.271 15.763672 12 15.763672 z "
|
||||
style="fill:#ff4b55;fill-opacity:1" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 283 B After Width: | Height: | Size: 1.5 KiB |
|
@ -215,8 +215,6 @@ $message-body-panel-icon-fg-color: #21262C; // "Separator"
|
|||
$message-body-panel-icon-bg-color: $tertiary-fg-color;
|
||||
|
||||
$voice-record-stop-border-color: $quaternary-fg-color;
|
||||
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
||||
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||
$voice-record-icon-color: $quaternary-fg-color;
|
||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||
|
|
|
@ -20,6 +20,9 @@ $tertiary-fg-color: $primary-fg-color;
|
|||
$primary-bg-color: $bg-color;
|
||||
$muted-fg-color: $header-panel-text-primary-color;
|
||||
|
||||
// Legacy theme backports
|
||||
$quaternary-fg-color: #6F7882;
|
||||
|
||||
// used for dialog box text
|
||||
$light-fg-color: $header-panel-text-secondary-color;
|
||||
|
||||
|
@ -209,8 +212,6 @@ $message-body-panel-icon-bg-color: $secondary-fg-color;
|
|||
|
||||
// See non-legacy dark for variable information
|
||||
$voice-record-stop-border-color: #6F7882;
|
||||
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
||||
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: #6F7882;
|
||||
$voice-record-icon-color: #6F7882;
|
||||
$voice-playback-button-bg-color: $tertiary-fg-color;
|
||||
|
|
|
@ -28,6 +28,9 @@ $tertiary-fg-color: $primary-fg-color;
|
|||
$primary-bg-color: #ffffff;
|
||||
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
|
||||
|
||||
// Legacy theme backports
|
||||
$quaternary-fg-color: #C1C6CD;
|
||||
|
||||
// used for dialog box text
|
||||
$light-fg-color: #747474;
|
||||
|
||||
|
@ -334,8 +337,6 @@ $message-body-panel-icon-bg-color: $primary-bg-color;
|
|||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
$voice-record-live-circle-color: #ff4b55;
|
||||
$voice-record-stop-border-color: #E3E8F0;
|
||||
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
||||
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
|
||||
$voice-record-icon-color: $tertiary-fg-color;
|
||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||
|
|
|
@ -335,8 +335,6 @@ $voice-record-stop-symbol-color: #ff4b55;
|
|||
$voice-record-live-circle-color: #ff4b55;
|
||||
|
||||
$voice-record-stop-border-color: #E3E8F0; // "Separator"
|
||||
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
||||
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||
$voice-record-icon-color: $tertiary-fg-color;
|
||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||
|
|
|
@ -5,4 +5,4 @@ FROM node:14-buster
|
|||
RUN apt-get update
|
||||
RUN apt-get -y install jq build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime
|
||||
# dependencies for chrome (installed by puppeteer)
|
||||
RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
|
||||
RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm-dev libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# generates .eslintignore.errorfiles to list the files which have errors in,
|
||||
# so that they can be ignored in future automated linting.
|
||||
|
||||
out=.eslintignore.errorfiles
|
||||
|
||||
cd `dirname $0`/..
|
||||
|
||||
echo "generating $out"
|
||||
|
||||
{
|
||||
cat <<EOF
|
||||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
||||
|
||||
EOF
|
||||
|
||||
./node_modules/.bin/eslint -f json src test |
|
||||
jq -r '.[] | select((.errorCount + .warningCount) > 0) | .filePath' |
|
||||
sed -e 's/.*matrix-react-sdk\///';
|
||||
} > "$out"
|
||||
# also append rules from eslintignore file
|
||||
cat .eslintignore >> $out
|
14
src/@types/global.d.ts
vendored
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
|
||||
import * as ModernizrStatic from "modernizr";
|
||||
|
||||
import ContentMessages from "../ContentMessages";
|
||||
import { IMatrixClientPeg } from "../MatrixClientPeg";
|
||||
import ToastStore from "../stores/ToastStore";
|
||||
|
@ -127,11 +128,24 @@ declare global {
|
|||
setSinkId(outputId: string);
|
||||
}
|
||||
|
||||
// Add Chrome-specific `instant` ScrollBehaviour
|
||||
type _ScrollBehavior = ScrollBehavior | "instant";
|
||||
|
||||
interface _ScrollOptions {
|
||||
behavior?: _ScrollBehavior;
|
||||
}
|
||||
|
||||
interface _ScrollIntoViewOptions extends _ScrollOptions {
|
||||
block?: ScrollLogicalPosition;
|
||||
inline?: ScrollLogicalPosition;
|
||||
}
|
||||
|
||||
interface Element {
|
||||
// Safari & IE11 only have this prefixed: we used prefixed versions
|
||||
// previously so let's continue to support them for now
|
||||
webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
|
||||
msRequestFullscreen(options?: FullscreenOptions): Promise<void>;
|
||||
scrollIntoView(arg?: boolean | _ScrollIntoViewOptions): void;
|
||||
}
|
||||
|
||||
interface Error {
|
||||
|
|
|
@ -189,7 +189,6 @@ export default class AddThreepid {
|
|||
// pop up an interactive auth dialog
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
|
||||
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("Use Single Sign On to continue"),
|
||||
|
|
|
@ -348,7 +348,7 @@ export default abstract class BasePlatform {
|
|||
/**
|
||||
* Create and store a pickle key for encrypting libolm objects.
|
||||
* @param {string} userId the user ID for the user that the pickle key is for.
|
||||
* @param {string} userId the device ID that the pickle key is for.
|
||||
* @param {string} deviceId the device ID that the pickle key is for.
|
||||
* @returns {string|null} the pickle key, or null if the platform does not
|
||||
* support storing pickle keys.
|
||||
*/
|
||||
|
|
|
@ -80,7 +80,7 @@ import CountlyAnalytics from "./CountlyAnalytics";
|
|||
import { UIFeature } from "./settings/UIFeature";
|
||||
import { CallError } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker"
|
||||
import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker";
|
||||
import { Action } from './dispatcher/actions';
|
||||
import VoipUserMapper from './VoipUserMapper';
|
||||
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
|
||||
|
@ -124,9 +124,9 @@ interface ThirdpartyLookupResponseFields {
|
|||
}
|
||||
|
||||
interface ThirdpartyLookupResponse {
|
||||
userid: string,
|
||||
protocol: string,
|
||||
fields: ThirdpartyLookupResponseFields,
|
||||
userid: string;
|
||||
protocol: string;
|
||||
fields: ThirdpartyLookupResponseFields;
|
||||
}
|
||||
|
||||
// Unlike 'CallType' in js-sdk, this one includes screen sharing
|
||||
|
@ -166,7 +166,7 @@ export default class CallHandler extends EventEmitter {
|
|||
|
||||
static sharedInstance() {
|
||||
if (!window.mxCallHandler) {
|
||||
window.mxCallHandler = new CallHandler()
|
||||
window.mxCallHandler = new CallHandler();
|
||||
}
|
||||
|
||||
return window.mxCallHandler;
|
||||
|
@ -185,7 +185,7 @@ export default class CallHandler extends EventEmitter {
|
|||
const nativeUser = this.assertedIdentityNativeUsers[call.callId];
|
||||
if (nativeUser) {
|
||||
const room = findDMForUser(MatrixClientPeg.get(), nativeUser);
|
||||
if (room) return room.roomId
|
||||
if (room) return room.roomId;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -299,7 +299,7 @@ export default class CallHandler extends EventEmitter {
|
|||
action: 'incoming_call',
|
||||
call: call,
|
||||
}, true);
|
||||
}
|
||||
};
|
||||
|
||||
getCallForRoom(roomId: string): MatrixCall {
|
||||
return this.calls.get(roomId) || null;
|
||||
|
@ -816,7 +816,7 @@ export default class CallHandler extends EventEmitter {
|
|||
|
||||
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.setCallListeners(call);
|
||||
|
||||
|
@ -872,7 +872,7 @@ export default class CallHandler extends EventEmitter {
|
|||
this.dialNumber(payload.number);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private async dialNumber(number: string) {
|
||||
const results = await this.pstnLookup(number);
|
||||
|
|
|
@ -17,9 +17,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import { encode } from "blurhash";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
|
@ -47,6 +48,10 @@ const MAX_HEIGHT = 600;
|
|||
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
||||
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
|
||||
|
||||
export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448
|
||||
const BLURHASH_X_COMPONENTS = 6;
|
||||
const BLURHASH_Y_COMPONENTS = 6;
|
||||
|
||||
export class UploadCanceledError extends Error {}
|
||||
|
||||
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
||||
|
@ -77,6 +82,7 @@ interface IThumbnail {
|
|||
};
|
||||
w: number;
|
||||
h: number;
|
||||
[BLURHASH_FIELD]: string;
|
||||
};
|
||||
thumbnail: Blob;
|
||||
}
|
||||
|
@ -124,7 +130,16 @@ function createThumbnail(
|
|||
const canvas = document.createElement("canvas");
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight);
|
||||
const context = canvas.getContext("2d");
|
||||
context.drawImage(element, 0, 0, targetWidth, targetHeight);
|
||||
const imageData = context.getImageData(0, 0, targetWidth, targetHeight);
|
||||
const blurhash = encode(
|
||||
imageData.data,
|
||||
imageData.width,
|
||||
imageData.height,
|
||||
BLURHASH_X_COMPONENTS,
|
||||
BLURHASH_Y_COMPONENTS,
|
||||
);
|
||||
canvas.toBlob(function(thumbnail) {
|
||||
resolve({
|
||||
info: {
|
||||
|
@ -136,8 +151,9 @@ function createThumbnail(
|
|||
},
|
||||
w: inputWidth,
|
||||
h: inputHeight,
|
||||
[BLURHASH_FIELD]: blurhash,
|
||||
},
|
||||
thumbnail: thumbnail,
|
||||
thumbnail,
|
||||
});
|
||||
}, mimeType);
|
||||
});
|
||||
|
@ -220,7 +236,8 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Load a file into a newly created video element.
|
||||
* Load a file into a newly created video element and pull some strings
|
||||
* in an attempt to guarantee the first frame will be showing.
|
||||
*
|
||||
* @param {File} videoFile The file to load in an video element.
|
||||
* @return {Promise} A promise that resolves with the video image element.
|
||||
|
@ -229,20 +246,25 @@ function loadVideoElement(videoFile): Promise<HTMLVideoElement> {
|
|||
return new Promise((resolve, reject) => {
|
||||
// Load the file into an html element
|
||||
const video = document.createElement("video");
|
||||
video.preload = "metadata";
|
||||
video.playsInline = true;
|
||||
video.muted = true;
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(ev) {
|
||||
video.src = ev.target.result as string;
|
||||
|
||||
// Once ready, returns its size
|
||||
// Wait until we have enough data to thumbnail the first frame.
|
||||
video.onloadeddata = function() {
|
||||
video.onloadeddata = async function() {
|
||||
resolve(video);
|
||||
video.pause();
|
||||
};
|
||||
video.onerror = function(e) {
|
||||
reject(e);
|
||||
};
|
||||
|
||||
video.src = ev.target.result as string;
|
||||
video.load();
|
||||
video.play();
|
||||
};
|
||||
reader.onerror = function(e) {
|
||||
reject(e);
|
||||
|
@ -307,7 +329,7 @@ function readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
|
|||
* 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.
|
||||
*/
|
||||
function uploadFile(
|
||||
export function uploadFile(
|
||||
matrixClient: MatrixClient,
|
||||
roomId: string,
|
||||
file: File | Blob,
|
||||
|
@ -347,7 +369,7 @@ function uploadFile(
|
|||
});
|
||||
(prom as IAbortablePromise<any>).abort = () => {
|
||||
canceled = true;
|
||||
if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise);
|
||||
if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
|
||||
};
|
||||
return prom;
|
||||
} else {
|
||||
|
@ -357,11 +379,11 @@ function uploadFile(
|
|||
const promise1 = basePromise.then(function(url) {
|
||||
if (canceled) throw new UploadCanceledError();
|
||||
// If the attachment isn't encrypted then include the URL directly.
|
||||
return {"url": url};
|
||||
return { url };
|
||||
});
|
||||
(promise1 as any).abort = () => {
|
||||
canceled = true;
|
||||
MatrixClientPeg.get().cancelUpload(basePromise);
|
||||
matrixClient.cancelUpload(basePromise);
|
||||
};
|
||||
return promise1;
|
||||
}
|
||||
|
@ -373,7 +395,7 @@ export default class ContentMessages {
|
|||
|
||||
sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const prom = MatrixClientPeg.get().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);
|
||||
throw e;
|
||||
});
|
||||
|
@ -415,7 +437,7 @@ export default class ContentMessages {
|
|||
|
||||
if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to
|
||||
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
||||
await this.ensureMediaConfigFetched();
|
||||
await this.ensureMediaConfigFetched(matrixClient);
|
||||
modal.close();
|
||||
}
|
||||
|
||||
|
@ -470,7 +492,7 @@ export default class ContentMessages {
|
|||
return this.inprogress.filter(u => !u.canceled);
|
||||
}
|
||||
|
||||
cancelUpload(promise: Promise<any>) {
|
||||
cancelUpload(promise: Promise<any>, matrixClient: MatrixClient) {
|
||||
let upload: IUpload;
|
||||
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||
if (this.inprogress[i].promise === promise) {
|
||||
|
@ -480,7 +502,7 @@ export default class ContentMessages {
|
|||
}
|
||||
if (upload) {
|
||||
upload.canceled = true;
|
||||
MatrixClientPeg.get().cancelUpload(upload.promise);
|
||||
matrixClient.cancelUpload(upload.promise);
|
||||
dis.dispatch<UploadCanceledPayload>({ action: Action.UploadCanceled, upload });
|
||||
}
|
||||
}
|
||||
|
@ -621,11 +643,11 @@ export default class ContentMessages {
|
|||
return true;
|
||||
}
|
||||
|
||||
private ensureMediaConfigFetched() {
|
||||
private ensureMediaConfigFetched(matrixClient: MatrixClient) {
|
||||
if (this.mediaConfig !== null) return;
|
||||
|
||||
console.log("[Media Config] Fetching");
|
||||
return MatrixClientPeg.get().getMediaConfig().then((config) => {
|
||||
return matrixClient.getMediaConfig().then((config) => {
|
||||
console.log("[Media Config] Fetched config:", config);
|
||||
return config;
|
||||
}).catch(() => {
|
||||
|
|
|
@ -15,12 +15,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { IContent } from "matrix-js-sdk/src/models/event";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { getCurrentLanguage } from './languageHandler';
|
||||
import PlatformPeg from './PlatformPeg';
|
||||
import SdkConfig from './SdkConfig';
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import {sleep} from "./utils/promise";
|
||||
import RoomViewStore from "./stores/RoomViewStore";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
|
||||
|
@ -255,7 +256,7 @@ interface ICreateRoomEvent extends IEvent {
|
|||
num_users: number;
|
||||
is_encrypted: boolean;
|
||||
is_public: boolean;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface IJoinRoomEvent extends IEvent {
|
||||
|
@ -338,8 +339,8 @@ const getRoomStats = (roomId: string) => {
|
|||
"is_encrypted": cli?.isRoomEncrypted(roomId),
|
||||
// eslint-disable-next-line camelcase
|
||||
"is_public": room?.currentState.getStateEvents("m.room.join_rules", "")?.getContent()?.join_rule === "public",
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// async wrapper for regex-powered String.prototype.replace
|
||||
const strReplaceAsync = async (str: string, regex: RegExp, fn: (...args: string[]) => Promise<string>) => {
|
||||
|
@ -414,7 +415,7 @@ export default class CountlyAnalytics {
|
|||
|
||||
this.anonymous = anonymous;
|
||||
if (anonymous) {
|
||||
await this.changeUserKey(randomString(64))
|
||||
await this.changeUserKey(randomString(64));
|
||||
} else {
|
||||
await this.changeUserKey(await hashHex(MatrixClientPeg.get().getUserId()), true);
|
||||
}
|
||||
|
@ -438,7 +439,7 @@ export default class CountlyAnalytics {
|
|||
await this.track("Opt-Out" );
|
||||
this.endSession();
|
||||
window.clearInterval(this.heartbeatIntervalId);
|
||||
window.clearTimeout(this.activityIntervalId)
|
||||
window.clearTimeout(this.activityIntervalId);
|
||||
this.baseUrl = null;
|
||||
// remove listeners bound in trackSessions()
|
||||
window.removeEventListener("beforeunload", this.endSession);
|
||||
|
@ -669,7 +670,7 @@ export default class CountlyAnalytics {
|
|||
count,
|
||||
platform: this.appPlatform,
|
||||
app_version: this.appVersion,
|
||||
}
|
||||
};
|
||||
|
||||
this.pendingEvents.push(ev);
|
||||
if (this.pendingEvents.length > MAX_PENDING_EVENTS) {
|
||||
|
@ -680,7 +681,7 @@ export default class CountlyAnalytics {
|
|||
private getOrientation = (): Orientation => {
|
||||
return window.matchMedia("(orientation: landscape)").matches
|
||||
? Orientation.Landscape
|
||||
: Orientation.Portrait
|
||||
: Orientation.Portrait;
|
||||
};
|
||||
|
||||
private reportOrientation = () => {
|
||||
|
@ -749,7 +750,7 @@ export default class CountlyAnalytics {
|
|||
const request: Parameters<typeof CountlyAnalytics.prototype.request>[0] = {
|
||||
begin_session: 1,
|
||||
user_details: JSON.stringify(userDetails),
|
||||
}
|
||||
};
|
||||
|
||||
const metrics = this.getMetrics();
|
||||
if (metrics) {
|
||||
|
@ -773,7 +774,7 @@ export default class CountlyAnalytics {
|
|||
|
||||
private endSession = () => {
|
||||
if (this.sessionStarted) {
|
||||
window.removeEventListener("resize", this.reportOrientation)
|
||||
window.removeEventListener("resize", this.reportOrientation);
|
||||
|
||||
this.reportViewDuration();
|
||||
this.request({
|
||||
|
@ -868,7 +869,7 @@ export default class CountlyAnalytics {
|
|||
roomId: string,
|
||||
isEdit: boolean,
|
||||
isReply: boolean,
|
||||
content: {format?: string, msgtype: string},
|
||||
content: IContent,
|
||||
) {
|
||||
if (this.disabled) return;
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
|
|
@ -138,7 +138,7 @@ export function getHtmlText(insaneHtml: string): string {
|
|||
selfClosing: [],
|
||||
allowedSchemes: [],
|
||||
disallowedTagsMode: 'discard',
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -358,11 +358,11 @@ interface IOpts {
|
|||
stripReplyFallback?: boolean;
|
||||
returnString?: boolean;
|
||||
forComposerQuote?: boolean;
|
||||
ref?: React.Ref<any>;
|
||||
ref?: React.Ref<HTMLSpanElement>;
|
||||
}
|
||||
|
||||
export interface IOptsReturnNode extends IOpts {
|
||||
returnString: false;
|
||||
returnString: false | undefined;
|
||||
}
|
||||
|
||||
export interface IOptsReturnString extends IOpts {
|
||||
|
@ -403,9 +403,14 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
|
|||
try {
|
||||
if (highlights && highlights.length > 0) {
|
||||
const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
||||
const safeHighlights = highlights.map(function(highlight) {
|
||||
return sanitizeHtml(highlight, sanitizeParams);
|
||||
});
|
||||
const safeHighlights = highlights
|
||||
// sanitizeHtml can hang if an unclosed HTML tag is thrown at it
|
||||
// A search for `<foo` will make the browser crash
|
||||
// an alternative would be to escape HTML special characters
|
||||
// but that would bring no additional benefit as the highlighter
|
||||
// does not work with those special chars
|
||||
.filter((highlight: string): boolean => !highlight.includes("<"))
|
||||
.map((highlight: string): string => sanitizeHtml(highlight, sanitizeParams));
|
||||
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure.
|
||||
sanitizeParams.textFilter = function(safeText) {
|
||||
return highlighter.applyHighlights(safeText, safeHighlights).join('');
|
||||
|
|
|
@ -156,7 +156,7 @@ const messageComposerBindings = (): KeyBinding<MessageComposerAction>[] => {
|
|||
}
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
};
|
||||
|
||||
const autocompleteBindings = (): KeyBinding<AutocompleteAction>[] => {
|
||||
return [
|
||||
|
@ -207,7 +207,7 @@ const autocompleteBindings = (): KeyBinding<AutocompleteAction>[] => {
|
|||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const roomListBindings = (): KeyBinding<RoomListAction>[] => {
|
||||
return [
|
||||
|
@ -248,7 +248,7 @@ const roomListBindings = (): KeyBinding<RoomListAction>[] => {
|
|||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const roomBindings = (): KeyBinding<RoomAction>[] => {
|
||||
const bindings: KeyBinding<RoomAction>[] = [
|
||||
|
@ -312,7 +312,7 @@ const roomBindings = (): KeyBinding<RoomAction>[] => {
|
|||
}
|
||||
|
||||
return bindings;
|
||||
}
|
||||
};
|
||||
|
||||
const navigationBindings = (): KeyBinding<NavigationAction>[] => {
|
||||
return [
|
||||
|
@ -396,7 +396,7 @@ const navigationBindings = (): KeyBinding<NavigationAction>[] => {
|
|||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
export const defaultBindingsProvider: IKeyBindingsProvider = {
|
||||
getMessageComposerBindings: messageComposerBindings,
|
||||
|
@ -404,4 +404,4 @@ export const defaultBindingsProvider: IKeyBindingsProvider = {
|
|||
getRoomListBindings: roomListBindings,
|
||||
getRoomBindings: roomBindings,
|
||||
getNavigationBindings: navigationBindings,
|
||||
}
|
||||
};
|
||||
|
|
|
@ -140,12 +140,12 @@ export type KeyCombo = {
|
|||
ctrlKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
shiftKey?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export type KeyBinding<T extends string> = {
|
||||
action: T;
|
||||
keyCombo: KeyCombo;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper method to check if a KeyboardEvent matches a KeyCombo
|
||||
|
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
|||
import { createClient } from 'matrix-js-sdk/src/matrix';
|
||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import {decryptAES, encryptAES} from "matrix-js-sdk/src/crypto/aes";
|
||||
import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes";
|
||||
|
||||
import { IMatrixClientCreds, MatrixClientPeg } from './MatrixClientPeg';
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
|
@ -303,7 +303,7 @@ export interface IStoredSession {
|
|||
hsUrl: string;
|
||||
isUrl: string;
|
||||
hasAccessToken: boolean;
|
||||
accessToken: string | object;
|
||||
accessToken: string | IEncryptedPayload;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
isGuest: boolean;
|
||||
|
|
|
@ -190,7 +190,6 @@ export default class Login {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send a login request to the given server, and format the response
|
||||
* as a MatrixClientCreds
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -22,9 +23,17 @@ const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
|||
// These types of node are definitely text
|
||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||
|
||||
function is_allowed_html_tag(node) {
|
||||
// As far as @types/commonmark is concerned, these are not public, so add them
|
||||
interface CommonmarkHtmlRendererInternal extends commonmark.HtmlRenderer {
|
||||
paragraph: (node: commonmark.Node, entering: boolean) => void;
|
||||
link: (node: commonmark.Node, entering: boolean) => void;
|
||||
html_inline: (node: commonmark.Node) => void; // eslint-disable-line camelcase
|
||||
html_block: (node: commonmark.Node) => void; // eslint-disable-line camelcase
|
||||
}
|
||||
|
||||
function isAllowedHtmlTag(node: commonmark.Node): boolean {
|
||||
if (node.literal != null &&
|
||||
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$') != null) {
|
||||
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -39,21 +48,12 @@ function is_allowed_html_tag(node) {
|
|||
return false;
|
||||
}
|
||||
|
||||
function html_if_tag_allowed(node) {
|
||||
if (is_allowed_html_tag(node)) {
|
||||
this.lit(node.literal);
|
||||
return;
|
||||
} else {
|
||||
this.lit(escape(node.literal));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns true if the parse output containing the node
|
||||
* comprises multiple block level elements (ie. lines),
|
||||
* or false if it is only a single line.
|
||||
*/
|
||||
function is_multi_line(node) {
|
||||
function isMultiLine(node: commonmark.Node): boolean {
|
||||
let par = node;
|
||||
while (par.parent) {
|
||||
par = par.parent;
|
||||
|
@ -67,6 +67,9 @@ function is_multi_line(node) {
|
|||
* it's plain text.
|
||||
*/
|
||||
export default class Markdown {
|
||||
private input: string;
|
||||
private parsed: commonmark.Node;
|
||||
|
||||
constructor(input) {
|
||||
this.input = input;
|
||||
|
||||
|
@ -74,7 +77,7 @@ export default class Markdown {
|
|||
this.parsed = parser.parse(this.input);
|
||||
}
|
||||
|
||||
isPlainText() {
|
||||
isPlainText(): boolean {
|
||||
const walker = this.parsed.walker();
|
||||
|
||||
let ev;
|
||||
|
@ -87,7 +90,7 @@ export default class Markdown {
|
|||
// if it's an allowed html tag, we need to render it and therefore
|
||||
// we will need to use HTML. If it's not allowed, it's not HTML since
|
||||
// we'll just be treating it as text.
|
||||
if (is_allowed_html_tag(node)) {
|
||||
if (isAllowedHtmlTag(node)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
|
@ -97,7 +100,7 @@ export default class Markdown {
|
|||
return true;
|
||||
}
|
||||
|
||||
toHTML({ externalLinks = false } = {}) {
|
||||
toHTML({ externalLinks = false } = {}): string {
|
||||
const renderer = new commonmark.HtmlRenderer({
|
||||
safe: false,
|
||||
|
||||
|
@ -107,7 +110,7 @@ export default class Markdown {
|
|||
// block quote ends up all on one line
|
||||
// (https://github.com/vector-im/element-web/issues/3154)
|
||||
softbreak: '<br />',
|
||||
});
|
||||
}) as CommonmarkHtmlRendererInternal;
|
||||
|
||||
// Trying to strip out the wrapping <p/> causes a lot more complication
|
||||
// than it's worth, i think. For instance, this code will go and strip
|
||||
|
@ -118,16 +121,16 @@ export default class Markdown {
|
|||
//
|
||||
// Let's try sending with <p/>s anyway for now, though.
|
||||
|
||||
const real_paragraph = renderer.paragraph;
|
||||
const realParagraph = renderer.paragraph;
|
||||
|
||||
renderer.paragraph = function(node, entering) {
|
||||
renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
|
||||
// If there is only one top level node, just return the
|
||||
// bare text: it's a single line of text and so should be
|
||||
// 'inline', rather than unnecessarily wrapped in its own
|
||||
// p tag. If, however, we have multiple nodes, each gets
|
||||
// its own p tag to keep them as separate paragraphs.
|
||||
if (is_multi_line(node)) {
|
||||
real_paragraph.call(this, node, entering);
|
||||
if (isMultiLine(node)) {
|
||||
realParagraph.call(this, node, entering);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -150,16 +153,23 @@ export default class Markdown {
|
|||
}
|
||||
};
|
||||
|
||||
renderer.html_inline = html_if_tag_allowed;
|
||||
renderer.html_inline = function(node: commonmark.Node) {
|
||||
if (isAllowedHtmlTag(node)) {
|
||||
this.lit(node.literal);
|
||||
return;
|
||||
} else {
|
||||
this.lit(escape(node.literal));
|
||||
}
|
||||
};
|
||||
|
||||
renderer.html_block = function(node) {
|
||||
renderer.html_block = function(node: commonmark.Node) {
|
||||
/*
|
||||
// as with `paragraph`, we only insert line breaks
|
||||
// if there are multiple lines in the markdown.
|
||||
const isMultiLine = is_multi_line(node);
|
||||
if (isMultiLine) this.cr();
|
||||
*/
|
||||
html_if_tag_allowed.call(this, node);
|
||||
renderer.html_inline(node);
|
||||
/*
|
||||
if (isMultiLine) this.cr();
|
||||
*/
|
||||
|
@ -177,23 +187,22 @@ export default class Markdown {
|
|||
* N.B. this does **NOT** render arbitrary MD to plain text - only MD
|
||||
* which has no formatting. Otherwise it emits HTML(!).
|
||||
*/
|
||||
toPlaintext() {
|
||||
const renderer = new commonmark.HtmlRenderer({safe: false});
|
||||
const real_paragraph = renderer.paragraph;
|
||||
toPlaintext(): string {
|
||||
const renderer = new commonmark.HtmlRenderer({ safe: false }) as CommonmarkHtmlRendererInternal;
|
||||
|
||||
renderer.paragraph = function(node, entering) {
|
||||
renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
|
||||
// as with toHTML, only append lines to paragraphs if there are
|
||||
// multiple paragraphs
|
||||
if (is_multi_line(node)) {
|
||||
if (isMultiLine(node)) {
|
||||
if (!entering && node.next) {
|
||||
this.lit('\n\n');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderer.html_block = function(node) {
|
||||
renderer.html_block = function(node: commonmark.Node) {
|
||||
this.lit(node.literal);
|
||||
if (is_multi_line(node) && node.next) this.lit('\n\n');
|
||||
if (isMultiLine(node) && node.next) this.lit('\n\n');
|
||||
};
|
||||
|
||||
return renderer.render(this.parsed);
|
|
@ -15,14 +15,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import { defer } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import Analytics from './Analytics';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import {defer} from './utils/promise';
|
||||
import AsyncWrapper from './AsyncWrapper';
|
||||
|
||||
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
||||
|
|
|
@ -68,7 +68,7 @@ export const Notifier = {
|
|||
// or not
|
||||
pendingEncryptedEventIds: [],
|
||||
|
||||
notificationMessageForEvent: function(ev: MatrixEvent) {
|
||||
notificationMessageForEvent: function(ev: MatrixEvent): string {
|
||||
if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) {
|
||||
return typehandlers[ev.getContent().msgtype](ev);
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ class Presence {
|
|||
this.setState(State.Online);
|
||||
this.unavailableTimer.restart();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the presence state.
|
||||
|
|
|
@ -104,7 +104,6 @@ export function setDMRoom(roomId: string, userId: string): Promise<void> {
|
|||
dmRoomMap[userId] = roomList;
|
||||
}
|
||||
|
||||
|
||||
return MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap);
|
||||
}
|
||||
|
||||
|
|
|
@ -208,7 +208,6 @@ Example:
|
|||
]
|
||||
}
|
||||
|
||||
|
||||
membership_state AND bot_options
|
||||
--------------------------------
|
||||
Get the content of the "m.room.member" or "m.room.bot.options" state event respectively.
|
||||
|
|
|
@ -42,8 +42,8 @@ let secretStorageBeingAccessed = false;
|
|||
let nonInteractive = false;
|
||||
|
||||
let dehydrationCache: {
|
||||
key?: Uint8Array,
|
||||
keyInfo?: ISecretStorageKeyInfo,
|
||||
key?: Uint8Array;
|
||||
keyInfo?: ISecretStorageKeyInfo;
|
||||
} = {};
|
||||
|
||||
function isCachingAllowed(): boolean {
|
||||
|
@ -135,7 +135,7 @@ async function getSecretStorageKey(
|
|||
|
||||
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
|
||||
if (keyFromCustomisations) {
|
||||
console.log("Using key from security customisations (secret storage)")
|
||||
console.log("Using key from security customisations (secret storage)");
|
||||
cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations);
|
||||
return [keyId, keyFromCustomisations];
|
||||
}
|
||||
|
@ -185,7 +185,7 @@ export async function getDehydrationKey(
|
|||
): Promise<Uint8Array> {
|
||||
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
|
||||
if (keyFromCustomisations) {
|
||||
console.log("Using key from security customisations (dehydration)")
|
||||
console.log("Using key from security customisations (dehydration)");
|
||||
return keyFromCustomisations;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
//@flow
|
||||
/*
|
||||
Copyright 2017 Aviral Dasgupta
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from
|
|||
import SdkConfig from "./SdkConfig";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { UIFeature } from "./settings/UIFeature";
|
||||
import {CHAT_EFFECTS} from "./effects"
|
||||
import { CHAT_EFFECTS } from "./effects";
|
||||
import CallHandler from "./CallHandler";
|
||||
import { guessAndSetDMRoom } from "./Rooms";
|
||||
|
||||
|
@ -1176,12 +1176,12 @@ export const Commands = [
|
|||
})());
|
||||
},
|
||||
category: CommandCategories.effects,
|
||||
})
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
// build a map from names and aliases to the Command objects.
|
||||
export const CommandMap = new Map();
|
||||
export const CommandMap = new Map<string, Command>();
|
||||
Commands.forEach(cmd => {
|
||||
CommandMap.set(cmd.command, cmd);
|
||||
cmd.aliases.forEach(alias => {
|
||||
|
@ -1189,15 +1189,15 @@ Commands.forEach(cmd => {
|
|||
});
|
||||
});
|
||||
|
||||
export function parseCommandString(input: string) {
|
||||
export function parseCommandString(input: string): { cmd?: string, args?: string } {
|
||||
// trim any trailing whitespace, as it can confuse the parser for
|
||||
// IRC-style commands
|
||||
input = input.replace(/\s+$/, '');
|
||||
if (input[0] !== '/') return {}; // not a command
|
||||
|
||||
const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/);
|
||||
let cmd;
|
||||
let args;
|
||||
let cmd: string;
|
||||
let args: string;
|
||||
if (bits) {
|
||||
cmd = bits[1].substring(1).toLowerCase();
|
||||
args = bits[2];
|
||||
|
@ -1208,6 +1208,11 @@ export function parseCommandString(input: string) {
|
|||
return { cmd, args };
|
||||
}
|
||||
|
||||
interface ICmd {
|
||||
cmd?: Command;
|
||||
args?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the given text for /commands and return a bound method to perform them.
|
||||
* @param {string} roomId The room in which the command was performed.
|
||||
|
@ -1216,7 +1221,7 @@ export function parseCommandString(input: string) {
|
|||
* processing the command, or 'promise' if a request was sent out.
|
||||
* Returns null if the input didn't match a command.
|
||||
*/
|
||||
export function getCommand(input: string) {
|
||||
export function getCommand(input: string): ICmd {
|
||||
const { cmd, args } = parseCommandString(input);
|
||||
|
||||
if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) {
|
||||
|
|
13
src/Terms.ts
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import * as sdk from '.';
|
||||
|
@ -32,7 +33,7 @@ export class Service {
|
|||
* @param {string} baseUrl The Base URL of the service (ie. before '/_matrix')
|
||||
* @param {string} accessToken The user's access token for the service
|
||||
*/
|
||||
constructor(public serviceType: string, public baseUrl: string, public accessToken: string) {
|
||||
constructor(public serviceType: SERVICE_TYPES, public baseUrl: string, public accessToken: string) {
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,13 +49,13 @@ export interface Policy {
|
|||
}
|
||||
|
||||
export type Policies = {
|
||||
[policy: string]: Policy,
|
||||
[policy: string]: Policy;
|
||||
};
|
||||
|
||||
export type TermsInteractionCallback = (
|
||||
policiesAndServicePairs: {
|
||||
service: Service,
|
||||
policies: Policies,
|
||||
service: Service;
|
||||
policies: Policies;
|
||||
}[],
|
||||
agreedUrls: string[],
|
||||
extraClassNames?: string,
|
||||
|
@ -180,8 +181,8 @@ export async function startTermsFlow(
|
|||
|
||||
export function dialogTermsInteractionCallback(
|
||||
policiesAndServicePairs: {
|
||||
service: Service,
|
||||
policies: { [policy: string]: Policy },
|
||||
service: Service;
|
||||
policies: { [policy: string]: Policy };
|
||||
}[],
|
||||
agreedUrls: string[],
|
||||
extraClassNames?: string,
|
||||
|
|
|
@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import { _t } from './languageHandler';
|
||||
import * as Roles from './Roles';
|
||||
|
@ -20,6 +22,11 @@ import {isValid3pidInvite} from "./RoomInvite";
|
|||
import SettingsStore from "./settings/SettingsStore";
|
||||
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 { RightPanelPhases } from './stores/RightPanelStorePhases';
|
||||
import { Action } from './dispatcher/actions';
|
||||
import defaultDispatcher from './dispatcher/dispatcher';
|
||||
import { SetRightPanelPhasePayload } from './dispatcher/payloads/SetRightPanelPhasePayload';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
// 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
|
||||
|
@ -105,7 +112,7 @@ function textForMemberEvent(ev): () => string | null {
|
|||
targetName,
|
||||
reason,
|
||||
})
|
||||
: _t('%(senderName)s withdrew %(targetName)s\'s invitation', { senderName, targetName })
|
||||
: _t('%(senderName)s withdrew %(targetName)s\'s invitation', { senderName, targetName });
|
||||
} else if (prevContent.membership === "join") {
|
||||
return () => reason
|
||||
? _t('%(senderName)s kicked %(targetName)s: %(reason)s', {
|
||||
|
@ -479,8 +486,32 @@ function textForPowerEvent(event): () => string | null {
|
|||
});
|
||||
}
|
||||
|
||||
function textForPinnedEvent(event): () => string | null {
|
||||
const onPinnedMessagesClick = (): void => {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.PinnedMessages,
|
||||
allowClose: false,
|
||||
});
|
||||
};
|
||||
|
||||
function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string | JSX.Element | null {
|
||||
if (!SettingsStore.getValue("feature_pinning")) return null;
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
|
||||
if (allowJSX) {
|
||||
return () => (
|
||||
<span>
|
||||
{
|
||||
_t(
|
||||
"%(senderName)s changed the <a>pinned messages</a> for the room.",
|
||||
{ senderName },
|
||||
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> },
|
||||
)
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return () => _t("%(senderName)s changed the pinned messages for the room.", { senderName });
|
||||
}
|
||||
|
||||
|
@ -607,7 +638,7 @@ function textForMjolnirEvent(event): () => string | null {
|
|||
}
|
||||
|
||||
interface IHandlers {
|
||||
[type: string]: (ev: any) => (() => string | null);
|
||||
[type: string]: (ev: MatrixEvent, allowJSX?: boolean) => (() => string | JSX.Element | null);
|
||||
}
|
||||
|
||||
const handlers: IHandlers = {
|
||||
|
@ -648,7 +679,9 @@ export function hasText(ev): boolean {
|
|||
return Boolean(handler?.(ev));
|
||||
}
|
||||
|
||||
export function textForEvent(ev): string {
|
||||
export function textForEvent(ev: MatrixEvent): string;
|
||||
export function textForEvent(ev: MatrixEvent, allowJSX: true): string | JSX.Element;
|
||||
export function textForEvent(ev: MatrixEvent, allowJSX = false): string | JSX.Element {
|
||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||
return handler?.(ev)?.() || '';
|
||||
return handler?.(ev, allowJSX)?.() || '';
|
||||
}
|
458
src/Tinter.js
|
@ -1,458 +0,0 @@
|
|||
/*
|
||||
Copyright 2015 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const DEBUG = 0;
|
||||
|
||||
// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
|
||||
function colorToRgb(color) {
|
||||
if (!color) {
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
if (color[0] === '#') {
|
||||
color = color.slice(1);
|
||||
if (color.length === 3) {
|
||||
color = color[0] + color[0] +
|
||||
color[1] + color[1] +
|
||||
color[2] + color[2];
|
||||
}
|
||||
const val = parseInt(color, 16);
|
||||
const r = (val >> 16) & 255;
|
||||
const g = (val >> 8) & 255;
|
||||
const b = val & 255;
|
||||
return [r, g, b];
|
||||
} else {
|
||||
const match = color.match(/rgb\((.*?),(.*?),(.*?)\)/);
|
||||
if (match) {
|
||||
return [
|
||||
parseInt(match[1]),
|
||||
parseInt(match[2]),
|
||||
parseInt(match[3]),
|
||||
];
|
||||
}
|
||||
}
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
// utility to turn [red,green,blue] into #rrggbb
|
||||
function rgbToColor(rgb) {
|
||||
const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
|
||||
return '#' + (0x1000000 + val).toString(16).slice(1);
|
||||
}
|
||||
|
||||
class Tinter {
|
||||
constructor() {
|
||||
// The default colour keys to be replaced as referred to in CSS
|
||||
// (should be overridden by .mx_theme_accentColor and .mx_theme_secondaryAccentColor)
|
||||
this.keyRgb = [
|
||||
"rgb(118, 207, 166)", // Vector Green
|
||||
"rgb(234, 245, 240)", // Vector Light Green
|
||||
"rgb(211, 239, 225)", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
|
||||
];
|
||||
|
||||
// Some algebra workings for calculating the tint % of Vector Green & Light Green
|
||||
// x * 118 + (1 - x) * 255 = 234
|
||||
// x * 118 + 255 - 255 * x = 234
|
||||
// x * 118 - x * 255 = 234 - 255
|
||||
// (255 - 118) x = 255 - 234
|
||||
// x = (255 - 234) / (255 - 118) = 0.16
|
||||
|
||||
// The colour keys to be replaced as referred to in SVGs
|
||||
this.keyHex = [
|
||||
"#76CFA6", // Vector Green
|
||||
"#EAF5F0", // Vector Light Green
|
||||
"#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
|
||||
"#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
|
||||
"#000000", // black lowlights of the SVGs (for switching to dark theme)
|
||||
];
|
||||
|
||||
// track the replacement colours actually being used
|
||||
// defaults to our keys.
|
||||
this.colors = [
|
||||
this.keyHex[0],
|
||||
this.keyHex[1],
|
||||
this.keyHex[2],
|
||||
this.keyHex[3],
|
||||
this.keyHex[4],
|
||||
];
|
||||
|
||||
// track the most current tint request inputs (which may differ from the
|
||||
// end result stored in this.colors
|
||||
this.currentTint = [
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
];
|
||||
|
||||
this.cssFixups = [
|
||||
// { theme: {
|
||||
// style: a style object that should be fixed up taken from a stylesheet
|
||||
// attr: name of the attribute to be clobbered, e.g. 'color'
|
||||
// index: ordinal of primary, secondary or tertiary
|
||||
// },
|
||||
// }
|
||||
];
|
||||
|
||||
// CSS attributes to be fixed up
|
||||
this.cssAttrs = [
|
||||
"color",
|
||||
"backgroundColor",
|
||||
"borderColor",
|
||||
"borderTopColor",
|
||||
"borderBottomColor",
|
||||
"borderLeftColor",
|
||||
];
|
||||
|
||||
this.svgAttrs = [
|
||||
"fill",
|
||||
"stroke",
|
||||
];
|
||||
|
||||
// List of functions to call when the tint changes.
|
||||
this.tintables = [];
|
||||
|
||||
// the currently loaded theme (if any)
|
||||
this.theme = undefined;
|
||||
|
||||
// whether to force a tint (e.g. after changing theme)
|
||||
this.forceTint = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback to fire when the tint changes.
|
||||
* This is used to rewrite the tintable SVGs with the new tint.
|
||||
*
|
||||
* It's not possible to unregister a tintable callback. So this can only be
|
||||
* used to register a static callback. If a set of tintables will change
|
||||
* over time then the best bet is to register a single callback for the
|
||||
* entire set.
|
||||
*
|
||||
* To ensure the tintable work happens at least once, it is also called as
|
||||
* part of registration.
|
||||
*
|
||||
* @param {Function} tintable Function to call when the tint changes.
|
||||
*/
|
||||
registerTintable(tintable) {
|
||||
this.tintables.push(tintable);
|
||||
tintable();
|
||||
}
|
||||
|
||||
getKeyRgb() {
|
||||
return this.keyRgb;
|
||||
}
|
||||
|
||||
tint(primaryColor, secondaryColor, tertiaryColor) {
|
||||
return;
|
||||
// eslint-disable-next-line no-unreachable
|
||||
this.currentTint[0] = primaryColor;
|
||||
this.currentTint[1] = secondaryColor;
|
||||
this.currentTint[2] = tertiaryColor;
|
||||
|
||||
this.calcCssFixups();
|
||||
|
||||
if (DEBUG) {
|
||||
console.log("Tinter.tint(" + primaryColor + ", " +
|
||||
secondaryColor + ", " +
|
||||
tertiaryColor + ")");
|
||||
}
|
||||
|
||||
if (!primaryColor) {
|
||||
primaryColor = this.keyRgb[0];
|
||||
secondaryColor = this.keyRgb[1];
|
||||
tertiaryColor = this.keyRgb[2];
|
||||
}
|
||||
|
||||
if (!secondaryColor) {
|
||||
const x = 0.16; // average weighting factor calculated from vector green & light green
|
||||
const rgb = colorToRgb(primaryColor);
|
||||
rgb[0] = x * rgb[0] + (1 - x) * 255;
|
||||
rgb[1] = x * rgb[1] + (1 - x) * 255;
|
||||
rgb[2] = x * rgb[2] + (1 - x) * 255;
|
||||
secondaryColor = rgbToColor(rgb);
|
||||
}
|
||||
|
||||
if (!tertiaryColor) {
|
||||
const x = 0.19;
|
||||
const rgb1 = colorToRgb(primaryColor);
|
||||
const rgb2 = colorToRgb(secondaryColor);
|
||||
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
|
||||
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
|
||||
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
|
||||
tertiaryColor = rgbToColor(rgb1);
|
||||
}
|
||||
|
||||
if (this.forceTint == false &&
|
||||
this.colors[0] === primaryColor &&
|
||||
this.colors[1] === secondaryColor &&
|
||||
this.colors[2] === tertiaryColor) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.forceTint = false;
|
||||
|
||||
this.colors[0] = primaryColor;
|
||||
this.colors[1] = secondaryColor;
|
||||
this.colors[2] = tertiaryColor;
|
||||
|
||||
if (DEBUG) {
|
||||
console.log("Tinter.tint final: (" + primaryColor + ", " +
|
||||
secondaryColor + ", " +
|
||||
tertiaryColor + ")");
|
||||
}
|
||||
|
||||
// go through manually fixing up the stylesheets.
|
||||
this.applyCssFixups();
|
||||
|
||||
// tell all the SVGs to go fix themselves up
|
||||
// we don't do this as a dispatch otherwise it will visually lag
|
||||
this.tintables.forEach(function(tintable) {
|
||||
tintable();
|
||||
});
|
||||
}
|
||||
|
||||
tintSvgWhite(whiteColor) {
|
||||
this.currentTint[3] = whiteColor;
|
||||
|
||||
if (!whiteColor) {
|
||||
whiteColor = this.colors[3];
|
||||
}
|
||||
if (this.colors[3] === whiteColor) {
|
||||
return;
|
||||
}
|
||||
this.colors[3] = whiteColor;
|
||||
this.tintables.forEach(function(tintable) {
|
||||
tintable();
|
||||
});
|
||||
}
|
||||
|
||||
tintSvgBlack(blackColor) {
|
||||
this.currentTint[4] = blackColor;
|
||||
|
||||
if (!blackColor) {
|
||||
blackColor = this.colors[4];
|
||||
}
|
||||
if (this.colors[4] === blackColor) {
|
||||
return;
|
||||
}
|
||||
this.colors[4] = blackColor;
|
||||
this.tintables.forEach(function(tintable) {
|
||||
tintable();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setTheme(theme) {
|
||||
this.theme = theme;
|
||||
|
||||
// update keyRgb from the current theme CSS itself, if it defines it
|
||||
if (document.getElementById('mx_theme_accentColor')) {
|
||||
this.keyRgb[0] = window.getComputedStyle(
|
||||
document.getElementById('mx_theme_accentColor')).color;
|
||||
}
|
||||
if (document.getElementById('mx_theme_secondaryAccentColor')) {
|
||||
this.keyRgb[1] = window.getComputedStyle(
|
||||
document.getElementById('mx_theme_secondaryAccentColor')).color;
|
||||
}
|
||||
if (document.getElementById('mx_theme_tertiaryAccentColor')) {
|
||||
this.keyRgb[2] = window.getComputedStyle(
|
||||
document.getElementById('mx_theme_tertiaryAccentColor')).color;
|
||||
}
|
||||
|
||||
this.calcCssFixups();
|
||||
this.forceTint = true;
|
||||
|
||||
this.tint(this.currentTint[0], this.currentTint[1], this.currentTint[2]);
|
||||
|
||||
if (theme === 'dark') {
|
||||
// abuse the tinter to change all the SVG's #fff to #2d2d2d
|
||||
// XXX: obviously this shouldn't be hardcoded here.
|
||||
this.tintSvgWhite('#2d2d2d');
|
||||
this.tintSvgBlack('#dddddd');
|
||||
} else {
|
||||
this.tintSvgWhite('#ffffff');
|
||||
this.tintSvgBlack('#000000');
|
||||
}
|
||||
}
|
||||
|
||||
calcCssFixups() {
|
||||
// cache our fixups
|
||||
if (this.cssFixups[this.theme]) return;
|
||||
|
||||
if (DEBUG) {
|
||||
console.debug("calcCssFixups start for " + this.theme + " (checking " +
|
||||
document.styleSheets.length +
|
||||
" stylesheets)");
|
||||
}
|
||||
|
||||
this.cssFixups[this.theme] = [];
|
||||
|
||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||
const ss = document.styleSheets[i];
|
||||
try {
|
||||
if (!ss) continue; // well done safari >:(
|
||||
// Chromium apparently sometimes returns null here; unsure why.
|
||||
// see $14534907369972FRXBx:matrix.org in HQ
|
||||
// ...ah, it's because there's a third party extension like
|
||||
// privacybadger inserting its own stylesheet in there with a
|
||||
// resource:// URI or something which results in a XSS error.
|
||||
// See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
|
||||
// ...except some browsers apparently return stylesheets without
|
||||
// hrefs, which we have no choice but ignore right now
|
||||
|
||||
// XXX seriously? we are hardcoding the name of vector's CSS file in
|
||||
// here?
|
||||
//
|
||||
// Why do we need to limit it to vector's CSS file anyway - if there
|
||||
// are other CSS files affecting the doc don't we want to apply the
|
||||
// same transformations to them?
|
||||
//
|
||||
// Iterating through the CSS looking for matches to hack on feels
|
||||
// pretty horrible anyway. And what if the application skin doesn't use
|
||||
// Vector Green as its primary color?
|
||||
// --richvdh
|
||||
|
||||
// Yes, tinting assumes that you are using the Element skin for now.
|
||||
// The right solution will be to move the CSS over to react-sdk.
|
||||
// And yes, the default assets for the base skin might as well use
|
||||
// Vector Green as any other colour.
|
||||
// --matthew
|
||||
|
||||
// stylesheets we don't have permission to access (eg. ones from extensions) have a null
|
||||
// href and will throw exceptions if we try to access their rules.
|
||||
if (!ss.href || !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue;
|
||||
if (ss.disabled) continue;
|
||||
if (!ss.cssRules) continue;
|
||||
|
||||
if (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href);
|
||||
|
||||
for (let j = 0; j < ss.cssRules.length; j++) {
|
||||
const rule = ss.cssRules[j];
|
||||
if (!rule.style) continue;
|
||||
if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
|
||||
for (let k = 0; k < this.cssAttrs.length; k++) {
|
||||
const attr = this.cssAttrs[k];
|
||||
for (let l = 0; l < this.keyRgb.length; l++) {
|
||||
if (rule.style[attr] === this.keyRgb[l]) {
|
||||
this.cssFixups[this.theme].push({
|
||||
style: rule.style,
|
||||
attr: attr,
|
||||
index: l,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Catch any random exceptions that happen here: all sorts of things can go
|
||||
// wrong with this (nulls, SecurityErrors) and mostly it's for other
|
||||
// stylesheets that we don't want to proces anyway. We should not propagate an
|
||||
// exception out since this will cause the app to fail to start.
|
||||
console.log("Failed to calculate CSS fixups for a stylesheet: " + ss.href, e);
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log("calcCssFixups end (" +
|
||||
this.cssFixups[this.theme].length +
|
||||
" fixups)");
|
||||
}
|
||||
}
|
||||
|
||||
applyCssFixups() {
|
||||
if (DEBUG) {
|
||||
console.log("applyCssFixups start (" +
|
||||
this.cssFixups[this.theme].length +
|
||||
" fixups)");
|
||||
}
|
||||
for (let i = 0; i < this.cssFixups[this.theme].length; i++) {
|
||||
const cssFixup = this.cssFixups[this.theme][i];
|
||||
try {
|
||||
cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index];
|
||||
} catch (e) {
|
||||
// Firefox Quantum explodes if you manually edit the CSS in the
|
||||
// inspector and then try to do a tint, as apparently all the
|
||||
// fixups are then stale.
|
||||
console.error("Failed to apply cssFixup in Tinter! ", e.name);
|
||||
}
|
||||
}
|
||||
if (DEBUG) console.log("applyCssFixups end");
|
||||
}
|
||||
|
||||
// XXX: we could just move this all into TintableSvg, but as it's so similar
|
||||
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
|
||||
// keeping it here for now.
|
||||
calcSvgFixups(svgs) {
|
||||
// go through manually fixing up SVG colours.
|
||||
// we could do this by stylesheets, but keeping the stylesheets
|
||||
// updated would be a PITA, so just brute-force search for the
|
||||
// key colour; cache the element and apply.
|
||||
|
||||
if (DEBUG) console.log("calcSvgFixups start for " + svgs);
|
||||
const fixups = [];
|
||||
for (let i = 0; i < svgs.length; i++) {
|
||||
let svgDoc;
|
||||
try {
|
||||
svgDoc = svgs[i].contentDocument;
|
||||
} catch (e) {
|
||||
let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
|
||||
if (e.message) {
|
||||
msg += e.message;
|
||||
}
|
||||
if (e.stack) {
|
||||
msg += ' | stack: ' + e.stack;
|
||||
}
|
||||
console.error(msg);
|
||||
}
|
||||
if (!svgDoc) continue;
|
||||
const tags = svgDoc.getElementsByTagName("*");
|
||||
for (let j = 0; j < tags.length; j++) {
|
||||
const tag = tags[j];
|
||||
for (let k = 0; k < this.svgAttrs.length; k++) {
|
||||
const attr = this.svgAttrs[k];
|
||||
for (let l = 0; l < this.keyHex.length; l++) {
|
||||
if (tag.getAttribute(attr) &&
|
||||
tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
|
||||
fixups.push({
|
||||
node: tag,
|
||||
attr: attr,
|
||||
index: l,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) console.log("calcSvgFixups end");
|
||||
|
||||
return fixups;
|
||||
}
|
||||
|
||||
applySvgFixups(fixups) {
|
||||
if (DEBUG) console.log("applySvgFixups start for " + fixups);
|
||||
for (let i = 0; i < fixups.length; i++) {
|
||||
const svgFixup = fixups[i];
|
||||
svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]);
|
||||
}
|
||||
if (DEBUG) console.log("applySvgFixups end");
|
||||
}
|
||||
}
|
||||
|
||||
if (global.singletonTinter === undefined) {
|
||||
global.singletonTinter = new Tinter();
|
||||
}
|
||||
export default global.singletonTinter;
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import shouldHideEvent from './shouldHideEvent';
|
||||
|
@ -43,12 +43,6 @@ export function eventTriggersUnreadCount(ev: MatrixEvent): boolean {
|
|||
case EventType.RoomCanonicalAlias:
|
||||
case EventType.RoomServerAcl:
|
||||
return false;
|
||||
|
||||
case EventType.RoomMessage:
|
||||
if (ev.getContent().msgtype === MsgType.Notice) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (ev.isRedacted()) return false;
|
||||
|
|
|
@ -68,7 +68,6 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
return matches.filter(cmd => cmd.isEnabled()).map((result) => {
|
||||
let completion = result.getCommand() + ' ';
|
||||
const usedAlias = result.aliases.find(alias => `/${alias}` === command[1]);
|
||||
|
|
|
@ -15,14 +15,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { HTMLAttributes } from "react";
|
||||
import React, { HTMLAttributes, WheelEvent } from "react";
|
||||
|
||||
interface IProps extends HTMLAttributes<HTMLDivElement> {
|
||||
interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onScroll"> {
|
||||
className?: string;
|
||||
onScroll?: () => void;
|
||||
onWheel?: () => void;
|
||||
style?: React.CSSProperties
|
||||
tabIndex?: number,
|
||||
onScroll?: (event: Event) => void;
|
||||
onWheel?: (event: WheelEvent) => void;
|
||||
style?: React.CSSProperties;
|
||||
tabIndex?: number;
|
||||
wrappedRef?: (ref: HTMLDivElement) => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ interface IProps {
|
|||
opacity?: number;
|
||||
}
|
||||
|
||||
|
||||
export default class BackdropPanel extends React.PureComponent<IProps> {
|
||||
private canvasRef = createRef<HTMLCanvasElement>();
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
@ -33,7 +32,7 @@ export default class BackdropPanel extends React.PureComponent<IProps> {
|
|||
static defaultProps = {
|
||||
blur: "60px",
|
||||
opacity: .15,
|
||||
}
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.ctx = this.canvasRef.current.getContext("2d");
|
||||
|
@ -78,7 +77,7 @@ export default class BackdropPanel extends React.PureComponent<IProps> {
|
|||
resultWidth,
|
||||
resultHeight,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
return <canvas
|
||||
|
|
|
@ -16,9 +16,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Filter } from 'matrix-js-sdk/src/filter';
|
||||
import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
|
||||
import { Direction } from "matrix-js-sdk/src/models/event-timeline";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
|
||||
|
||||
import * as sdk from '../../index';
|
||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
import EventIndexPeg from "../../indexing/EventIndexPeg";
|
||||
|
@ -28,25 +33,33 @@ import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
|||
import DesktopBuildsNotice, { WarningKind } from "../views/elements/DesktopBuildsNotice";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
|
||||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
onClose: () => void;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
timelineSet: EventTimelineSet;
|
||||
}
|
||||
|
||||
/*
|
||||
* Component which shows the filtered file using a TimelinePanel
|
||||
*/
|
||||
@replaceableComponent("structures.FilePanel")
|
||||
class FilePanel extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class FilePanel extends React.Component<IProps, IState> {
|
||||
// This is used to track if a decrypted event was a live event and should be
|
||||
// added to the timeline.
|
||||
decryptingEvents = new Set();
|
||||
private decryptingEvents = new Set<string>();
|
||||
public noRoom: boolean;
|
||||
|
||||
state = {
|
||||
timelineSet: null,
|
||||
};
|
||||
|
||||
onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
|
||||
private onRoomTimeline = (ev: MatrixEvent, room: Room, toStartOfTimeline: true, removed: true, data: any): void => {
|
||||
if (room?.roomId !== this.props?.roomId) return;
|
||||
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
|
||||
|
||||
|
@ -60,7 +73,7 @@ class FilePanel extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onEventDecrypted = (ev, err) => {
|
||||
private onEventDecrypted = (ev: MatrixEvent, err?: any): void => {
|
||||
if (ev.getRoomId() !== this.props.roomId) return;
|
||||
const eventId = ev.getId();
|
||||
|
||||
|
@ -70,7 +83,7 @@ class FilePanel extends React.Component {
|
|||
this.addEncryptedLiveEvent(ev);
|
||||
};
|
||||
|
||||
addEncryptedLiveEvent(ev, toStartOfTimeline) {
|
||||
public addEncryptedLiveEvent(ev: MatrixEvent): void {
|
||||
if (!this.state.timelineSet) return;
|
||||
|
||||
const timeline = this.state.timelineSet.getLiveTimeline();
|
||||
|
@ -84,7 +97,7 @@ class FilePanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
public async componentDidMount(): Promise<void> {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
await this.updateTimelineSet(this.props.roomId);
|
||||
|
@ -105,7 +118,7 @@ class FilePanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (client === null) return;
|
||||
|
||||
|
@ -117,7 +130,7 @@ class FilePanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async fetchFileEventsServer(room) {
|
||||
public async fetchFileEventsServer(room: Room): Promise<EventTimelineSet> {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
const filter = new Filter(client.credentials.userId);
|
||||
|
@ -141,7 +154,11 @@ class FilePanel extends React.Component {
|
|||
return timelineSet;
|
||||
}
|
||||
|
||||
onPaginationRequest = (timelineWindow, direction, limit) => {
|
||||
private onPaginationRequest = (
|
||||
timelineWindow: TimelineWindow,
|
||||
direction: Direction,
|
||||
limit: number,
|
||||
): Promise<boolean> => {
|
||||
const client = MatrixClientPeg.get();
|
||||
const eventIndex = EventIndexPeg.get();
|
||||
const roomId = this.props.roomId;
|
||||
|
@ -159,7 +176,7 @@ class FilePanel extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
async updateTimelineSet(roomId: string) {
|
||||
public async updateTimelineSet(roomId: string): Promise<void> {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
const eventIndex = EventIndexPeg.get();
|
||||
|
@ -195,7 +212,7 @@ class FilePanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return <BaseCard
|
||||
className="mx_FilePanel mx_RoomView_messageListWrapper"
|
|
@ -36,7 +36,7 @@ import FlairStore from '../../stores/FlairStore';
|
|||
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import { makeGroupPermalink, makeUserPermalink } from "../../utils/permalinks/Permalinks";
|
||||
import { Group } from "matrix-js-sdk/src/models/group";
|
||||
import {sleep} from "../../utils/promise";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import { mediaFromMxc } from "../../customisations/Media";
|
||||
|
@ -126,12 +126,11 @@ class CategoryRoomList extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const addButton = this.props.editing ?
|
||||
(<AccessibleButton className="mx_GroupView_featuredThings_addButton"
|
||||
onClick={this.onAddRoomsToSummaryClicked}
|
||||
>
|
||||
<TintableSvg src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
|
||||
<img src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
|
||||
<div className="mx_GroupView_featuredThings_addButton_label">
|
||||
{ _t('Add a Room') }
|
||||
</div>
|
||||
|
@ -300,10 +299,9 @@ class RoleUserList extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const addButton = this.props.editing ?
|
||||
(<AccessibleButton className="mx_GroupView_featuredThings_addButton" onClick={this.onAddUsersClicked}>
|
||||
<TintableSvg src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
|
||||
<img src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
|
||||
<div className="mx_GroupView_featuredThings_addButton_label">
|
||||
{ _t('Add a User') }
|
||||
</div>
|
||||
|
@ -363,7 +361,10 @@ class FeaturedUser extends React.Component {
|
|||
"Failed to remove a user from the summary of %(groupId)s",
|
||||
{ groupId: this.props.groupId },
|
||||
),
|
||||
description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}),
|
||||
description: _t(
|
||||
"The user '%(displayName)s' could not be removed from the summary.",
|
||||
{ displayName },
|
||||
),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -855,7 +856,6 @@ export default class GroupView extends React.Component {
|
|||
_getRoomsNode() {
|
||||
const RoomDetailList = sdk.getComponent('rooms.RoomDetailList');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const TooltipButton = sdk.getComponent('elements.TooltipButton');
|
||||
|
||||
|
@ -871,7 +871,7 @@ export default class GroupView extends React.Component {
|
|||
onClick={this._onAddRoomsClick}
|
||||
>
|
||||
<div className="mx_GroupView_rooms_header_addRow_button">
|
||||
<TintableSvg src={require("../../../res/img/icons-room-add.svg")} width="24" height="24" />
|
||||
<img src={require("../../../res/img/icons-room-add.svg")} width="24" height="24" />
|
||||
</div>
|
||||
<div className="mx_GroupView_rooms_header_addRow_label">
|
||||
{ _t('Add rooms to this community') }
|
||||
|
|
|
@ -117,7 +117,6 @@ const HomePage: React.FC<IProps> = ({ justRegistered = false }) => {
|
|||
</React.Fragment>;
|
||||
}
|
||||
|
||||
|
||||
return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default">
|
||||
<div className="mx_HomePage_default_wrapper">
|
||||
{ introSection }
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class HostSignupAction extends React.PureComponent<IProps, IState
|
|||
private openDialog = async () => {
|
||||
this.props.onClick?.();
|
||||
await HostSignupStore.instance.setHostSignupActive(true);
|
||||
}
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const hostSignupConfig = SdkConfig.get().hostSignup;
|
||||
|
|
|
@ -70,7 +70,6 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
this._autoHideScrollbar = autoHideScrollbar;
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const prevLen = prevProps && prevProps.children && prevProps.children.length || 0;
|
||||
const curLen = this.props.children && this.props.children.length || 0;
|
||||
|
|
|
@ -24,6 +24,7 @@ import CustomRoomTagPanel from "./CustomRoomTagPanel";
|
|||
import dis from "../../dispatcher/dispatcher";
|
||||
import { _t } from "../../languageHandler";
|
||||
import RoomList from "../views/rooms/RoomList";
|
||||
import CallHandler from "../../CallHandler";
|
||||
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import UserMenu from "./UserMenu";
|
||||
|
@ -120,6 +121,10 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
this.setState({ activeSpace });
|
||||
};
|
||||
|
||||
private onDialPad = () => {
|
||||
dis.fire(Action.OpenDialPad);
|
||||
};
|
||||
|
||||
private onExplore = () => {
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
};
|
||||
|
@ -127,7 +132,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
private refreshStickyHeaders = () => {
|
||||
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
|
||||
this.handleStickyHeaders(this.listContainerRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
private onBreadcrumbsUpdate = () => {
|
||||
const newVal = BreadcrumbsStore.instance.visible;
|
||||
|
@ -376,7 +381,20 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private renderSearchExplore(): React.ReactNode {
|
||||
private renderSearchDialExplore(): React.ReactNode {
|
||||
let dialPadButton = null;
|
||||
|
||||
// If we have dialer support, show a button to bring up the dial pad
|
||||
// to start a new call
|
||||
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
|
||||
dialPadButton =
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_LeftPanel_dialPadButton", {})}
|
||||
onClick={this.onDialPad}
|
||||
title={_t("Open dial pad")}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mx_LeftPanel_filterContainer"
|
||||
|
@ -389,6 +407,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
onKeyDown={this.onKeyDown}
|
||||
onSelectRoom={this.selectRoom}
|
||||
/>
|
||||
|
||||
{dialPadButton}
|
||||
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_LeftPanel_exploreButton", {
|
||||
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
|
||||
|
@ -444,7 +465,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
{leftLeftPanel}
|
||||
<aside className="mx_LeftPanel_roomListContainer">
|
||||
{this.renderHeader()}
|
||||
{this.renderSearchExplore()}
|
||||
{this.renderSearchDialExplore()}
|
||||
{this.renderBreadcrumbs()}
|
||||
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
|
||||
<div className="mx_LeftPanel_roomListWrapper">
|
||||
|
|
|
@ -48,7 +48,7 @@ import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPay
|
|||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import Modal from "../../Modal";
|
||||
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
||||
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
||||
|
@ -62,7 +62,6 @@ import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
|
|||
import { OwnProfileStore } from '../../stores/OwnProfileStore';
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
// NB. this is just for server notices rather than pinned messages in general.
|
||||
|
@ -84,14 +83,14 @@ interface IProps {
|
|||
page_type: string;
|
||||
autoJoin: boolean;
|
||||
threepidInvite?: IThreepidInvite;
|
||||
roomOobData?: object;
|
||||
roomOobData?: IOOBData;
|
||||
currentRoomId: string;
|
||||
collapseLhs: boolean;
|
||||
config: {
|
||||
piwik: {
|
||||
policyUrl: string;
|
||||
},
|
||||
[key: string]: any,
|
||||
};
|
||||
[key: string]: any;
|
||||
};
|
||||
currentUserId?: string;
|
||||
currentGroupId?: string;
|
||||
|
@ -221,7 +220,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
this.setState({
|
||||
backgroundImage: await OwnProfileStore.instance.getAvatarBitmap(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onCallsChanged = () => {
|
||||
this.setState({
|
||||
|
@ -332,7 +331,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
this.setState({
|
||||
usageLimitDismissed: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
|
||||
const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
|
||||
|
|
|
@ -19,6 +19,8 @@ import { createClient } from "matrix-js-sdk/src/matrix";
|
|||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { sleep, defer, IDeferred } from "matrix-js-sdk/src/utils";
|
||||
|
||||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
||||
import 'focus-visible';
|
||||
// what-input helps improve keyboard accessibility
|
||||
|
@ -34,7 +36,6 @@ import dis from "../../dispatcher/dispatcher";
|
|||
import Notifier from '../../Notifier';
|
||||
|
||||
import Modal from "../../Modal";
|
||||
import Tinter from "../../Tinter";
|
||||
import * as sdk from '../../index';
|
||||
import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite';
|
||||
import * as Rooms from '../../Rooms';
|
||||
|
@ -56,7 +57,6 @@ import DMRoomMap from '../../utils/DMRoomMap';
|
|||
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
||||
import { FontWatcher } from '../../settings/watchers/FontWatcher';
|
||||
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
||||
import { defer, IDeferred, sleep } from "../../utils/promise";
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
import * as StorageManager from "../../utils/StorageManager";
|
||||
import type LoggedInViewType from "./LoggedInView";
|
||||
|
@ -204,7 +204,7 @@ interface IState {
|
|||
resizeNotifier: ResizeNotifier;
|
||||
serverConfig?: ValidatedServerConfig;
|
||||
ready: boolean;
|
||||
threepidInvite?: IThreepidInvite,
|
||||
threepidInvite?: IThreepidInvite;
|
||||
roomOobData?: object;
|
||||
pendingInitialSync?: boolean;
|
||||
justRegistered?: boolean;
|
||||
|
@ -283,11 +283,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
this.pageChanging = false;
|
||||
|
||||
// check we have the right tint applied for this theme.
|
||||
// N.B. we don't call the whole of setTheme() here as we may be
|
||||
// racing with the theme CSS download finishing from index.js
|
||||
Tinter.tint();
|
||||
|
||||
// For PersistentElement
|
||||
this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize);
|
||||
|
||||
|
@ -668,7 +663,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.createRoom(payload.public, payload.defaultName);
|
||||
break;
|
||||
case 'view_create_group': {
|
||||
let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog")
|
||||
let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
|
||||
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||
CreateGroupDialog = CreateCommunityPrototypeDialog;
|
||||
}
|
||||
|
@ -1131,8 +1126,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
description: (
|
||||
<span>
|
||||
{ isSpace
|
||||
? _t("Are you sure you want to leave the space '%(spaceName)s'?", {spaceName: roomToLeave.name})
|
||||
: _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
|
||||
? _t(
|
||||
"Are you sure you want to leave the space '%(spaceName)s'?",
|
||||
{ spaceName: roomToLeave.name },
|
||||
)
|
||||
: _t(
|
||||
"Are you sure you want to leave the room '%(roomName)s'?",
|
||||
{ roomName: roomToLeave.name },
|
||||
)}
|
||||
{ warnings }
|
||||
</span>
|
||||
),
|
||||
|
@ -1263,7 +1264,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// HACK: This is a pretty brutal way of threading the invite back through
|
||||
// our systems, but it's the safest we have for now.
|
||||
const params = ThreepidInviteStore.instance.translateToWireFormat(threepidInvite);
|
||||
this.showScreen(`room/${threepidInvite.roomId}`, params)
|
||||
this.showScreen(`room/${threepidInvite.roomId}`, params);
|
||||
} else {
|
||||
// The user has just logged in after registering,
|
||||
// so show the homepage.
|
||||
|
@ -1573,10 +1574,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
});
|
||||
// Fire the tinter right on startup to ensure the default theme is applied
|
||||
// A later sync can/will correct the tint to be the right value for the user
|
||||
const colorScheme = SettingsStore.getValue("roomColor");
|
||||
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -123,7 +123,7 @@ export default class MyGroups extends React.Component {
|
|||
</div>
|
||||
{/*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
|
||||
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
|
||||
<TintableSvg src={require("../../../res/img/icons-create-room.svg")} width="50" height="50" />
|
||||
<img src={require("../../../res/img/icons-create-room.svg")} width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
<div className="mx_MyGroups_headerCard_content">
|
||||
<div className="mx_MyGroups_headerCard_header">
|
||||
|
|
|
@ -24,7 +24,7 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IState {
|
||||
toasts: ComponentClass[],
|
||||
toasts: ComponentClass[];
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.NonUrgentToastContainer")
|
||||
|
|
|
@ -22,6 +22,7 @@ import BaseCard from "../views/right_panel/BaseCard";
|
|||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import { TileShape } from "../views/rooms/EventTile";
|
||||
|
||||
interface IProps {
|
||||
onClose(): void;
|
||||
|
@ -48,7 +49,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
|
|||
manageReadMarkers={false}
|
||||
timelineSet={timelineSet}
|
||||
showUrlPreview={false}
|
||||
tileShape="notif"
|
||||
tileShape={TileShape.Notif}
|
||||
empty={emptyState}
|
||||
alwaysShowTimestamps={true}
|
||||
/>
|
||||
|
|
|
@ -23,7 +23,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
import {
|
||||
RIGHT_PANEL_PHASES_NO_ARGS,
|
||||
|
@ -48,6 +47,7 @@ import FilePanel from "./FilePanel";
|
|||
import NotificationPanel from "./NotificationPanel";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
interface IProps {
|
||||
room?: Room; // if showing panels for a given room, this is set
|
||||
|
@ -73,7 +73,6 @@ interface IState {
|
|||
export default class RightPanel extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
private readonly delayedUpdate: RateLimitedFunc;
|
||||
private dispatcherRef: string;
|
||||
|
||||
constructor(props, context) {
|
||||
|
@ -84,12 +83,12 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
|||
isUserPrivilegedInGroup: null,
|
||||
member: this.getUserForPanel(),
|
||||
};
|
||||
|
||||
this.delayedUpdate = new RateLimitedFunc(() => {
|
||||
this.forceUpdate();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private readonly delayedUpdate = throttle((): void => {
|
||||
this.forceUpdate();
|
||||
}, 500, { leading: true, trailing: true });
|
||||
|
||||
// Helper function to split out the logic for getPhaseFromProps() and the constructor
|
||||
// as both are called at the same time in the constructor.
|
||||
private getUserForPanel() {
|
||||
|
|
|
@ -45,7 +45,6 @@ import ScrollPanel from "./ScrollPanel";
|
|||
import Spinner from "../views/elements/Spinner";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
|
||||
|
||||
const MAX_NAME_LENGTH = 80;
|
||||
const MAX_TOPIC_LENGTH = 800;
|
||||
|
||||
|
@ -95,7 +94,7 @@ interface IPublicRoomsRequest {
|
|||
@replaceableComponent("structures.RoomDirectory")
|
||||
export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||
private readonly startTime: number;
|
||||
private unmounted = false
|
||||
private unmounted = false;
|
||||
private nextBatch: string = null;
|
||||
private filterTimeout: NodeJS.Timeout;
|
||||
private protocols: Protocols;
|
||||
|
@ -207,9 +206,9 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
this.getMoreRooms();
|
||||
};
|
||||
|
||||
private getMoreRooms() {
|
||||
if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
|
||||
if (!MatrixClientPeg.get()) return Promise.resolve();
|
||||
private getMoreRooms(): Promise<boolean> {
|
||||
if (this.state.selectedCommunityId) return Promise.resolve(false); // no more rooms
|
||||
if (!MatrixClientPeg.get()) return Promise.resolve(false);
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
|
@ -239,12 +238,12 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
// if the filter or server has changed since this request was sent,
|
||||
// throw away the result (don't even clear the busy flag
|
||||
// since we must still have a request in flight)
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.unmounted) {
|
||||
// if we've been unmounted, we don't care either.
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.state.filterString) {
|
||||
|
@ -264,14 +263,13 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
filterString != this.state.filterString ||
|
||||
roomServer != this.state.roomServer ||
|
||||
nextBatch != this.nextBatch) {
|
||||
// as above: we don't care about errors for old
|
||||
// requests either
|
||||
return;
|
||||
// as above: we don't care about errors for old requests either
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.unmounted) {
|
||||
// if we've been unmounted, we don't care either.
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
console.error("Failed to get publicRooms: %s", JSON.stringify(err));
|
||||
|
@ -372,7 +370,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
|
||||
private onFilterChange = (alias: string) => {
|
||||
this.setState({
|
||||
filterString: alias || null,
|
||||
filterString: alias || "",
|
||||
});
|
||||
|
||||
// don't send the request for a little bit,
|
||||
|
@ -391,7 +389,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
private onFilterClear = () => {
|
||||
// update immediately
|
||||
this.setState({
|
||||
filterString: null,
|
||||
filterString: "",
|
||||
}, this.refreshRoomList);
|
||||
|
||||
if (this.filterTimeout) {
|
||||
|
|
|
@ -37,8 +37,6 @@ import Modal from '../../Modal';
|
|||
import * as sdk from '../../index';
|
||||
import CallHandler, { PlaceCallType } from '../../CallHandler';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import Tinter from '../../Tinter';
|
||||
import rateLimitedFunc from '../../ratelimitedfunc';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import eventSearch, { searchPagination } from '../../Searching';
|
||||
import MainSplit from './MainSplit';
|
||||
|
@ -65,7 +63,7 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
|||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import { XOR } from "../../@types/common";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import { containsEmoji } from '../../effects/utils';
|
||||
import { CHAT_EFFECTS } from '../../effects';
|
||||
|
@ -82,6 +80,8 @@ import SpaceRoomView from "./SpaceRoomView";
|
|||
import { IOpts } from "../../createRoom";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
import { throttle } from "lodash";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -94,22 +94,8 @@ if (DEBUG) {
|
|||
}
|
||||
|
||||
interface IProps {
|
||||
threepidInvite: IThreepidInvite,
|
||||
|
||||
// Any data about the room that would normally come from the homeserver
|
||||
// but has been passed out-of-band, eg. the room name and avatar URL
|
||||
// from an email invite (a workaround for the fact that we can't
|
||||
// get this information from the HS using an email invite).
|
||||
// Fields:
|
||||
// * name (string) The room's name
|
||||
// * avatarUrl (string) The mxc:// avatar URL for the room
|
||||
// * inviterName (string) The display name of the person who
|
||||
// * invited us to the room
|
||||
oobData?: {
|
||||
name?: string;
|
||||
avatarUrl?: string;
|
||||
inviterName?: string;
|
||||
};
|
||||
threepidInvite: IThreepidInvite;
|
||||
oobData?: IOOBData;
|
||||
|
||||
resizeNotifier: ResizeNotifier;
|
||||
justCreatedOpts?: IOpts;
|
||||
|
@ -192,6 +178,7 @@ export interface IState {
|
|||
// whether or not a spaces context switch brought us here,
|
||||
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||
wasContextSwitch?: boolean;
|
||||
editState?: EditorStateTransfer;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomView")
|
||||
|
@ -285,7 +272,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
if (this.state.room) {
|
||||
this.checkWidgets(this.state.room);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private checkWidgets = (room) => {
|
||||
this.setState({
|
||||
|
@ -674,12 +661,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this.updateRoomMembers.cancelPendingCall();
|
||||
|
||||
// no need to do this as Dir & Settings are now overlays. It just burnt CPU.
|
||||
// console.log("Tinter.tint from RoomView.unmount");
|
||||
// Tinter.tint(); // reset colourscheme
|
||||
// cancel any pending calls to the throttled updated
|
||||
this.updateRoomMembers.cancel();
|
||||
|
||||
for (const watcher of this.settingWatchers) {
|
||||
SettingsStore.unwatchSetting(watcher);
|
||||
|
@ -696,7 +679,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
replyingToEvent: this.state.replyToEvent,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onRightPanelStoreUpdate = () => {
|
||||
this.setState({
|
||||
|
@ -815,6 +798,36 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
case 'focus_search':
|
||||
this.onSearchClick();
|
||||
break;
|
||||
|
||||
case "edit_event": {
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({ editState }, () => {
|
||||
if (payload.event) {
|
||||
this.messagePanel?.scrollToEventIfNeeded(payload.event.getId());
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case Action.ComposerInsert: {
|
||||
// re-dispatch to the correct composer
|
||||
if (this.state.editState) {
|
||||
dis.dispatch({
|
||||
...payload,
|
||||
action: "edit_composer_insert",
|
||||
});
|
||||
} else {
|
||||
dis.dispatch({
|
||||
...payload,
|
||||
action: "send_composer_insert",
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "scroll_to_bottom":
|
||||
this.messagePanel?.jumpToLiveTimeline();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1027,15 +1040,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private updateTint() {
|
||||
const room = this.state.room;
|
||||
if (!room) return;
|
||||
|
||||
console.log("Tinter.tint from updateTint");
|
||||
const colorScheme = SettingsStore.getValue("roomColor", room.roomId);
|
||||
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
|
||||
}
|
||||
|
||||
private onAccountData = (event: MatrixEvent) => {
|
||||
const type = event.getType();
|
||||
if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) {
|
||||
|
@ -1047,12 +1051,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
private onRoomAccountData = (event: MatrixEvent, room: Room) => {
|
||||
if (room.roomId == this.state.roomId) {
|
||||
const type = event.getType();
|
||||
if (type === "org.matrix.room.color_scheme") {
|
||||
const colorScheme = event.getContent();
|
||||
// XXX: we should validate the event
|
||||
console.log("Tinter.tint from onRoomAccountData");
|
||||
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
|
||||
} else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
|
||||
if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
|
||||
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
|
||||
this.updatePreviewUrlVisibility(room);
|
||||
}
|
||||
|
@ -1079,7 +1078,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
return;
|
||||
}
|
||||
|
||||
this.updateRoomMembers(member);
|
||||
this.updateRoomMembers();
|
||||
};
|
||||
|
||||
private onMyMembership = (room: Room, membership: string, oldMembership: string) => {
|
||||
|
@ -1101,10 +1100,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// rate limited because a power level change will emit an event for every member in the room.
|
||||
private updateRoomMembers = rateLimitedFunc(() => {
|
||||
private updateRoomMembers = throttle(() => {
|
||||
this.updateDMState();
|
||||
this.updateE2EStatus(this.state.room);
|
||||
}, 500);
|
||||
}, 500, { leading: true, trailing: true });
|
||||
|
||||
private checkDesktopNotifications() {
|
||||
const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount();
|
||||
|
@ -1125,7 +1124,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private onSearchResultsFillRequest = (backwards: boolean) => {
|
||||
private onSearchResultsFillRequest = (backwards: boolean): Promise<boolean> => {
|
||||
if (!backwards) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
@ -1248,7 +1247,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private injectSticker(url, info, text) {
|
||||
private injectSticker(url: string, info: object, text: string) {
|
||||
if (this.context.isGuest()) {
|
||||
dis.dispatch({ action: 'require_registration' });
|
||||
return;
|
||||
|
@ -1291,7 +1290,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.handleSearchResult(searchPromise);
|
||||
};
|
||||
|
||||
private handleSearchResult(searchPromise: Promise<any>) {
|
||||
private handleSearchResult(searchPromise: Promise<any>): Promise<boolean> {
|
||||
// keep a record of the current search id, so that if the search terms
|
||||
// change before we get a response, we can ignore the results.
|
||||
const localSearchId = this.searchId;
|
||||
|
@ -1304,7 +1303,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
debuglog("search complete");
|
||||
if (this.unmounted || !this.state.searching || this.searchId != localSearchId) {
|
||||
console.error("Discarding stale search results");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// postgres on synapse returns us precise details of the strings
|
||||
|
@ -1336,6 +1335,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
description: ((error && error.message) ? error.message :
|
||||
_t("Server may be unavailable, overloaded, or search timed out :(")),
|
||||
});
|
||||
return false;
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
searchInProgress: false,
|
||||
|
@ -1446,13 +1446,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onLeaveClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
room_id: this.state.room.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
private onForgetClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'forget_room',
|
||||
|
@ -1686,10 +1679,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
// otherwise react calls it with null on each update.
|
||||
private gatherTimelinePanelRef = r => {
|
||||
this.messagePanel = r;
|
||||
if (r) {
|
||||
console.log("updateTint from RoomView.gatherTimelinePanelRef");
|
||||
this.updateTint();
|
||||
}
|
||||
};
|
||||
|
||||
private getOldRoom() {
|
||||
|
@ -2039,6 +2028,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
resizeNotifier={this.props.resizeNotifier}
|
||||
showReactions={true}
|
||||
layout={this.state.layout}
|
||||
editState={this.state.editState}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
@ -2095,7 +2085,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
|
||||
appsShown={this.state.showApps}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2015 - 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.
|
||||
|
@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { createRef, CSSProperties, ReactNode, SyntheticEvent, KeyboardEvent } from "react";
|
||||
import Timer from '../../utils/Timer';
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { getKeyBindingsManager, RoomAction } from "../../KeyBindingsManager";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
|
||||
const DEBUG_SCROLL = false;
|
||||
|
||||
// The amount of extra scroll distance to allow prior to unfilling.
|
||||
// See _getExcessHeight.
|
||||
// See getExcessHeight.
|
||||
const UNPAGINATION_PADDING = 6000;
|
||||
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
||||
// many scroll events causing many unfilling requests.
|
||||
|
@ -43,6 +43,75 @@ if (DEBUG_SCROLL) {
|
|||
debuglog = function() {};
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
/* stickyBottom: if set to true, then once the user hits the bottom of
|
||||
* the list, any new children added to the list will cause the list to
|
||||
* scroll down to show the new element, rather than preserving the
|
||||
* existing view.
|
||||
*/
|
||||
stickyBottom?: boolean;
|
||||
|
||||
/* startAtBottom: if set to true, the view is assumed to start
|
||||
* scrolled to the bottom.
|
||||
* XXX: It's likely this is unnecessary and can be derived from
|
||||
* stickyBottom, but I'm adding an extra parameter to ensure
|
||||
* behaviour stays the same for other uses of ScrollPanel.
|
||||
* If so, let's remove this parameter down the line.
|
||||
*/
|
||||
startAtBottom?: boolean;
|
||||
|
||||
/* className: classnames to add to the top-level div
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/* style: styles to add to the top-level div
|
||||
*/
|
||||
style?: CSSProperties;
|
||||
|
||||
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
|
||||
*/
|
||||
resizeNotifier?: ResizeNotifier;
|
||||
|
||||
/* fixedChildren: allows for children to be passed which are rendered outside
|
||||
* of the wrapper
|
||||
*/
|
||||
fixedChildren?: ReactNode;
|
||||
|
||||
/* onFillRequest(backwards): a callback which is called on scroll when
|
||||
* the user nears the start (backwards = true) or end (backwards =
|
||||
* false) of the list.
|
||||
*
|
||||
* This should return a promise; no more calls will be made until the
|
||||
* promise completes.
|
||||
*
|
||||
* The promise should resolve to true if there is more data to be
|
||||
* retrieved in this direction (in which case onFillRequest may be
|
||||
* called again immediately), or false if there is no more data in this
|
||||
* directon (at this time) - which will stop the pagination cycle until
|
||||
* the user scrolls again.
|
||||
*/
|
||||
onFillRequest?(backwards: boolean): Promise<boolean>;
|
||||
|
||||
/* onUnfillRequest(backwards): a callback which is called on scroll when
|
||||
* there are children elements that are far out of view and could be removed
|
||||
* without causing pagination to occur.
|
||||
*
|
||||
* This function should accept a boolean, which is true to indicate the back/top
|
||||
* of the panel and false otherwise, and a scroll token, which refers to the
|
||||
* first element to remove if removing from the front/bottom, and last element
|
||||
* to remove if removing from the back/top.
|
||||
*/
|
||||
onUnfillRequest?(backwards: boolean, scrollToken: string): void;
|
||||
|
||||
/* onScroll: a callback which is called whenever any scroll happens.
|
||||
*/
|
||||
onScroll?(event: Event): void;
|
||||
|
||||
/* onUserScroll: callback which is called when the user interacts with the room timeline
|
||||
*/
|
||||
onUserScroll?(event: SyntheticEvent): void;
|
||||
}
|
||||
|
||||
/* This component implements an intelligent scrolling list.
|
||||
*
|
||||
* It wraps a list of <li> children; when items are added to the start or end
|
||||
|
@ -84,97 +153,54 @@ if (DEBUG_SCROLL) {
|
|||
* offset as normal.
|
||||
*/
|
||||
|
||||
export interface IScrollState {
|
||||
stuckAtBottom: boolean;
|
||||
trackedNode?: HTMLElement;
|
||||
trackedScrollToken?: string;
|
||||
bottomOffset?: number;
|
||||
pixelOffset?: number;
|
||||
}
|
||||
|
||||
interface IPreventShrinkingState {
|
||||
offsetFromBottom: number;
|
||||
offsetNode: HTMLElement;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.ScrollPanel")
|
||||
export default class ScrollPanel extends React.Component {
|
||||
static propTypes = {
|
||||
/* stickyBottom: if set to true, then once the user hits the bottom of
|
||||
* the list, any new children added to the list will cause the list to
|
||||
* scroll down to show the new element, rather than preserving the
|
||||
* existing view.
|
||||
*/
|
||||
stickyBottom: PropTypes.bool,
|
||||
|
||||
/* startAtBottom: if set to true, the view is assumed to start
|
||||
* scrolled to the bottom.
|
||||
* XXX: It's likely this is unnecessary and can be derived from
|
||||
* stickyBottom, but I'm adding an extra parameter to ensure
|
||||
* behaviour stays the same for other uses of ScrollPanel.
|
||||
* If so, let's remove this parameter down the line.
|
||||
*/
|
||||
startAtBottom: PropTypes.bool,
|
||||
|
||||
/* onFillRequest(backwards): a callback which is called on scroll when
|
||||
* the user nears the start (backwards = true) or end (backwards =
|
||||
* false) of the list.
|
||||
*
|
||||
* This should return a promise; no more calls will be made until the
|
||||
* promise completes.
|
||||
*
|
||||
* The promise should resolve to true if there is more data to be
|
||||
* retrieved in this direction (in which case onFillRequest may be
|
||||
* called again immediately), or false if there is no more data in this
|
||||
* directon (at this time) - which will stop the pagination cycle until
|
||||
* the user scrolls again.
|
||||
*/
|
||||
onFillRequest: PropTypes.func,
|
||||
|
||||
/* onUnfillRequest(backwards): a callback which is called on scroll when
|
||||
* there are children elements that are far out of view and could be removed
|
||||
* without causing pagination to occur.
|
||||
*
|
||||
* This function should accept a boolean, which is true to indicate the back/top
|
||||
* of the panel and false otherwise, and a scroll token, which refers to the
|
||||
* first element to remove if removing from the front/bottom, and last element
|
||||
* to remove if removing from the back/top.
|
||||
*/
|
||||
onUnfillRequest: PropTypes.func,
|
||||
|
||||
/* onScroll: a callback which is called whenever any scroll happens.
|
||||
*/
|
||||
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: PropTypes.string,
|
||||
|
||||
/* style: styles to add to the top-level div
|
||||
*/
|
||||
style: PropTypes.object,
|
||||
|
||||
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
|
||||
*/
|
||||
resizeNotifier: PropTypes.object,
|
||||
|
||||
/* fixedChildren: allows for children to be passed which are rendered outside
|
||||
* of the wrapper
|
||||
*/
|
||||
fixedChildren: PropTypes.node,
|
||||
};
|
||||
|
||||
export default class ScrollPanel extends React.Component<IProps> {
|
||||
static defaultProps = {
|
||||
stickyBottom: true,
|
||||
startAtBottom: true,
|
||||
onFillRequest: function(backwards) { return Promise.resolve(false); },
|
||||
onUnfillRequest: function(backwards, scrollToken) {},
|
||||
onFillRequest: function(backwards: boolean) { return Promise.resolve(false); },
|
||||
onUnfillRequest: function(backwards: boolean, scrollToken: string) {},
|
||||
onScroll: function() {},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
private readonly pendingFillRequests: Record<"b" | "f", boolean> = {
|
||||
b: null,
|
||||
f: null,
|
||||
};
|
||||
private readonly itemlist = createRef<HTMLOListElement>();
|
||||
private unmounted = false;
|
||||
private scrollTimeout: Timer;
|
||||
private isFilling: boolean;
|
||||
private fillRequestWhileRunning: boolean;
|
||||
private scrollState: IScrollState;
|
||||
private preventShrinkingState: IPreventShrinkingState;
|
||||
private unfillDebouncer: NodeJS.Timeout;
|
||||
private bottomGrowth: number;
|
||||
private pages: number;
|
||||
private heightUpdateInProgress: boolean;
|
||||
private divScroll: HTMLDivElement;
|
||||
|
||||
this._pendingFillRequests = {b: null, f: null};
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
|
||||
}
|
||||
|
||||
this.resetScrollState();
|
||||
|
||||
this._itemlist = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -203,18 +229,18 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onScroll = ev => {
|
||||
private onScroll = ev => {
|
||||
// skip scroll events caused by resizing
|
||||
if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return;
|
||||
debuglog("onScroll", this._getScrollNode().scrollTop);
|
||||
this._scrollTimeout.restart();
|
||||
this._saveScrollState();
|
||||
debuglog("onScroll", this.getScrollNode().scrollTop);
|
||||
this.scrollTimeout.restart();
|
||||
this.saveScrollState();
|
||||
this.updatePreventShrinking();
|
||||
this.props.onScroll(ev);
|
||||
this.checkFillState();
|
||||
};
|
||||
|
||||
onResize = () => {
|
||||
private onResize = () => {
|
||||
debuglog("onResize");
|
||||
this.checkScroll();
|
||||
// update preventShrinkingState if present
|
||||
|
@ -225,11 +251,11 @@ export default class ScrollPanel extends React.Component {
|
|||
|
||||
// after an update to the contents of the panel, check that the scroll is
|
||||
// where it ought to be, and set off pagination requests if necessary.
|
||||
checkScroll = () => {
|
||||
public checkScroll = () => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this._restoreSavedScrollState();
|
||||
this.restoreSavedScrollState();
|
||||
this.checkFillState();
|
||||
};
|
||||
|
||||
|
@ -238,8 +264,8 @@ export default class ScrollPanel extends React.Component {
|
|||
// note that this is independent of the 'stuckAtBottom' state - it is simply
|
||||
// about whether the content is scrolled down right now, irrespective of
|
||||
// whether it will stay that way when the children update.
|
||||
isAtBottom = () => {
|
||||
const sn = this._getScrollNode();
|
||||
public isAtBottom = () => {
|
||||
const sn = this.getScrollNode();
|
||||
// fractional values (both too big and too small)
|
||||
// for scrollTop happen on certain browsers/platforms
|
||||
// when scrolled all the way down. E.g. Chrome 72 on debian.
|
||||
|
@ -278,10 +304,10 @@ export default class ScrollPanel extends React.Component {
|
|||
// |#########| - |
|
||||
// |#########| |
|
||||
// `---------' -
|
||||
_getExcessHeight(backwards) {
|
||||
const sn = this._getScrollNode();
|
||||
const contentHeight = this._getMessagesHeight();
|
||||
const listHeight = this._getListHeight();
|
||||
private getExcessHeight(backwards: boolean): number {
|
||||
const sn = this.getScrollNode();
|
||||
const contentHeight = this.getMessagesHeight();
|
||||
const listHeight = this.getListHeight();
|
||||
const clippedHeight = contentHeight - listHeight;
|
||||
const unclippedScrollTop = sn.scrollTop + clippedHeight;
|
||||
|
||||
|
@ -293,13 +319,13 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
|
||||
// check the scroll state and send out backfill requests if necessary.
|
||||
checkFillState = async (depth=0) => {
|
||||
public checkFillState = async (depth = 0): Promise<void> => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFirstCall = depth === 0;
|
||||
const sn = this._getScrollNode();
|
||||
const sn = this.getScrollNode();
|
||||
|
||||
// if there is less than a screenful of messages above or below the
|
||||
// viewport, try to get some more messages.
|
||||
|
@ -330,17 +356,17 @@ export default class ScrollPanel extends React.Component {
|
|||
// do make a note when a new request comes in while already running one,
|
||||
// so we can trigger a new chain of calls once done.
|
||||
if (isFirstCall) {
|
||||
if (this._isFilling) {
|
||||
debuglog("_isFilling: not entering while request is ongoing, marking for a subsequent request");
|
||||
this._fillRequestWhileRunning = true;
|
||||
if (this.isFilling) {
|
||||
debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request");
|
||||
this.fillRequestWhileRunning = true;
|
||||
return;
|
||||
}
|
||||
debuglog("_isFilling: setting");
|
||||
this._isFilling = true;
|
||||
debuglog("isFilling: setting");
|
||||
this.isFilling = true;
|
||||
}
|
||||
|
||||
const itemlist = this._itemlist.current;
|
||||
const firstTile = itemlist && itemlist.firstElementChild;
|
||||
const itemlist = this.itemlist.current;
|
||||
const firstTile = itemlist && itemlist.firstElementChild as HTMLElement;
|
||||
const contentTop = firstTile && firstTile.offsetTop;
|
||||
const fillPromises = [];
|
||||
|
||||
|
@ -348,13 +374,13 @@ export default class ScrollPanel extends React.Component {
|
|||
// try backward filling
|
||||
if (!firstTile || (sn.scrollTop - contentTop) < sn.clientHeight) {
|
||||
// need to back-fill
|
||||
fillPromises.push(this._maybeFill(depth, true));
|
||||
fillPromises.push(this.maybeFill(depth, true));
|
||||
}
|
||||
// if scrollTop gets to 2 screens from the end (so 1 screen below viewport),
|
||||
// try forward filling
|
||||
if ((sn.scrollHeight - sn.scrollTop) < sn.clientHeight * 2) {
|
||||
// need to forward-fill
|
||||
fillPromises.push(this._maybeFill(depth, false));
|
||||
fillPromises.push(this.maybeFill(depth, false));
|
||||
}
|
||||
|
||||
if (fillPromises.length) {
|
||||
|
@ -365,26 +391,26 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
}
|
||||
if (isFirstCall) {
|
||||
debuglog("_isFilling: clearing");
|
||||
this._isFilling = false;
|
||||
debuglog("isFilling: clearing");
|
||||
this.isFilling = false;
|
||||
}
|
||||
|
||||
if (this._fillRequestWhileRunning) {
|
||||
this._fillRequestWhileRunning = false;
|
||||
if (this.fillRequestWhileRunning) {
|
||||
this.fillRequestWhileRunning = false;
|
||||
this.checkFillState();
|
||||
}
|
||||
};
|
||||
|
||||
// check if unfilling is possible and send an unfill request if necessary
|
||||
_checkUnfillState(backwards) {
|
||||
let excessHeight = this._getExcessHeight(backwards);
|
||||
private checkUnfillState(backwards: boolean): void {
|
||||
let excessHeight = this.getExcessHeight(backwards);
|
||||
if (excessHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const origExcessHeight = excessHeight;
|
||||
|
||||
const tiles = this._itemlist.current.children;
|
||||
const tiles = this.itemlist.current.children;
|
||||
|
||||
// The scroll token of the first/last tile to be unpaginated
|
||||
let markerScrollToken = null;
|
||||
|
@ -413,11 +439,11 @@ export default class ScrollPanel extends React.Component {
|
|||
if (markerScrollToken) {
|
||||
// Use a debouncer to prevent multiple unfill calls in quick succession
|
||||
// This is to make the unfilling process less aggressive
|
||||
if (this._unfillDebouncer) {
|
||||
clearTimeout(this._unfillDebouncer);
|
||||
if (this.unfillDebouncer) {
|
||||
clearTimeout(this.unfillDebouncer);
|
||||
}
|
||||
this._unfillDebouncer = setTimeout(() => {
|
||||
this._unfillDebouncer = null;
|
||||
this.unfillDebouncer = setTimeout(() => {
|
||||
this.unfillDebouncer = null;
|
||||
debuglog("unfilling now", backwards, origExcessHeight);
|
||||
this.props.onUnfillRequest(backwards, markerScrollToken);
|
||||
}, UNFILL_REQUEST_DEBOUNCE_MS);
|
||||
|
@ -425,9 +451,9 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
|
||||
// check if there is already a pending fill request. If not, set one off.
|
||||
_maybeFill(depth, backwards) {
|
||||
private maybeFill(depth: number, backwards: boolean): Promise<void> {
|
||||
const dir = backwards ? 'b' : 'f';
|
||||
if (this._pendingFillRequests[dir]) {
|
||||
if (this.pendingFillRequests[dir]) {
|
||||
debuglog("Already a "+dir+" fill in progress - not starting another");
|
||||
return;
|
||||
}
|
||||
|
@ -436,7 +462,7 @@ export default class ScrollPanel extends React.Component {
|
|||
|
||||
// onFillRequest can end up calling us recursively (via onScroll
|
||||
// events) so make sure we set this before firing off the call.
|
||||
this._pendingFillRequests[dir] = true;
|
||||
this.pendingFillRequests[dir] = true;
|
||||
|
||||
// wait 1ms before paginating, because otherwise
|
||||
// this will block the scroll event handler for +700ms
|
||||
|
@ -445,13 +471,13 @@ export default class ScrollPanel extends React.Component {
|
|||
return new Promise(resolve => setTimeout(resolve, 1)).then(() => {
|
||||
return this.props.onFillRequest(backwards);
|
||||
}).finally(() => {
|
||||
this._pendingFillRequests[dir] = false;
|
||||
this.pendingFillRequests[dir] = false;
|
||||
}).then((hasMoreResults) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
// Unpaginate once filling is complete
|
||||
this._checkUnfillState(!backwards);
|
||||
this.checkUnfillState(!backwards);
|
||||
|
||||
debuglog(""+dir+" fill complete; hasMoreResults:"+hasMoreResults);
|
||||
if (hasMoreResults) {
|
||||
|
@ -477,7 +503,7 @@ export default class ScrollPanel extends React.Component {
|
|||
* the number of pixels the bottom of the tracked child is above the
|
||||
* bottom of the scroll panel.
|
||||
*/
|
||||
getScrollState = () => this.scrollState;
|
||||
public getScrollState = (): IScrollState => this.scrollState;
|
||||
|
||||
/* reset the saved scroll state.
|
||||
*
|
||||
|
@ -491,35 +517,35 @@ export default class ScrollPanel extends React.Component {
|
|||
* no use if no children exist yet, or if you are about to replace the
|
||||
* child list.)
|
||||
*/
|
||||
resetScrollState = () => {
|
||||
public resetScrollState = (): void => {
|
||||
this.scrollState = {
|
||||
stuckAtBottom: this.props.startAtBottom,
|
||||
};
|
||||
this._bottomGrowth = 0;
|
||||
this._pages = 0;
|
||||
this._scrollTimeout = new Timer(100);
|
||||
this._heightUpdateInProgress = false;
|
||||
this.bottomGrowth = 0;
|
||||
this.pages = 0;
|
||||
this.scrollTimeout = new Timer(100);
|
||||
this.heightUpdateInProgress = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* jump to the top of the content.
|
||||
*/
|
||||
scrollToTop = () => {
|
||||
this._getScrollNode().scrollTop = 0;
|
||||
this._saveScrollState();
|
||||
public scrollToTop = (): void => {
|
||||
this.getScrollNode().scrollTop = 0;
|
||||
this.saveScrollState();
|
||||
};
|
||||
|
||||
/**
|
||||
* jump to the bottom of the content.
|
||||
*/
|
||||
scrollToBottom = () => {
|
||||
public scrollToBottom = (): void => {
|
||||
// the easiest way to make sure that the scroll state is correctly
|
||||
// saved is to do the scroll, then save the updated state. (Calculating
|
||||
// it ourselves is hard, and we can't rely on an onScroll callback
|
||||
// happening, since there may be no user-visible change here).
|
||||
const sn = this._getScrollNode();
|
||||
const sn = this.getScrollNode();
|
||||
sn.scrollTop = sn.scrollHeight;
|
||||
this._saveScrollState();
|
||||
this.saveScrollState();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -527,18 +553,18 @@ export default class ScrollPanel extends React.Component {
|
|||
*
|
||||
* @param {number} mult: -1 to page up, +1 to page down
|
||||
*/
|
||||
scrollRelative = mult => {
|
||||
const scrollNode = this._getScrollNode();
|
||||
public scrollRelative = (mult: number): void => {
|
||||
const scrollNode = this.getScrollNode();
|
||||
const delta = mult * scrollNode.clientHeight * 0.9;
|
||||
scrollNode.scrollBy(0, delta);
|
||||
this._saveScrollState();
|
||||
this.saveScrollState();
|
||||
};
|
||||
|
||||
/**
|
||||
* Scroll up/down in response to a scroll key
|
||||
* @param {object} ev the keyboard event
|
||||
*/
|
||||
handleScrollKey = ev => {
|
||||
public handleScrollKey = (ev: KeyboardEvent) => {
|
||||
let isScrolling = false;
|
||||
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
||||
switch (roomAction) {
|
||||
|
@ -575,17 +601,17 @@ export default class ScrollPanel extends React.Component {
|
|||
* node (specifically, the bottom of it) will be positioned. If omitted, it
|
||||
* defaults to 0.
|
||||
*/
|
||||
scrollToToken = (scrollToken, pixelOffset, offsetBase) => {
|
||||
public scrollToToken = (scrollToken: string, pixelOffset: number, offsetBase: number): void => {
|
||||
pixelOffset = pixelOffset || 0;
|
||||
offsetBase = offsetBase || 0;
|
||||
|
||||
// set the trackedScrollToken so we can get the node through _getTrackedNode
|
||||
// set the trackedScrollToken so we can get the node through getTrackedNode
|
||||
this.scrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedScrollToken: scrollToken,
|
||||
};
|
||||
const trackedNode = this._getTrackedNode();
|
||||
const scrollNode = this._getScrollNode();
|
||||
const trackedNode = this.getTrackedNode();
|
||||
const scrollNode = this.getScrollNode();
|
||||
if (trackedNode) {
|
||||
// set the scrollTop to the position we want.
|
||||
// note though, that this might not succeed if the combination of offsetBase and pixelOffset
|
||||
|
@ -595,34 +621,34 @@ export default class ScrollPanel extends React.Component {
|
|||
// enough so it ends up in the top of the viewport.
|
||||
debuglog("scrollToken: setting scrollTop", { offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop });
|
||||
scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset;
|
||||
this._saveScrollState();
|
||||
this.saveScrollState();
|
||||
}
|
||||
};
|
||||
|
||||
_saveScrollState() {
|
||||
private saveScrollState(): void {
|
||||
if (this.props.stickyBottom && this.isAtBottom()) {
|
||||
this.scrollState = { stuckAtBottom: true };
|
||||
debuglog("saved stuckAtBottom state");
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollNode = this._getScrollNode();
|
||||
const scrollNode = this.getScrollNode();
|
||||
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
|
||||
|
||||
const itemlist = this._itemlist.current;
|
||||
const itemlist = this.itemlist.current;
|
||||
const messages = itemlist.children;
|
||||
let node = null;
|
||||
|
||||
// TODO: do a binary search here, as items are sorted by offsetTop
|
||||
// loop backwards, from bottom-most message (as that is the most common case)
|
||||
for (let i = messages.length - 1; i >= 0; --i) {
|
||||
if (!messages[i].dataset.scrollTokens) {
|
||||
if (!(messages[i] as HTMLElement).dataset.scrollTokens) {
|
||||
continue;
|
||||
}
|
||||
node = messages[i];
|
||||
// break at the first message (coming from the bottom)
|
||||
// that has it's offsetTop above the bottom of the viewport.
|
||||
if (this._topFromBottom(node) > viewportBottom) {
|
||||
if (this.topFromBottom(node) > viewportBottom) {
|
||||
// Use this node as the scrollToken
|
||||
break;
|
||||
}
|
||||
|
@ -634,7 +660,7 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
const scrollToken = node.dataset.scrollTokens.split(',')[0];
|
||||
debuglog("saving anchored scroll state to message", node && node.innerText, scrollToken);
|
||||
const bottomOffset = this._topFromBottom(node);
|
||||
const bottomOffset = this.topFromBottom(node);
|
||||
this.scrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedNode: node,
|
||||
|
@ -644,35 +670,35 @@ export default class ScrollPanel extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
async _restoreSavedScrollState() {
|
||||
private async restoreSavedScrollState(): Promise<void> {
|
||||
const scrollState = this.scrollState;
|
||||
|
||||
if (scrollState.stuckAtBottom) {
|
||||
const sn = this._getScrollNode();
|
||||
const sn = this.getScrollNode();
|
||||
if (sn.scrollTop !== sn.scrollHeight) {
|
||||
sn.scrollTop = sn.scrollHeight;
|
||||
}
|
||||
} else if (scrollState.trackedScrollToken) {
|
||||
const itemlist = this._itemlist.current;
|
||||
const trackedNode = this._getTrackedNode();
|
||||
const itemlist = this.itemlist.current;
|
||||
const trackedNode = this.getTrackedNode();
|
||||
if (trackedNode) {
|
||||
const newBottomOffset = this._topFromBottom(trackedNode);
|
||||
const newBottomOffset = this.topFromBottom(trackedNode);
|
||||
const bottomDiff = newBottomOffset - scrollState.bottomOffset;
|
||||
this._bottomGrowth += bottomDiff;
|
||||
this.bottomGrowth += bottomDiff;
|
||||
scrollState.bottomOffset = newBottomOffset;
|
||||
const newHeight = `${this._getListHeight()}px`;
|
||||
const newHeight = `${this.getListHeight()}px`;
|
||||
if (itemlist.style.height !== newHeight) {
|
||||
itemlist.style.height = newHeight;
|
||||
}
|
||||
debuglog("balancing height because messages below viewport grew by", bottomDiff);
|
||||
}
|
||||
}
|
||||
if (!this._heightUpdateInProgress) {
|
||||
this._heightUpdateInProgress = true;
|
||||
if (!this.heightUpdateInProgress) {
|
||||
this.heightUpdateInProgress = true;
|
||||
try {
|
||||
await this._updateHeight();
|
||||
await this.updateHeight();
|
||||
} finally {
|
||||
this._heightUpdateInProgress = false;
|
||||
this.heightUpdateInProgress = false;
|
||||
}
|
||||
} else {
|
||||
debuglog("not updating height because request already in progress");
|
||||
|
@ -680,11 +706,11 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
|
||||
// need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content?
|
||||
async _updateHeight() {
|
||||
private async updateHeight(): Promise<void> {
|
||||
// wait until user has stopped scrolling
|
||||
if (this._scrollTimeout.isRunning()) {
|
||||
if (this.scrollTimeout.isRunning()) {
|
||||
debuglog("updateHeight waiting for scrolling to end ... ");
|
||||
await this._scrollTimeout.finished();
|
||||
await this.scrollTimeout.finished();
|
||||
} else {
|
||||
debuglog("updateHeight getting straight to business, no scrolling going on.");
|
||||
}
|
||||
|
@ -694,14 +720,14 @@ export default class ScrollPanel extends React.Component {
|
|||
return;
|
||||
}
|
||||
|
||||
const sn = this._getScrollNode();
|
||||
const itemlist = this._itemlist.current;
|
||||
const contentHeight = this._getMessagesHeight();
|
||||
const sn = this.getScrollNode();
|
||||
const itemlist = this.itemlist.current;
|
||||
const contentHeight = this.getMessagesHeight();
|
||||
const minHeight = sn.clientHeight;
|
||||
const height = Math.max(minHeight, contentHeight);
|
||||
this._pages = Math.ceil(height / PAGE_SIZE);
|
||||
this._bottomGrowth = 0;
|
||||
const newHeight = `${this._getListHeight()}px`;
|
||||
this.pages = Math.ceil(height / PAGE_SIZE);
|
||||
this.bottomGrowth = 0;
|
||||
const newHeight = `${this.getListHeight()}px`;
|
||||
|
||||
const scrollState = this.scrollState;
|
||||
if (scrollState.stuckAtBottom) {
|
||||
|
@ -713,7 +739,7 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
debuglog("updateHeight to", newHeight);
|
||||
} else if (scrollState.trackedScrollToken) {
|
||||
const trackedNode = this._getTrackedNode();
|
||||
const trackedNode = this.getTrackedNode();
|
||||
// if the timeline has been reloaded
|
||||
// this can be called before scrollToBottom or whatever has been called
|
||||
// so don't do anything if the node has disappeared from
|
||||
|
@ -735,17 +761,17 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getTrackedNode() {
|
||||
private getTrackedNode(): HTMLElement {
|
||||
const scrollState = this.scrollState;
|
||||
const trackedNode = scrollState.trackedNode;
|
||||
|
||||
if (!trackedNode || !trackedNode.parentElement) {
|
||||
let node;
|
||||
const messages = this._itemlist.current.children;
|
||||
const messages = this.itemlist.current.children;
|
||||
const scrollToken = scrollState.trackedScrollToken;
|
||||
|
||||
for (let i = messages.length-1; i >= 0; --i) {
|
||||
const m = messages[i];
|
||||
const m = messages[i] as HTMLElement;
|
||||
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
|
||||
// There might only be one scroll token
|
||||
if (m.dataset.scrollTokens &&
|
||||
|
@ -768,45 +794,45 @@ export default class ScrollPanel extends React.Component {
|
|||
return scrollState.trackedNode;
|
||||
}
|
||||
|
||||
_getListHeight() {
|
||||
return this._bottomGrowth + (this._pages * PAGE_SIZE);
|
||||
private getListHeight(): number {
|
||||
return this.bottomGrowth + (this.pages * PAGE_SIZE);
|
||||
}
|
||||
|
||||
_getMessagesHeight() {
|
||||
const itemlist = this._itemlist.current;
|
||||
const lastNode = itemlist.lastElementChild;
|
||||
private getMessagesHeight(): number {
|
||||
const itemlist = this.itemlist.current;
|
||||
const lastNode = itemlist.lastElementChild as HTMLElement;
|
||||
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
|
||||
const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
|
||||
const firstNodeTop = itemlist.firstElementChild ? (itemlist.firstElementChild as HTMLElement).offsetTop : 0;
|
||||
// 18 is itemlist padding
|
||||
return lastNodeBottom - firstNodeTop + (18 * 2);
|
||||
}
|
||||
|
||||
_topFromBottom(node) {
|
||||
private topFromBottom(node: HTMLElement): number {
|
||||
// current capped height - distance from top = distance from bottom of container to top of tracked element
|
||||
return this._itemlist.current.clientHeight - node.offsetTop;
|
||||
return this.itemlist.current.clientHeight - node.offsetTop;
|
||||
}
|
||||
|
||||
/* get the DOM node which has the scrollTop property we care about for our
|
||||
* message panel.
|
||||
*/
|
||||
_getScrollNode() {
|
||||
private getScrollNode(): HTMLDivElement {
|
||||
if (this.unmounted) {
|
||||
// this shouldn't happen, but when it does, turn the NPE into
|
||||
// something more meaningful.
|
||||
throw new Error("ScrollPanel._getScrollNode called when unmounted");
|
||||
throw new Error("ScrollPanel.getScrollNode called when unmounted");
|
||||
}
|
||||
|
||||
if (!this._divScroll) {
|
||||
if (!this.divScroll) {
|
||||
// Likewise, we should have the ref by this point, but if not
|
||||
// turn the NPE into something meaningful.
|
||||
throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected");
|
||||
throw new Error("ScrollPanel.getScrollNode called before AutoHideScrollbar ref collected");
|
||||
}
|
||||
|
||||
return this._divScroll;
|
||||
return this.divScroll;
|
||||
}
|
||||
|
||||
_collectScroll = divScroll => {
|
||||
this._divScroll = divScroll;
|
||||
private collectScroll = (divScroll: HTMLDivElement) => {
|
||||
this.divScroll = divScroll;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -814,15 +840,15 @@ export default class ScrollPanel extends React.Component {
|
|||
anything below it changes, by calling updatePreventShrinking, to keep
|
||||
the same minimum bottom offset, effectively preventing the timeline to shrink.
|
||||
*/
|
||||
preventShrinking = () => {
|
||||
const messageList = this._itemlist.current;
|
||||
public preventShrinking = (): void => {
|
||||
const messageList = this.itemlist.current;
|
||||
const tiles = messageList && messageList.children;
|
||||
if (!messageList) {
|
||||
return;
|
||||
}
|
||||
let lastTileNode;
|
||||
for (let i = tiles.length - 1; i >= 0; i--) {
|
||||
const node = tiles[i];
|
||||
const node = tiles[i] as HTMLElement;
|
||||
if (node.dataset.scrollTokens) {
|
||||
lastTileNode = node;
|
||||
break;
|
||||
|
@ -841,8 +867,8 @@ export default class ScrollPanel extends React.Component {
|
|||
};
|
||||
|
||||
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
|
||||
clearPreventShrinking = () => {
|
||||
const messageList = this._itemlist.current;
|
||||
public clearPreventShrinking = (): void => {
|
||||
const messageList = this.itemlist.current;
|
||||
const balanceElement = messageList && messageList.parentElement;
|
||||
if (balanceElement) balanceElement.style.paddingBottom = null;
|
||||
this.preventShrinkingState = null;
|
||||
|
@ -857,11 +883,11 @@ export default class ScrollPanel extends React.Component {
|
|||
from the bottom of the marked tile grows larger than
|
||||
what it was when marking.
|
||||
*/
|
||||
updatePreventShrinking = () => {
|
||||
public updatePreventShrinking = (): void => {
|
||||
if (this.preventShrinkingState) {
|
||||
const sn = this._getScrollNode();
|
||||
const sn = this.getScrollNode();
|
||||
const scrollState = this.scrollState;
|
||||
const messageList = this._itemlist.current;
|
||||
const messageList = this.itemlist.current;
|
||||
const { offsetNode, offsetFromBottom } = this.preventShrinkingState;
|
||||
// element used to set paddingBottom to balance the typing notifs disappearing
|
||||
const balanceElement = messageList.parentElement;
|
||||
|
@ -898,13 +924,15 @@ export default class ScrollPanel extends React.Component {
|
|||
// list-style-type: none; is no longer a list
|
||||
return (
|
||||
<AutoHideScrollbar
|
||||
wrappedRef={this._collectScroll}
|
||||
wrappedRef={this.collectScroll}
|
||||
onScroll={this.onScroll}
|
||||
onWheel={this.props.onUserScroll}
|
||||
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
||||
className={`mx_ScrollPanel ${this.props.className}`}
|
||||
style={this.props.style}
|
||||
>
|
||||
{ this.props.fixedChildren }
|
||||
<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">
|
||||
{ this.props.children }
|
||||
</ol>
|
||||
</div>
|
|
@ -58,7 +58,7 @@ export interface ISpaceSummaryRoom {
|
|||
avatar_url?: string;
|
||||
guest_can_join: boolean;
|
||||
name?: string;
|
||||
num_joined_members: number
|
||||
num_joined_members: number;
|
||||
room_id: string;
|
||||
topic?: string;
|
||||
world_readable: boolean;
|
||||
|
@ -112,12 +112,12 @@ const Tile: React.FC<ITileProps> = ({
|
|||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onViewRoomClick(false);
|
||||
}
|
||||
};
|
||||
const onJoinClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onViewRoomClick(true);
|
||||
}
|
||||
};
|
||||
|
||||
let button;
|
||||
if (joinedRoom) {
|
||||
|
@ -137,7 +137,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
} else {
|
||||
checkbox = <TextWithTooltip
|
||||
tooltip={_t("You don't have permission")}
|
||||
onClick={ev => { ev.stopPropagation() }}
|
||||
onClick={ev => { ev.stopPropagation(); }}
|
||||
>
|
||||
<StyledCheckbox disabled={true} />
|
||||
</TextWithTooltip>;
|
||||
|
@ -340,7 +340,7 @@ export const HierarchyLevel = ({
|
|||
</Tile>
|
||||
))
|
||||
}
|
||||
</React.Fragment>
|
||||
</React.Fragment>;
|
||||
};
|
||||
|
||||
// mutate argument refreshToken to force a reload
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import React, { RefObject, useContext, useRef, useState } from "react";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EventSubscription } from "fbemitter";
|
||||
|
||||
|
@ -34,8 +35,9 @@ import {useEventEmitter} from "../../hooks/useEventEmitter";
|
|||
import withValidation from "../views/elements/Validation";
|
||||
import * as Email from "../../email";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier"
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import MainSplit from './MainSplit';
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
|
@ -45,7 +47,7 @@ import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
|||
import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import { useStateArray } from "../../hooks/useStateArray";
|
||||
import SpacePublicShare from "../views/spaces/SpacePublicShare";
|
||||
import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
|
||||
import { shouldShowSpaceSettings, showAddExistingRooms, showCreateNewRoom, showSpaceSettings } from "../../utils/space";
|
||||
import { showRoom, SpaceHierarchy } from "./SpaceRoomDirectory";
|
||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
||||
import { useStateToggle } from "../../hooks/useStateToggle";
|
||||
|
@ -61,11 +63,11 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
|||
import { BetaPill } from "../views/beta/BetaCard";
|
||||
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import Modal from "../../Modal";
|
||||
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||
import { JoinRule } from "../views/settings/tabs/room/SecurityRoomSettingsTab";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -160,7 +162,7 @@ const SpaceInfo = ({ space }) => {
|
|||
</AccessibleButton>
|
||||
) : null}
|
||||
</RoomMemberCount> }
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const onBetaClick = () => {
|
||||
|
@ -178,6 +180,9 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
|
||||
const spacesEnabled = SettingsStore.getValue("feature_spaces");
|
||||
|
||||
const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave
|
||||
&& space.getJoinRule() !== JoinRule.Public;
|
||||
|
||||
let inviterSection;
|
||||
let joinButtons;
|
||||
if (myMembership === "join") {
|
||||
|
@ -244,7 +249,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
setBusy(true);
|
||||
onJoinButtonClicked();
|
||||
}}
|
||||
disabled={!spacesEnabled}
|
||||
disabled={!spacesEnabled || cannotJoin}
|
||||
>
|
||||
{ _t("Join") }
|
||||
</AccessibleButton>
|
||||
|
@ -255,6 +260,30 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
joinButtons = <InlineSpinner />;
|
||||
}
|
||||
|
||||
let footer;
|
||||
if (!spacesEnabled) {
|
||||
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
||||
{ myMembership === "join"
|
||||
? _t("To view %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
||||
spaceName: space.name,
|
||||
}, {
|
||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
||||
})
|
||||
: _t("To join %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
||||
spaceName: space.name,
|
||||
}, {
|
||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
} else if (cannotJoin) {
|
||||
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
||||
{ _t("To view %(spaceName)s, you need an invite", {
|
||||
spaceName: space.name,
|
||||
}) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceRoomView_preview">
|
||||
<BetaPill onClick={onBetaClick} />
|
||||
{ inviterSection }
|
||||
|
@ -274,20 +303,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
<div className="mx_SpaceRoomView_preview_joinButtons">
|
||||
{ joinButtons }
|
||||
</div>
|
||||
{ !spacesEnabled && <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
||||
{ myMembership === "join"
|
||||
? _t("To view %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
||||
spaceName: space.name,
|
||||
}, {
|
||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
||||
})
|
||||
: _t("To join %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
||||
spaceName: space.name,
|
||||
}, {
|
||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
||||
})
|
||||
}
|
||||
</div> }
|
||||
{ footer }
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
@ -576,14 +592,14 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
|
|||
|
||||
<AccessibleButton
|
||||
className="mx_SpaceRoomView_privateScope_justMeButton"
|
||||
onClick={() => { onFinished(false) }}
|
||||
onClick={() => { onFinished(false); }}
|
||||
>
|
||||
<h3>{ _t("Just me") }</h3>
|
||||
<div>{ _t("A private space to organise your rooms") }</div>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className="mx_SpaceRoomView_privateScope_meAndMyTeammatesButton"
|
||||
onClick={() => { onFinished(true) }}
|
||||
onClick={() => { onFinished(true); }}
|
||||
>
|
||||
<h3>{ _t("Me and my teammates") }</h3>
|
||||
<div>{ _t("A private space for you and your teammates") }</div>
|
||||
|
@ -670,7 +686,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
|||
let buttonLabel = _t("Skip for now");
|
||||
if (emailAddresses.some(name => name.trim())) {
|
||||
onClick = onNextClick;
|
||||
buttonLabel = busy ? _t("Inviting...") : _t("Continue")
|
||||
buttonLabel = busy ? _t("Inviting...") : _t("Continue");
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceRoomView_inviteTeammates">
|
||||
|
|
|
@ -26,6 +26,7 @@ import ProgressBar from "../views/elements/ProgressBar";
|
|||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import { IUpload } from "../../models/IUpload";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -38,6 +39,8 @@ interface IState {
|
|||
|
||||
@replaceableComponent("structures.UploadBar")
|
||||
export default class UploadBar extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
private dispatcherRef: string;
|
||||
private mounted: boolean;
|
||||
|
||||
|
@ -82,7 +85,7 @@ export default class UploadBar extends React.Component<IProps, IState> {
|
|||
|
||||
private onCancelClick = (ev) => {
|
||||
ev.preventDefault();
|
||||
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise);
|
||||
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise, this.context);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
@ -123,7 +123,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
|
||||
private onRoom = (room: Room): void => {
|
||||
this.removePendingJoinRoom(room.roomId);
|
||||
}
|
||||
};
|
||||
|
||||
private onTagStoreUpdate = () => {
|
||||
this.forceUpdate(); // we don't have anything useful in state to update
|
||||
|
@ -185,7 +185,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
if (this.state.pendingRoomJoin.delete(roomId)) {
|
||||
this.setState({
|
||||
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -357,7 +357,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
),
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
} else if (hostSignupConfig) {
|
||||
if (hostSignupConfig && hostSignupConfig.url) {
|
||||
// If hostSignup.domains is set to a non-empty array, only show
|
||||
|
@ -509,7 +509,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</React.Fragment>
|
||||
)
|
||||
);
|
||||
} else if (MatrixClientPeg.get().isGuest()) {
|
||||
primaryOptionList = (
|
||||
<React.Fragment>
|
||||
|
|
|
@ -501,9 +501,9 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
|||
return <React.Fragment>
|
||||
{ flows.map(flow => {
|
||||
const stepRenderer = this.stepRendererMap[flow.type];
|
||||
return <React.Fragment key={flow.type}>{ stepRenderer() }</React.Fragment>
|
||||
return <React.Fragment key={flow.type}>{ stepRenderer() }</React.Fragment>;
|
||||
}) }
|
||||
</React.Fragment>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
private renderPasswordStep = () => {
|
||||
|
|
|
@ -49,7 +49,7 @@ interface IProps {
|
|||
// for operations like uploading cross-signing keys).
|
||||
onLoggedIn(params: {
|
||||
userId: string;
|
||||
deviceId: string
|
||||
deviceId: string;
|
||||
homeserverUrl: string;
|
||||
identityServerUrl?: string;
|
||||
accessToken: string;
|
||||
|
@ -267,7 +267,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
session_id: sessionId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private onUIAuthFinished = async (success: boolean, response: any) => {
|
||||
if (!success) {
|
||||
|
@ -487,7 +487,13 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
/>
|
||||
<h3 className="mx_AuthBody_centered">
|
||||
{ _t("%(ssoButtons)s Or %(usernamePassword)s", { ssoButtons: "", usernamePassword: ""}).trim() }
|
||||
{_t(
|
||||
"%(ssoButtons)s Or %(usernamePassword)s",
|
||||
{
|
||||
ssoButtons: "",
|
||||
usernamePassword: "",
|
||||
},
|
||||
).trim()}
|
||||
</h3>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,33 +15,43 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
|
||||
import * as sdk from '../../../index';
|
||||
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ISecretStorageKeyInfo } from 'matrix-js-sdk';
|
||||
import EncryptionPanel from "../../views/right_panel/EncryptionPanel";
|
||||
import AccessibleButton from '../../views/elements/AccessibleButton';
|
||||
import Spinner from '../../views/elements/Spinner';
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
function keyHasPassphrase(keyInfo) {
|
||||
return (
|
||||
function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean {
|
||||
return Boolean(
|
||||
keyInfo.passphrase &&
|
||||
keyInfo.passphrase.salt &&
|
||||
keyInfo.passphrase.iterations
|
||||
keyInfo.passphrase.iterations,
|
||||
);
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.SetupEncryptionBody")
|
||||
export default class SetupEncryptionBody extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
onFinished: (boolean) => void;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
verificationRequest: VerificationRequest;
|
||||
backupInfo: IKeyBackupInfo;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.SetupEncryptionBody")
|
||||
export default class SetupEncryptionBody extends React.Component<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.on("update", this._onStoreUpdate);
|
||||
store.on("update", this.onStoreUpdate);
|
||||
store.start();
|
||||
this.state = {
|
||||
phase: store.phase,
|
||||
|
@ -53,10 +63,10 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
_onStoreUpdate = () => {
|
||||
private onStoreUpdate = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
if (store.phase === Phase.Finished) {
|
||||
this.props.onFinished();
|
||||
this.props.onFinished(true);
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
|
@ -66,18 +76,18 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount() {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.off("update", this._onStoreUpdate);
|
||||
store.off("update", this.onStoreUpdate);
|
||||
store.stop();
|
||||
}
|
||||
|
||||
_onUsePassphraseClick = async () => {
|
||||
private onUsePassphraseClick = async () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.usePassPhrase();
|
||||
}
|
||||
};
|
||||
|
||||
_onVerifyClick = () => {
|
||||
private onVerifyClick = () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const userId = cli.getUserId();
|
||||
const requestPromise = cli.requestVerification(userId);
|
||||
|
@ -91,42 +101,44 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
request.cancel();
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onSkipClick = () => {
|
||||
private onSkipClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.skip();
|
||||
}
|
||||
};
|
||||
|
||||
onSkipConfirmClick = () => {
|
||||
private onSkipConfirmClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.skipConfirm();
|
||||
}
|
||||
};
|
||||
|
||||
onSkipBackClick = () => {
|
||||
private onSkipBackClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.returnAfterSkip();
|
||||
}
|
||||
};
|
||||
|
||||
onDoneClick = () => {
|
||||
private onDoneClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.done();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
private onEncryptionPanelClose = () => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const {
|
||||
phase,
|
||||
} = this.state;
|
||||
|
||||
if (this.state.verificationRequest) {
|
||||
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
|
||||
return <EncryptionPanel
|
||||
layout="dialog"
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
onClose={this.props.onFinished}
|
||||
onClose={this.onEncryptionPanelClose}
|
||||
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
|
||||
isRoomEncrypted={false}
|
||||
/>;
|
||||
} else if (phase === Phase.Intro) {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
|
@ -139,14 +151,14 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
|
||||
let useRecoveryKeyButton;
|
||||
if (recoveryKeyPrompt) {
|
||||
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this._onUsePassphraseClick}>
|
||||
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this.onUsePassphraseClick}>
|
||||
{recoveryKeyPrompt}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let verifyButton;
|
||||
if (store.hasDevicesToVerifyAgainst) {
|
||||
verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}>
|
||||
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
|
||||
{ _t("Use another login") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
@ -217,7 +229,6 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
</div>
|
||||
);
|
||||
} else if (phase === Phase.Busy || phase === Phase.Loading) {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
return <Spinner />;
|
||||
} else {
|
||||
console.log(`SetupEncryptionBody: Unknown phase ${phase}`);
|
|
@ -49,7 +49,7 @@ interface IProps {
|
|||
fragmentAfterLogin?: string;
|
||||
|
||||
// Called when the SSO login completes
|
||||
onTokenLoginCompleted: () => void,
|
||||
onTokenLoginCompleted: () => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
124
src/components/views/audio_messages/AudioPlayer.tsx
Normal file
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import { Playback, PlaybackState } from "../../../voice/Playback";
|
||||
import React, { createRef, ReactNode, RefObject } from "react";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import PlayPauseButton from "./PlayPauseButton";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { formatBytes } from "../../../utils/FormattingUtils";
|
||||
import DurationClock from "./DurationClock";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SeekBar from "./SeekBar";
|
||||
import PlaybackClock from "./PlaybackClock";
|
||||
|
||||
interface IProps {
|
||||
// Playback instance to render. Cannot change during component lifecycle: create
|
||||
// an all-new component instead.
|
||||
playback: Playback;
|
||||
|
||||
mediaName: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
playbackPhase: PlaybackState;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.audio_messages.AudioPlayer")
|
||||
export default class AudioPlayer extends React.PureComponent<IProps, IState> {
|
||||
private playPauseRef: RefObject<PlayPauseButton> = createRef();
|
||||
private seekRef: RefObject<SeekBar> = createRef();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
playbackPhase: PlaybackState.Decoding, // default assumption
|
||||
};
|
||||
|
||||
// We don't need to de-register: the class handles this for us internally
|
||||
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
|
||||
|
||||
// Don't wait for the promise to complete - it will emit a progress update when it
|
||||
// is done, and it's not meant to take long anyhow.
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.props.playback.prepare();
|
||||
}
|
||||
|
||||
private onPlaybackUpdate = (ev: PlaybackState) => {
|
||||
this.setState({ playbackPhase: ev });
|
||||
};
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
// stopPropagation() prevents the FocusComposer catch-all from triggering,
|
||||
// but we need to do it on key down instead of press (even though the user
|
||||
// interaction is typically on press).
|
||||
if (ev.key === Key.SPACE) {
|
||||
ev.stopPropagation();
|
||||
this.playPauseRef.current?.toggleState();
|
||||
} else if (ev.key === Key.ARROW_LEFT) {
|
||||
ev.stopPropagation();
|
||||
this.seekRef.current?.left();
|
||||
} else if (ev.key === Key.ARROW_RIGHT) {
|
||||
ev.stopPropagation();
|
||||
this.seekRef.current?.right();
|
||||
}
|
||||
};
|
||||
|
||||
protected renderFileSize(): string {
|
||||
const bytes = this.props.playback.sizeBytes;
|
||||
if (!bytes) return null;
|
||||
|
||||
// Not translated here - we're just presenting the data which should already
|
||||
// be translated if needed.
|
||||
return `(${formatBytes(bytes)})`;
|
||||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
// tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard
|
||||
// events for accessibility
|
||||
return <div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}>
|
||||
<div className='mx_AudioPlayer_primaryContainer'>
|
||||
<PlayPauseButton
|
||||
playback={this.props.playback}
|
||||
playbackPhase={this.state.playbackPhase}
|
||||
tabIndex={-1} // prevent tabbing into the button
|
||||
ref={this.playPauseRef}
|
||||
/>
|
||||
<div className='mx_AudioPlayer_mediaInfo'>
|
||||
<span className='mx_AudioPlayer_mediaName'>
|
||||
{this.props.mediaName || _t("Unnamed audio")}
|
||||
</span>
|
||||
<div className='mx_AudioPlayer_byline'>
|
||||
<DurationClock playback={this.props.playback} />
|
||||
{/* easiest way to introduce a gap between the components */}
|
||||
{ this.renderFileSize() }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx_AudioPlayer_seek'>
|
||||
<SeekBar
|
||||
playback={this.props.playback}
|
||||
tabIndex={-1} // prevent tabbing into the bar
|
||||
playbackPhase={this.state.playbackPhase}
|
||||
ref={this.seekRef}
|
||||
/>
|
||||
<PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
export interface IProps {
|
||||
seconds: number;
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ interface IState {
|
|||
* Simply converts seconds into minutes and seconds. Note that hours will not be
|
||||
* displayed, making it possible to see "82:29".
|
||||
*/
|
||||
@replaceableComponent("views.voice_messages.Clock")
|
||||
@replaceableComponent("views.audio_messages.Clock")
|
||||
export default class Clock extends React.Component<IProps, IState> {
|
||||
public constructor(props) {
|
||||
super(props);
|
55
src/components/views/audio_messages/DurationClock.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Clock from "./Clock";
|
||||
import { Playback } from "../../../voice/Playback";
|
||||
|
||||
interface IProps {
|
||||
playback: Playback;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
durationSeconds: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A clock which shows a clip's maximum duration.
|
||||
*/
|
||||
@replaceableComponent("views.audio_messages.DurationClock")
|
||||
export default class DurationClock extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
// we track the duration on state because we won't really know what the clip duration
|
||||
// is until the first time update, and as a PureComponent we are trying to dedupe state
|
||||
// updates as much as possible. This is just the easiest way to avoid a forceUpdate() or
|
||||
// member property to track "did we get a duration".
|
||||
durationSeconds: this.props.playback.clockInfo.durationSeconds,
|
||||
};
|
||||
this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate);
|
||||
}
|
||||
|
||||
private onTimeUpdate = (time: number[]) => {
|
||||
this.setState({ durationSeconds: time[1] });
|
||||
};
|
||||
|
||||
public render() {
|
||||
return <Clock seconds={this.state.durationSeconds} />;
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ import React from "react";
|
|||
import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Clock from "./Clock";
|
||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||
|
||||
interface IProps {
|
||||
recorder: VoiceRecording;
|
||||
|
@ -30,18 +31,33 @@ interface IState {
|
|||
/**
|
||||
* A clock for a live recording.
|
||||
*/
|
||||
@replaceableComponent("views.voice_messages.LiveRecordingClock")
|
||||
@replaceableComponent("views.audio_messages.LiveRecordingClock")
|
||||
export default class LiveRecordingClock extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
private seconds = 0;
|
||||
private scheduledUpdate = new MarkedExecution(
|
||||
() => this.updateClock(),
|
||||
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
||||
);
|
||||
|
||||
this.state = {seconds: 0};
|
||||
this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
seconds: 0,
|
||||
};
|
||||
}
|
||||
|
||||
private onRecordingUpdate = (update: IRecordingUpdate) => {
|
||||
this.setState({seconds: update.timeSeconds});
|
||||
};
|
||||
componentDidMount() {
|
||||
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
|
||||
this.seconds = update.timeSeconds;
|
||||
this.scheduledUpdate.mark();
|
||||
});
|
||||
}
|
||||
|
||||
private updateClock() {
|
||||
this.setState({
|
||||
seconds: this.seconds,
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <Clock seconds={this.state.seconds} />;
|