diff --git a/.eslintrc.js b/.eslintrc.js index 827b373949..9d68942228 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -63,6 +63,11 @@ module.exports = { "@typescript-eslint/ban-ts-comment": "off", }, }], + settings: { + react: { + version: "detect", + } + } }; function buildRestrictedPropertiesOptions(properties, message) { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..2c068fff33 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @matrix-org/element-web diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 0ae59da09a..4f9826391a 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -10,6 +10,8 @@ on: jobs: end-to-end: runs-on: ubuntu-latest + env: + PR_NUMBER: ${{github.event.number}} container: vectorim/element-web-ci-e2etests-env:latest steps: - name: Checkout code diff --git a/.github/workflows/layered-build.yaml b/.github/workflows/layered-build.yaml new file mode 100644 index 0000000000..c9d7e89a75 --- /dev/null +++ b/.github/workflows/layered-build.yaml @@ -0,0 +1,31 @@ +name: Layered Preview Build +on: + pull_request: + branches: [develop] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Build + run: scripts/ci/layered.sh && cd element-web && cp element.io/develop/config.json config.json && CI_PACKAGE=true yarn build + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: previewbuild + path: element-web/webapp + # We'll only use this in a triggered job, then we're done with it + retention-days: 1 + - uses: actions/github-script@v3.1.0 + with: + script: | + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request)); + - name: Upload PR Info + uses: actions/upload-artifact@v2 + with: + name: pr.json + path: pr.json + # We'll only use this in a triggered job, then we're done with it + retention-days: 1 + diff --git a/.github/workflows/netlify.yaml b/.github/workflows/netlify.yaml new file mode 100644 index 0000000000..a6a408bdbd --- /dev/null +++ b/.github/workflows/netlify.yaml @@ -0,0 +1,80 @@ +name: Upload Preview Build to Netlify +on: + workflow_run: + workflows: ["Layered Preview Build"] + types: + - completed +jobs: + build: + runs-on: ubuntu-latest + if: > + ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + # There's a 'download artifact' action but it hasn't been updated for the + # workflow_run action (https://github.com/actions/download-artifact/issues/60) + # so instead we get this mess: + - name: 'Download artifact' + uses: actions/github-script@v3.1.0 + with: + script: | + var artifacts = await github.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + var matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "previewbuild" + })[0]; + var download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data)); + + var prInfoArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "pr.json" + })[0]; + var download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: prInfoArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data)); + - name: Extract Artifacts + run: unzip -d webapp previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip + - name: 'Read PR Info' + id: readctx + uses: actions/github-script@v3.1.0 + with: + script: | + var fs = require('fs'); + var pr = JSON.parse(fs.readFileSync('${{github.workspace}}/pr.json')); + console.log(`::set-output name=prnumber::${pr.number}`); + - name: Deploy to Netlify + id: netlify + uses: nwtgck/actions-netlify@v1.2 + with: + publish-dir: webapp + deploy-message: "Deploy from GitHub Actions" + # These don't work because we're in workflow_run + enable-pull-request-comment: false + enable-commit-comment: false + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + timeout-minutes: 1 + - name: Edit PR Description + uses: velas/pr-description@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + pull-request-number: ${{ steps.readctx.outputs.prnumber }} + description-message: | + Preview: ${{ steps.netlify.outputs.deploy-url }} + ⚠️ Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts. + diff --git a/.github/workflows/preview_changelog.yaml b/.github/workflows/preview_changelog.yaml new file mode 100644 index 0000000000..d68d19361d --- /dev/null +++ b/.github/workflows/preview_changelog.yaml @@ -0,0 +1,12 @@ +name: Preview Changelog +on: + pull_request_target: + types: [ opened, edited, labeled ] +jobs: + changelog: + runs-on: ubuntu-latest + steps: + - name: Preview Changelog + uses: matrix-org/allchange@main + with: + ghToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/typecheck.yaml b/.github/workflows/typecheck.yaml new file mode 100644 index 0000000000..2e08418cf6 --- /dev/null +++ b/.github/workflows/typecheck.yaml @@ -0,0 +1,24 @@ +name: Type Check +on: + pull_request: + branches: [develop] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: c-hive/gha-yarn-cache@v2 + - name: Install Deps + run: "./scripts/ci/install-deps.sh --ignore-scripts" + - name: Typecheck + run: "yarn run lint:types" + - name: Switch js-sdk to release mode + run: | + scripts/ci/js-sdk-to-release.js + cd node_modules/matrix-js-sdk + yarn install + yarn run build:compile + yarn run build:types + - name: Typecheck (release mode) + run: "yarn run lint:types" + diff --git a/.node-version b/.node-version new file mode 100644 index 0000000000..8351c19397 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +14 diff --git a/.stylelintrc.js b/.stylelintrc.js index 0e6de7000f..c044b19a63 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -17,6 +17,7 @@ module.exports = { "selector-list-comma-newline-after": null, "at-rule-no-unknown": null, "no-descending-specificity": null, + "no-empty-first-line": true, "scss/at-rule-no-unknown": [true, { // https://github.com/vector-im/element-web/issues/10544 "ignoreAtRules": ["define-mixin"], diff --git a/CHANGELOG.md b/CHANGELOG.md index 73b383d76d..42e186220f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,238 @@ +Changes in [3.29.0](https://github.com/vector-im/element-desktop/releases/tag/v3.29.0) (2021-08-31) +=================================================================================================== + +## ✨ Features + * [Release]Increase general app performance by optimizing layers ([\#6672](https://github.com/matrix-org/matrix-react-sdk/pull/6672)). Fixes vector-im/element-web#18730 and vector-im/element-web#18730. Contributed by [Palid](https://github.com/Palid). + * Add a warning on E2EE rooms if you try to make them public ([\#5698](https://github.com/matrix-org/matrix-react-sdk/pull/5698)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Allow pagination of the space hierarchy and use new APIs ([\#6507](https://github.com/matrix-org/matrix-react-sdk/pull/6507)). Fixes vector-im/element-web#18089 and vector-im/element-web#18427. + * Improve emoji in composer ([\#6650](https://github.com/matrix-org/matrix-react-sdk/pull/6650)). Fixes vector-im/element-web#18593 and vector-im/element-web#18593. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Allow playback of replied-to voice message ([\#6629](https://github.com/matrix-org/matrix-react-sdk/pull/6629)). Fixes vector-im/element-web#18599 and vector-im/element-web#18599. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Format autocomplete suggestions vertically ([\#6620](https://github.com/matrix-org/matrix-react-sdk/pull/6620)). Fixes vector-im/element-web#17574 and vector-im/element-web#17574. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Remember last `MemberList` search query per-room ([\#6640](https://github.com/matrix-org/matrix-react-sdk/pull/6640)). Fixes vector-im/element-web#18613 and vector-im/element-web#18613. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Sentry rageshakes ([\#6597](https://github.com/matrix-org/matrix-react-sdk/pull/6597)). Fixes vector-im/element-web#11111 and vector-im/element-web#11111. Contributed by [novocaine](https://github.com/novocaine). + * Autocomplete has been updated to match modern accessibility standards. Navigate via up/down arrows rather than Tab. Enter or Tab to confirm a suggestion. This should be familiar to Slack & Discord users. You can now use Tab to navigate around the application and do more without touching your mouse. No more accidentally sending half of people's names because the completion didn't fire on Enter! ([\#5659](https://github.com/matrix-org/matrix-react-sdk/pull/5659)). Fixes vector-im/element-web#4872, vector-im/element-web#11071, vector-im/element-web#17171, vector-im/element-web#15646 vector-im/element-web#4872 and vector-im/element-web#4872. + * Add new call tile states ([\#6610](https://github.com/matrix-org/matrix-react-sdk/pull/6610)). Fixes vector-im/element-web#18521 and vector-im/element-web#18521. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Left align call tiles ([\#6609](https://github.com/matrix-org/matrix-react-sdk/pull/6609)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Make loading encrypted images look snappier ([\#6590](https://github.com/matrix-org/matrix-react-sdk/pull/6590)). Fixes vector-im/element-web#17878 and vector-im/element-web#17862. Contributed by [Palid](https://github.com/Palid). + * Offer a way to create a space based on existing community ([\#6543](https://github.com/matrix-org/matrix-react-sdk/pull/6543)). Fixes vector-im/element-web#18092. + * Accessibility improvements in and around Spaces ([\#6569](https://github.com/matrix-org/matrix-react-sdk/pull/6569)). Fixes vector-im/element-web#18094 and vector-im/element-web#18094. + +## 🐛 Bug Fixes + * [Release] Fix commit edit history ([\#6690](https://github.com/matrix-org/matrix-react-sdk/pull/6690)). Fixes vector-im/element-web#18742 and vector-im/element-web#18742. Contributed by [Palid](https://github.com/Palid). + * Fix images not rendering when sent from other clients. ([\#6661](https://github.com/matrix-org/matrix-react-sdk/pull/6661)). Fixes vector-im/element-web#18702 and vector-im/element-web#18702. + * Fix autocomplete scrollbar and make the autocomplete a little smaller ([\#6655](https://github.com/matrix-org/matrix-react-sdk/pull/6655)). Fixes vector-im/element-web#18682 and vector-im/element-web#18682. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix replies on the bubble layout ([\#6451](https://github.com/matrix-org/matrix-react-sdk/pull/6451)). Fixes vector-im/element-web#18184. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Show "Enable encryption in settings" only when the user can do that ([\#6646](https://github.com/matrix-org/matrix-react-sdk/pull/6646)). Fixes vector-im/element-web#18646 and vector-im/element-web#18646. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix cross signing setup from settings screen ([\#6633](https://github.com/matrix-org/matrix-react-sdk/pull/6633)). Fixes vector-im/element-web#17761 and vector-im/element-web#17761. + * Fix call tiles on the bubble layout ([\#6647](https://github.com/matrix-org/matrix-react-sdk/pull/6647)). Fixes vector-im/element-web#18648 and vector-im/element-web#18648. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix error on accessing encrypted media without encryption keys ([\#6625](https://github.com/matrix-org/matrix-react-sdk/pull/6625)). Contributed by [Palid](https://github.com/Palid). + * Fix jitsi widget sometimes being permanently stuck in the bottom-right corner ([\#6632](https://github.com/matrix-org/matrix-react-sdk/pull/6632)). Fixes vector-im/element-web#17226 and vector-im/element-web#17226. Contributed by [Palid](https://github.com/Palid). + * Fix FilePanel pagination in E2EE rooms ([\#6630](https://github.com/matrix-org/matrix-react-sdk/pull/6630)). Fixes vector-im/element-web#18415 and vector-im/element-web#18415. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix call tile buttons ([\#6624](https://github.com/matrix-org/matrix-react-sdk/pull/6624)). Fixes vector-im/element-web#18565 and vector-im/element-web#18565. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix vertical call tile spacing issues ([\#6621](https://github.com/matrix-org/matrix-react-sdk/pull/6621)). Fixes vector-im/element-web#18558 and vector-im/element-web#18558. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix long display names in call tiles ([\#6618](https://github.com/matrix-org/matrix-react-sdk/pull/6618)). Fixes vector-im/element-web#18562 and vector-im/element-web#18562. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Avoid access token overflow ([\#6616](https://github.com/matrix-org/matrix-react-sdk/pull/6616)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Properly handle media errors ([\#6615](https://github.com/matrix-org/matrix-react-sdk/pull/6615)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix glare related regressions ([\#6614](https://github.com/matrix-org/matrix-react-sdk/pull/6614)). Fixes vector-im/element-web#18538 and vector-im/element-web#18538. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix long display names in call toasts ([\#6617](https://github.com/matrix-org/matrix-react-sdk/pull/6617)). Fixes vector-im/element-web#18557 and vector-im/element-web#18557. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix PiP of held calls ([\#6611](https://github.com/matrix-org/matrix-react-sdk/pull/6611)). Fixes vector-im/element-web#18539 and vector-im/element-web#18539. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix call tile behaviour on narrow layouts ([\#6556](https://github.com/matrix-org/matrix-react-sdk/pull/6556)). Fixes vector-im/element-web#18398. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix video call persisting when widget removed ([\#6608](https://github.com/matrix-org/matrix-react-sdk/pull/6608)). Fixes vector-im/element-web#15703 and vector-im/element-web#15703. + * Fix toast colors ([\#6606](https://github.com/matrix-org/matrix-react-sdk/pull/6606)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Remove tiny scrollbar dot from code blocks ([\#6596](https://github.com/matrix-org/matrix-react-sdk/pull/6596)). Fixes vector-im/element-web#18474. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Improve handling of pills in the composer ([\#6353](https://github.com/matrix-org/matrix-react-sdk/pull/6353)). Fixes vector-im/element-web#10134 vector-im/element-web#10896 and vector-im/element-web#15037. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + +Changes in [3.28.1](https://github.com/vector-im/element-desktop/releases/tag/v3.28.1) (2021-08-17) +=================================================================================================== + +## 🐛 Bug Fixes + * Fix multiple VoIP regressions ([matrix-org/matrix-js-sdk#1860](https://github.com/matrix-org/matrix-js-sdk/pull/1860)). + +Changes in [3.28.0](https://github.com/vector-im/element-desktop/releases/tag/v3.28.0) (2021-08-16) +=================================================================================================== + +## ✨ Features + * Show how long a call was on call tiles ([\#6570](https://github.com/matrix-org/matrix-react-sdk/pull/6570)). Fixes vector-im/element-web#18405. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Add regional indicators to emoji picker ([\#6490](https://github.com/matrix-org/matrix-react-sdk/pull/6490)). Fixes vector-im/element-web#14963. Contributed by [robintown](https://github.com/robintown). + * Make call control buttons accessible to screen reader users ([\#6181](https://github.com/matrix-org/matrix-react-sdk/pull/6181)). Fixes vector-im/element-web#18358. Contributed by [pvagner](https://github.com/pvagner). + * Skip sending a thumbnail if it is not a sufficient saving over the original ([\#6559](https://github.com/matrix-org/matrix-react-sdk/pull/6559)). Fixes vector-im/element-web#17906. + * Increase PiP snapping speed ([\#6539](https://github.com/matrix-org/matrix-react-sdk/pull/6539)). Fixes vector-im/element-web#18371. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Improve and move the incoming call toast ([\#6470](https://github.com/matrix-org/matrix-react-sdk/pull/6470)). Fixes vector-im/element-web#17912. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Allow all of the URL schemes that Firefox allows ([\#6457](https://github.com/matrix-org/matrix-react-sdk/pull/6457)). Contributed by [aaronraimist](https://github.com/aaronraimist). + * Improve bubble layout colors ([\#6452](https://github.com/matrix-org/matrix-react-sdk/pull/6452)). Fixes vector-im/element-web#18081. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Spaces let users switch between Home and All Rooms behaviours ([\#6497](https://github.com/matrix-org/matrix-react-sdk/pull/6497)). Fixes vector-im/element-web#18093. + * Support for MSC2285 (hidden read receipts) ([\#6390](https://github.com/matrix-org/matrix-react-sdk/pull/6390)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Group pinned message events with MELS ([\#6349](https://github.com/matrix-org/matrix-react-sdk/pull/6349)). Fixes vector-im/element-web#17938. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Make version copiable ([\#6227](https://github.com/matrix-org/matrix-react-sdk/pull/6227)). Fixes vector-im/element-web#17603 and vector-im/element-web#18329. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Improve voice messages uploading state ([\#6530](https://github.com/matrix-org/matrix-react-sdk/pull/6530)). Fixes vector-im/element-web#18226 and vector-im/element-web#18224. + * Add surround with feature ([\#5510](https://github.com/matrix-org/matrix-react-sdk/pull/5510)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Improve call event tile wording ([\#6545](https://github.com/matrix-org/matrix-react-sdk/pull/6545)). Fixes vector-im/element-web#18376. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Show an avatar/a turned off microphone icon for muted users ([\#6486](https://github.com/matrix-org/matrix-react-sdk/pull/6486)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Prompt user to leave rooms/subspaces in a space when leaving space ([\#6424](https://github.com/matrix-org/matrix-react-sdk/pull/6424)). Fixes vector-im/element-web#18071. + * Add customisation point to override widget variables ([\#6455](https://github.com/matrix-org/matrix-react-sdk/pull/6455)). Fixes vector-im/element-web#18035. + * Add support for screen sharing in 1:1 calls ([\#5992](https://github.com/matrix-org/matrix-react-sdk/pull/5992)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + +## 🐛 Bug Fixes + * [Release] Fix glare related regressions ([\#6622](https://github.com/matrix-org/matrix-react-sdk/pull/6622)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * [Release] Fix PiP of held calls ([\#6612](https://github.com/matrix-org/matrix-react-sdk/pull/6612)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * [Release] Fix toast colors ([\#6607](https://github.com/matrix-org/matrix-react-sdk/pull/6607)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix [object Object] in Widget Permissions ([\#6560](https://github.com/matrix-org/matrix-react-sdk/pull/6560)). Fixes vector-im/element-web#18384. Contributed by [Palid](https://github.com/Palid). + * Fix right margin for events on IRC layout ([\#6542](https://github.com/matrix-org/matrix-react-sdk/pull/6542)). Fixes vector-im/element-web#18354. + * Mirror only usermedia feeds ([\#6512](https://github.com/matrix-org/matrix-react-sdk/pull/6512)). Fixes vector-im/element-web#5633. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix LogoutDialog warning + TypeScript migration ([\#6533](https://github.com/matrix-org/matrix-react-sdk/pull/6533)). + * Fix the wrong font being used in the room topic field ([\#6527](https://github.com/matrix-org/matrix-react-sdk/pull/6527)). Fixes vector-im/element-web#18339. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix inconsistent styling for links on hover ([\#6513](https://github.com/matrix-org/matrix-react-sdk/pull/6513)). Contributed by [janogarcia](https://github.com/janogarcia). + * Fix incorrect height for encoded placeholder images ([\#6514](https://github.com/matrix-org/matrix-react-sdk/pull/6514)). Contributed by [Palid](https://github.com/Palid). + * Fix call events layout for message bubble ([\#6465](https://github.com/matrix-org/matrix-react-sdk/pull/6465)). Fixes vector-im/element-web#18144. + * Improve subspaces and some utilities around room/space creation ([\#6458](https://github.com/matrix-org/matrix-react-sdk/pull/6458)). Fixes vector-im/element-web#18090 vector-im/element-web#18091 and vector-im/element-web#17256. + * Restore pointer cursor for SenderProfile in message bubbles ([\#6501](https://github.com/matrix-org/matrix-react-sdk/pull/6501)). Fixes vector-im/element-web#18249. + * Fix issues with the Call View ([\#6472](https://github.com/matrix-org/matrix-react-sdk/pull/6472)). Fixes vector-im/element-web#18221. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Align event list summary read receipts when using message bubbles ([\#6500](https://github.com/matrix-org/matrix-react-sdk/pull/6500)). Fixes vector-im/element-web#18143. + * Better positioning for unbubbled events in timeline ([\#6477](https://github.com/matrix-org/matrix-react-sdk/pull/6477)). Fixes vector-im/element-web#18132. + * Realign reactions row with messages in modern layout ([\#6491](https://github.com/matrix-org/matrix-react-sdk/pull/6491)). Fixes vector-im/element-web#18118. Contributed by [robintown](https://github.com/robintown). + * Fix CreateRoomDialog exploding when making public room outside of a space ([\#6492](https://github.com/matrix-org/matrix-react-sdk/pull/6492)). Fixes vector-im/element-web#18275. + * Fix call crashing because `element` was undefined ([\#6488](https://github.com/matrix-org/matrix-react-sdk/pull/6488)). Fixes vector-im/element-web#18270. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Upscale thumbnails to the container size ([\#6589](https://github.com/matrix-org/matrix-react-sdk/pull/6589)). Fixes vector-im/element-web#18307. + * Fix create room dialog in spaces no longer adding to the space ([\#6587](https://github.com/matrix-org/matrix-react-sdk/pull/6587)). Fixes vector-im/element-web#18465. + * Don't show a modal on call reject/user hangup ([\#6580](https://github.com/matrix-org/matrix-react-sdk/pull/6580)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fade Call View Buttons after `componentDidMount` ([\#6581](https://github.com/matrix-org/matrix-react-sdk/pull/6581)). Fixes vector-im/element-web#18439. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix missing expand button on codeblocks ([\#6565](https://github.com/matrix-org/matrix-react-sdk/pull/6565)). Fixes vector-im/element-web#18388. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * allow customizing the bubble layout colors ([\#6568](https://github.com/matrix-org/matrix-react-sdk/pull/6568)). Fixes vector-im/element-web#18408. Contributed by [benneti](https://github.com/benneti). + * Don't flash "Missed call" when accepting a call ([\#6567](https://github.com/matrix-org/matrix-react-sdk/pull/6567)). Fixes vector-im/element-web#18404. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix clicking whitespaces on replies ([\#6571](https://github.com/matrix-org/matrix-react-sdk/pull/6571)). Fixes vector-im/element-web#18327. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix disabled state for voice messages + send button tooltip ([\#6562](https://github.com/matrix-org/matrix-react-sdk/pull/6562)). Fixes vector-im/element-web#18413. + * Fix voice feed being cut-off ([\#6550](https://github.com/matrix-org/matrix-react-sdk/pull/6550)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix sizing issues of the screen picker ([\#6498](https://github.com/matrix-org/matrix-react-sdk/pull/6498)). Fixes vector-im/element-web#18281. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Stop voice messages that are playing when starting a recording ([\#6563](https://github.com/matrix-org/matrix-react-sdk/pull/6563)). Fixes vector-im/element-web#18410. + * Properly set style attribute on shared usercontent iframe ([\#6561](https://github.com/matrix-org/matrix-react-sdk/pull/6561)). Fixes vector-im/element-web#18414. + * Null guard space inviter to prevent the app exploding ([\#6558](https://github.com/matrix-org/matrix-react-sdk/pull/6558)). + * Make the ringing sound mutable/disablable ([\#6534](https://github.com/matrix-org/matrix-react-sdk/pull/6534)). Fixes vector-im/element-web#15591. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix wrong cursor being used in PiP ([\#6551](https://github.com/matrix-org/matrix-react-sdk/pull/6551)). Fixes vector-im/element-web#18383. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Re-pin Jitsi if the widget already exists ([\#6226](https://github.com/matrix-org/matrix-react-sdk/pull/6226)). Fixes vector-im/element-web#17679. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix broken call notification regression ([\#6526](https://github.com/matrix-org/matrix-react-sdk/pull/6526)). Fixes vector-im/element-web#18335. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * createRoom, only send join rule event if we have a join rule to put in it ([\#6516](https://github.com/matrix-org/matrix-react-sdk/pull/6516)). Fixes vector-im/element-web#18301. + * Fix clicking pills inside replies ([\#6508](https://github.com/matrix-org/matrix-react-sdk/pull/6508)). Fixes vector-im/element-web#18283. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix grecaptcha regression ([\#6503](https://github.com/matrix-org/matrix-react-sdk/pull/6503)). Fixes vector-im/element-web#18284. Contributed by [Palid](https://github.com/Palid). + +Changes in [3.27.0](https://github.com/vector-im/element-desktop/releases/tag/v3.27.0) (2021-08-02) +=================================================================================================== + +## 🔒 SECURITY FIXES + * Sanitize untrusted variables from message previews before translation + Fixes vector-im/element-web#18314 + +## ✨ Features + * Fix editing of `` & ` & `` + [\#6469](https://github.com/matrix-org/matrix-react-sdk/pull/6469) + Fixes vector-im/element-web#18211 + * Zoom images in lightbox to where the cursor points + [\#6418](https://github.com/matrix-org/matrix-react-sdk/pull/6418) + Fixes vector-im/element-web#17870 + * Avoid hitting the settings store from TextForEvent + [\#6205](https://github.com/matrix-org/matrix-react-sdk/pull/6205) + Fixes vector-im/element-web#17650 + * Initial MSC3083 + MSC3244 support + [\#6212](https://github.com/matrix-org/matrix-react-sdk/pull/6212) + Fixes vector-im/element-web#17686 and vector-im/element-web#17661 + * Navigate to the first room with notifications when clicked on space notification dot + [\#5974](https://github.com/matrix-org/matrix-react-sdk/pull/5974) + * Add matrix: to the list of permitted URL schemes + [\#6388](https://github.com/matrix-org/matrix-react-sdk/pull/6388) + * Add "Copy Link" to room context menu + [\#6374](https://github.com/matrix-org/matrix-react-sdk/pull/6374) + * 💭 Message bubble layout + [\#6291](https://github.com/matrix-org/matrix-react-sdk/pull/6291) + Fixes vector-im/element-web#4635, vector-im/element-web#17773 vector-im/element-web#16220 and vector-im/element-web#7687 + * Play only one audio file at a time + [\#6417](https://github.com/matrix-org/matrix-react-sdk/pull/6417) + Fixes vector-im/element-web#17439 + * Move download button for media to the action bar + [\#6386](https://github.com/matrix-org/matrix-react-sdk/pull/6386) + Fixes vector-im/element-web#17943 + * Improved display of one-to-one call history with summary boxes for each call + [\#6121](https://github.com/matrix-org/matrix-react-sdk/pull/6121) + Fixes vector-im/element-web#16409 + * Notification settings UI refresh + [\#6352](https://github.com/matrix-org/matrix-react-sdk/pull/6352) + Fixes vector-im/element-web#17782 + * Fix EventIndex double handling events and erroring + [\#6385](https://github.com/matrix-org/matrix-react-sdk/pull/6385) + Fixes vector-im/element-web#18008 + * Improve reply rendering + [\#3553](https://github.com/matrix-org/matrix-react-sdk/pull/3553) + Fixes vector-im/riot-web#9217, vector-im/riot-web#7633, vector-im/riot-web#7530, vector-im/riot-web#7169, vector-im/riot-web#7151, vector-im/riot-web#6692 vector-im/riot-web#6579 and vector-im/element-web#17440 + +## 🐛 Bug Fixes + * Fix CreateRoomDialog exploding when making public room outside of a space + [\#6493](https://github.com/matrix-org/matrix-react-sdk/pull/6493) + * Fix regression where registration would soft-crash on captcha + [\#6505](https://github.com/matrix-org/matrix-react-sdk/pull/6505) + Fixes vector-im/element-web#18284 + * only send join rule event if we have a join rule to put in it + [\#6517](https://github.com/matrix-org/matrix-react-sdk/pull/6517) + * Improve the new download button's discoverability and interactions. + [\#6510](https://github.com/matrix-org/matrix-react-sdk/pull/6510) + * Fix voice recording UI looking broken while microphone permissions are being requested. + [\#6479](https://github.com/matrix-org/matrix-react-sdk/pull/6479) + Fixes vector-im/element-web#18223 + * Match colors of room and user avatars in DMs + [\#6393](https://github.com/matrix-org/matrix-react-sdk/pull/6393) + Fixes vector-im/element-web#2449 + * Fix onPaste handler to work with copying files from Finder + [\#5389](https://github.com/matrix-org/matrix-react-sdk/pull/5389) + Fixes vector-im/element-web#15536 and vector-im/element-web#16255 + * Fix infinite pagination loop when offline + [\#6478](https://github.com/matrix-org/matrix-react-sdk/pull/6478) + Fixes vector-im/element-web#18242 + * Fix blurhash rounded corners missing regression + [\#6467](https://github.com/matrix-org/matrix-react-sdk/pull/6467) + Fixes vector-im/element-web#18110 + * Fix position of the space hierarchy spinner + [\#6462](https://github.com/matrix-org/matrix-react-sdk/pull/6462) + Fixes vector-im/element-web#18182 + * Fix display of image messages that lack thumbnails + [\#6456](https://github.com/matrix-org/matrix-react-sdk/pull/6456) + Fixes vector-im/element-web#18175 + * Fix crash with large audio files. + [\#6436](https://github.com/matrix-org/matrix-react-sdk/pull/6436) + Fixes vector-im/element-web#18149 + * Make diff colors in codeblocks more pleasant + [\#6355](https://github.com/matrix-org/matrix-react-sdk/pull/6355) + Fixes vector-im/element-web#17939 + * Show the correct audio file duration while loading the file. + [\#6435](https://github.com/matrix-org/matrix-react-sdk/pull/6435) + Fixes vector-im/element-web#18160 + * Fix various timeline settings not applying immediately. + [\#6261](https://github.com/matrix-org/matrix-react-sdk/pull/6261) + Fixes vector-im/element-web#17748 + * Fix issues with room list duplication + [\#6391](https://github.com/matrix-org/matrix-react-sdk/pull/6391) + Fixes vector-im/element-web#14508 + * Fix grecaptcha throwing useless error sometimes + [\#6401](https://github.com/matrix-org/matrix-react-sdk/pull/6401) + Fixes vector-im/element-web#15142 + * Update Emojibase and Twemoji and switch to IamCal (Slack-style) shortcodes + [\#6347](https://github.com/matrix-org/matrix-react-sdk/pull/6347) + Fixes vector-im/element-web#13857 and vector-im/element-web#13334 + * Respect compound emojis in default avatar initial generation + [\#6397](https://github.com/matrix-org/matrix-react-sdk/pull/6397) + Fixes vector-im/element-web#18040 + * Fix bug where the 'other homeserver' field in the server selection dialog would become briefly focus and then unfocus when clicked. + [\#6394](https://github.com/matrix-org/matrix-react-sdk/pull/6394) + Fixes vector-im/element-web#18031 + * Standardise spelling and casing of homeserver, identity server, and integration manager + [\#6365](https://github.com/matrix-org/matrix-react-sdk/pull/6365) + * Fix widgets not receiving decrypted events when they have permission. + [\#6371](https://github.com/matrix-org/matrix-react-sdk/pull/6371) + Fixes vector-im/element-web#17615 + * Prevent client hangs when calculating blurhashes + [\#6366](https://github.com/matrix-org/matrix-react-sdk/pull/6366) + Fixes vector-im/element-web#17945 + * Exclude state events from widgets reading room events + [\#6378](https://github.com/matrix-org/matrix-react-sdk/pull/6378) + * Cache feature_spaces\* flags to improve performance + [\#6381](https://github.com/matrix-org/matrix-react-sdk/pull/6381) + Changes in [3.26.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.26.0) (2021-07-19) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.26.0-rc.1...v3.26.0) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f7c8c8b1c5..f0ca3eb8a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ Contributing code to The React SDK ================================== -matrix-react-sdk follows the same pattern as https://github.com/matrix-org/matrix-js-sdk/blob/master/CONTRIBUTING.rst +matrix-react-sdk follows the same pattern as https://github.com/matrix-org/matrix-js-sdk/blob/master/CONTRIBUTING.md diff --git a/README.md b/README.md index b3e96ef001..67e5e12f59 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ All code lands on the `develop` branch - `master` is only used for stable releas **Please file PRs against `develop`!!** Please follow the standard Matrix contributor's guide: -https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst +https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md Please follow the Matrix JS/React code style as per: https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md diff --git a/package.json b/package.json index b73462d188..9798503e9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.26.0", + "version": "3.29.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -55,6 +55,8 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", + "@sentry/browser": "^6.11.0", + "@sentry/tracing": "^6.11.0", "await-lock": "^2.1.0", "blurhash": "^1.1.3", "browser-encrypt-attachment": "^0.3.0", @@ -80,13 +82,14 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "12.1.0", - "matrix-widget-api": "^0.1.0-beta.15", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-widget-api": "^0.1.0-beta.16", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", "parse5": "^6.0.1", "png-chunks-extract": "^1.0.0", + "posthog-js": "1.12.2", "prop-types": "^15.7.2", "qrcode": "^1.4.4", "re-resizable": "^6.9.0", @@ -123,6 +126,7 @@ "@babel/traverse": "^7.12.12", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "@peculiar/webcrypto": "^1.1.4", + "@sentry/types": "^6.10.0", "@sinonjs/fake-timers": "^7.0.2", "@types/classnames": "^2.2.11", "@types/commonmark": "^0.27.4", @@ -147,13 +151,14 @@ "@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/parser": "^4.17.0", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", + "allchange": "^1.0.3", "babel-jest": "^26.6.3", "chokidar": "^3.5.1", "concurrently": "^5.3.0", "enzyme": "^3.11.0", "eslint": "7.18.0", "eslint-config-google": "^0.14.0", - "eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main", + "eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945", "eslint-plugin-react": "^7.22.0", "eslint-plugin-react-hooks": "^4.2.0", "glob": "^7.1.6", @@ -166,6 +171,7 @@ "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", "react-test-renderer": "^17.0.2", "rimraf": "^3.0.2", + "rrweb-snapshot": "1.1.7", "stylelint": "^13.9.0", "stylelint-config-standard": "^20.0.0", "stylelint-scss": "^3.18.0", @@ -189,7 +195,8 @@ "decoderWorker\\.min\\.js": "/__mocks__/empty.js", "decoderWorker\\.min\\.wasm": "/__mocks__/empty.js", "waveWorker\\.min\\.js": "/__mocks__/empty.js", - "workers/(.+)\\.worker\\.ts": "/__mocks__/workerMock.js" + "workers/(.+)\\.worker\\.ts": "/__mocks__/workerMock.js", + "RecorderWorklet": "/__mocks__/empty.js" }, "transformIgnorePatterns": [ "/node_modules/(?!matrix-js-sdk).+$" diff --git a/release_config.yaml b/release_config.yaml new file mode 100644 index 0000000000..12e857cbdd --- /dev/null +++ b/release_config.yaml @@ -0,0 +1,4 @@ +subprojects: + matrix-js-sdk: + includeByDefault: false + diff --git a/res/css/_animations.scss b/res/css/_animations.scss new file mode 100644 index 0000000000..4d3ad97141 --- /dev/null +++ b/res/css/_animations.scss @@ -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. +*/ + +/** + * React Transition Group animations are prefixed with 'mx_rtg--' so that we + * know they should not be used anywhere outside of React Transition Groups. +*/ + +.mx_rtg--fade-enter { + opacity: 0; +} +.mx_rtg--fade-enter-active { + opacity: 1; + transition: opacity 300ms ease; +} +.mx_rtg--fade-exit { + opacity: 1; +} +.mx_rtg--fade-exit-active { + opacity: 0; + transition: opacity 300ms ease; +} + + +@keyframes mx--anim-pulse { + 0% { opacity: 1; } + 50% { opacity: 0.7; } + 100% { opacity: 1; } +} + + +@media (prefers-reduced-motion) { + @keyframes mx--anim-pulse { + // Override all keyframes in reduced-motion + } + .mx_rtg--fade-enter-active { + transition: none; + } + .mx_rtg--fade-exit-active { + transition: none; + } +} diff --git a/res/css/_common.scss b/res/css/_common.scss index c85a2a74e5..07f6c316b6 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -18,6 +18,7 @@ limitations under the License. @import "./_font-sizes.scss"; @import "./_font-weights.scss"; +@import "./_animations.scss"; $hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic @@ -52,8 +53,8 @@ html { body { font-family: $font-family; font-size: $font-15px; - background-color: $primary-bg-color; - color: $primary-fg-color; + background-color: $background; + color: $primary-content; border: 0px; margin: 0px; @@ -88,7 +89,7 @@ b { } h2 { - color: $primary-fg-color; + color: $primary-content; font-weight: 400; font-size: $font-18px; margin-top: 16px; @@ -104,8 +105,8 @@ a:visited { input[type=text], input[type=search], input[type=password] { + font-family: inherit; padding: 9px; - font-family: $font-family; font-size: $font-14px; font-weight: 600; min-width: 0; @@ -141,13 +142,12 @@ textarea::placeholder { input[type=text], input[type=password], textarea { background-color: transparent; - color: $primary-fg-color; + color: $primary-content; } /* Required by Firefox */ textarea { - font-family: $font-family; - color: $primary-fg-color; + color: $primary-content; } input[type=text]:focus, input[type=password]:focus, textarea:focus { @@ -168,12 +168,12 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { // it has the appearance of a text box so the controls // appear to be part of the input -.mx_Dialog, .mx_MatrixChat { +.mx_Dialog, .mx_MatrixChat_wrapper { .mx_textinput > input[type=text], .mx_textinput > input[type=search] { border: none; flex: 1; - color: $primary-fg-color; + color: $primary-content; } :not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=text], @@ -184,7 +184,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { background-color: transparent; color: $input-darker-fg-color; border-radius: 4px; - border: 1px solid rgba($primary-fg-color, .1); + border: 1px solid rgba($primary-content, .1); // these things should probably not be defined globally margin: 9px; } @@ -209,7 +209,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { :not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=search], .mx_textinput { color: $input-darker-fg-color; - background-color: $primary-bg-color; + background-color: $background; border: none; } } @@ -271,7 +271,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } .mx_Dialog { - background-color: $primary-bg-color; + background-color: $background; color: $light-fg-color; z-index: 4012; font-weight: 300; @@ -390,7 +390,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Dialog_content { margin: 24px 0 68px; font-size: $font-14px; - color: $primary-fg-color; + color: $primary-content; word-wrap: break-word; } @@ -499,8 +499,8 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { border-radius: 3px; border: 1px solid $input-border-color; padding: 9px; - color: $primary-fg-color; - background-color: $primary-bg-color; + color: $primary-content; + background-color: $background; } .mx_textButton { diff --git a/res/css/_components.scss b/res/css/_components.scss index 396a5560a6..ffaec43b68 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -17,6 +17,7 @@ @import "./structures/_LeftPanelWidget.scss"; @import "./structures/_MainSplit.scss"; @import "./structures/_MatrixChat.scss"; +@import "./structures/_BackdropPanel.scss"; @import "./structures/_MyGroups.scss"; @import "./structures/_NonUrgentToastContainer.scss"; @import "./structures/_NotificationPanel.scss"; @@ -27,8 +28,8 @@ @import "./structures/_RoomView.scss"; @import "./structures/_ScrollPanel.scss"; @import "./structures/_SearchBox.scss"; +@import "./structures/_SpaceHierarchy.scss"; @import "./structures/_SpacePanel.scss"; -@import "./structures/_SpaceRoomDirectory.scss"; @import "./structures/_SpaceRoomView.scss"; @import "./structures/_TabbedView.scss"; @import "./structures/_ToastContainer.scss"; @@ -67,7 +68,6 @@ @import "./views/dialogs/_AddExistingToSpaceDialog.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; -@import "./views/dialogs/_BetaFeedbackDialog.scss"; @import "./views/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; @@ -76,16 +76,22 @@ @import "./views/dialogs/_CreateCommunityPrototypeDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; +@import "./views/dialogs/_CreateSpaceFromCommunityDialog.scss"; +@import "./views/dialogs/_CreateSubspaceDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_FeedbackDialog.scss"; @import "./views/dialogs/_ForwardDialog.scss"; +@import "./views/dialogs/_GenericFeatureFeedbackDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_HostSignupDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_InviteDialog.scss"; +@import "./views/dialogs/_JoinRuleDropdown.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; +@import "./views/dialogs/_LeaveSpaceDialog.scss"; +@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_ModalWidgetDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; @@ -126,6 +132,7 @@ @import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_EventListSummary.scss"; +@import "./views/elements/_EventTilePreview.scss"; @import "./views/elements/_FacePile.scss"; @import "./views/elements/_Field.scss"; @import "./views/elements/_ImageView.scss"; @@ -236,6 +243,7 @@ @import "./views/settings/_E2eAdvancedPanel.scss"; @import "./views/settings/_EmailAddresses.scss"; @import "./views/settings/_IntegrationManager.scss"; +@import "./views/settings/_LayoutSwitcher.scss"; @import "./views/settings/_Notifications.scss"; @import "./views/settings/_PhoneNumbers.scss"; @import "./views/settings/_ProfileSettings.scss"; @@ -262,12 +270,16 @@ @import "./views/spaces/_SpacePublicShare.scss"; @import "./views/terms/_InlineTermsAgreement.scss"; @import "./views/toasts/_AnalyticsToast.scss"; +@import "./views/toasts/_IncomingCallToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; +@import "./views/voip/CallView/_CallViewButtons.scss"; @import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallPreview.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_CallViewForRoom.scss"; +@import "./views/voip/_CallViewHeader.scss"; +@import "./views/voip/_CallViewSidebar.scss"; @import "./views/voip/_DialPad.scss"; @import "./views/voip/_DialPadContextMenu.scss"; @import "./views/voip/_DialPadModal.scss"; diff --git a/res/css/structures/_BackdropPanel.scss b/res/css/structures/_BackdropPanel.scss new file mode 100644 index 0000000000..482507cb15 --- /dev/null +++ b/res/css/structures/_BackdropPanel.scss @@ -0,0 +1,37 @@ +/* +Copyright 2021 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_BackdropPanel { + position: absolute; + left: 0; + top: 0; + height: 100vh; + width: 100%; + overflow: hidden; + filter: blur(var(--lp-background-blur)); + // Force a new layer for the backdropPanel so it's better hardware supported + transform: translateZ(0); +} + +.mx_BackdropPanel--image { + position: absolute; + top: 0; + left: 0; + min-height: 100%; + z-index: 0; + pointer-events: none; + overflow: hidden; +} diff --git a/res/css/structures/_ContextualMenu.scss b/res/css/structures/_ContextualMenu.scss index d7f2cb76e8..9f2b9e24b8 100644 --- a/res/css/structures/_ContextualMenu.scss +++ b/res/css/structures/_ContextualMenu.scss @@ -34,7 +34,7 @@ limitations under the License. border-radius: 8px; box-shadow: 4px 4px 12px 0 $menu-box-shadow-color; background-color: $menu-bg-color; - color: $primary-fg-color; + color: $primary-content; position: absolute; font-size: $font-14px; z-index: 5001; diff --git a/res/css/structures/_CreateRoom.scss b/res/css/structures/_CreateRoom.scss index e859beb20e..3d23ccc4b2 100644 --- a/res/css/structures/_CreateRoom.scss +++ b/res/css/structures/_CreateRoom.scss @@ -18,7 +18,7 @@ limitations under the License. width: 960px; margin-left: auto; margin-right: auto; - color: $primary-fg-color; + color: $primary-content; } .mx_CreateRoom input, diff --git a/res/css/structures/_GroupFilterPanel.scss b/res/css/structures/_GroupFilterPanel.scss index 444435dd57..cc0e760031 100644 --- a/res/css/structures/_GroupFilterPanel.scss +++ b/res/css/structures/_GroupFilterPanel.scss @@ -14,10 +14,27 @@ See the License for the specific language governing permissions and limitations under the License. */ +$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations + +.mx_GroupFilterPanelContainer { + flex-grow: 0; + flex-shrink: 0; + width: $groupFilterPanelWidth; + height: 100%; + + // Create another flexbox so the GroupFilterPanel fills the container + display: flex; + flex-direction: column; + + // GroupFilterPanel handles its own CSS +} + .mx_GroupFilterPanel { - flex: 1; + z-index: 1; background-color: $groupFilterPanel-bg-color; + flex: 1; cursor: pointer; + position: relative; display: flex; flex-direction: column; @@ -75,13 +92,13 @@ limitations under the License. } .mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected_prototype { - background-color: $primary-bg-color; + background-color: $background; border-radius: 6px; } .mx_TagTile_selected_prototype { .mx_TagTile_homeIcon::before { - background-color: $primary-fg-color; // dark-on-light + background-color: $primary-content; // dark-on-light } } diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss index 60f9ebdd08..5e224b1f38 100644 --- a/res/css/structures/_GroupView.scss +++ b/res/css/structures/_GroupView.scss @@ -132,7 +132,7 @@ limitations under the License. width: 100%; height: 31px; overflow: hidden; - color: $primary-fg-color; + color: $primary-content; font-weight: bold; font-size: $font-22px; padding-left: 19px; @@ -368,6 +368,65 @@ limitations under the License. padding: 40px 20px; } +.mx_GroupView_spaceUpgradePrompt { + padding: 16px 50px; + background-color: $header-panel-bg-color; + border-radius: 8px; + max-width: 632px; + font-size: $font-15px; + line-height: $font-24px; + margin-top: 24px; + position: relative; + + > h2 { + font-size: inherit; + font-weight: $font-semi-bold; + } + + > p, h2 { + margin: 0; + } + + &::before { + content: ""; + position: absolute; + height: $font-24px; + width: 20px; + left: 18px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + background-color: $secondary-content; + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + + .mx_GroupView_spaceUpgradePrompt_close { + width: 16px; + height: 16px; + border-radius: 8px; + background-color: $input-darker-bg-color; + position: absolute; + top: 16px; + right: 16px; + + &::before { + content: ""; + position: absolute; + width: inherit; + height: inherit; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 8px; + mask-image: url('$(res)/img/image-view/close.svg'); + background-color: $secondary-content; + } + } +} + .mx_GroupView .mx_MemberInfo .mx_AutoHideScrollbar > :not(.mx_MemberInfo_avatar) { padding-left: 16px; padding-right: 16px; diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index f254ca3226..5ddea244f3 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -14,31 +14,47 @@ See the License for the specific language governing permissions and limitations under the License. */ -$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations $roomListCollapsedWidth: 68px; +.mx_MatrixChat--with-avatar { + .mx_LeftPanel, + .mx_LeftPanel .mx_LeftPanel_roomListContainer { + background-color: transparent; + } +} + +.mx_LeftPanel_wrapper { + display: flex; + max-width: 50%; + position: relative; + + // Contain the amount of layers rendered by constraining what actually needs re-layering via css + contain: layout paint; + + .mx_LeftPanel_wrapper--user { + background-color: $roomlist-bg-color; + display: flex; + overflow: hidden; + position: relative; + + &[data-collapsed] { + max-width: $roomListCollapsedWidth; + } + } +} + + + .mx_LeftPanel { background-color: $roomlist-bg-color; // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel - min-width: 206px; - max-width: 50%; // Create a row-based flexbox for the GroupFilterPanel and the room list display: flex; contain: content; - - .mx_LeftPanel_GroupFilterPanelContainer { - flex-grow: 0; - flex-shrink: 0; - flex-basis: $groupFilterPanelWidth; - height: 100%; - - // Create another flexbox so the GroupFilterPanel fills the container - display: flex; - flex-direction: column; - - // GroupFilterPanel handles its own CSS - } + position: relative; + flex-grow: 1; + overflow: hidden; // Note: The 'room list' in this context is actually everything that isn't the tag // panel, such as the menu options, breadcrumbs, filtering, etc @@ -130,7 +146,7 @@ $roomListCollapsedWidth: 68px; mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $secondary-fg-color; + background: $secondary-content; } } @@ -153,7 +169,7 @@ $roomListCollapsedWidth: 68px; mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $secondary-fg-color; + background: $secondary-content; } &.mx_LeftPanel_exploreButton_space::before { @@ -171,6 +187,8 @@ $roomListCollapsedWidth: 68px; } .mx_LeftPanel_roomListWrapper { + // Make the y-scrollbar more responsive + padding-right: 2px; overflow: hidden; margin-top: 10px; // so we're not up against the search/filter flex: 1 0 0; // needed in Safari to properly set flex-basis @@ -192,6 +210,7 @@ $roomListCollapsedWidth: 68px; // These styles override the defaults for the minimized (66px) layout &.mx_LeftPanel_minimized { + flex-grow: 0; min-width: unset; width: unset !important; diff --git a/res/css/structures/_LeftPanelWidget.scss b/res/css/structures/_LeftPanelWidget.scss index 6e2d99bb37..93c2898395 100644 --- a/res/css/structures/_LeftPanelWidget.scss +++ b/res/css/structures/_LeftPanelWidget.scss @@ -113,7 +113,7 @@ limitations under the License. &:hover .mx_LeftPanelWidget_resizerHandle { opacity: 0.8; - background-color: $primary-fg-color; + background-color: $primary-content; } .mx_LeftPanelWidget_maximizeButton { diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss index 8199121420..407a1c270c 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -38,7 +38,7 @@ limitations under the License. width: 4px !important; border-radius: 4px !important; - background-color: $primary-fg-color; + background-color: $primary-content; opacity: 0.8; } } diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index a220c5d505..fdf5cb1a03 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -29,8 +29,6 @@ limitations under the License. .mx_MatrixChat_wrapper { display: flex; - flex-direction: column; - width: 100%; height: 100%; } @@ -42,13 +40,12 @@ limitations under the License. } .mx_MatrixChat { + position: relative; width: 100%; height: 100%; display: flex; - order: 2; - flex: 1; min-height: 0; } @@ -66,8 +63,8 @@ limitations under the License. } /* not the left panel, and not the resize handle, so the roomview/groupview/... */ -.mx_MatrixChat > :not(.mx_LeftPanel):not(.mx_SpacePanel):not(.mx_ResizeHandle) { - background-color: $primary-bg-color; +.mx_MatrixChat > :not(.mx_LeftPanel):not(.mx_SpacePanel):not(.mx_ResizeHandle):not(.mx_LeftPanel_wrapper) { + background-color: $background; flex: 1 1 0; min-width: 0; @@ -94,7 +91,7 @@ limitations under the License. content: ' '; - background-color: $primary-fg-color; + background-color: $primary-content; opacity: 0.8; } } diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss index d271cd2bcc..68e1dd6a9a 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -49,7 +49,7 @@ limitations under the License. bottom: 0; left: 0; right: 0; - background-color: $tertiary-fg-color; + background-color: $tertiary-content; height: 1px; opacity: 0.4; content: ''; @@ -70,7 +70,7 @@ limitations under the License. } .mx_NotificationPanel .mx_EventTile_roomName a { - color: $primary-fg-color; + color: $primary-content; } .mx_NotificationPanel .mx_EventTile_avatar { @@ -79,7 +79,7 @@ limitations under the License. .mx_NotificationPanel .mx_EventTile .mx_SenderProfile, .mx_NotificationPanel .mx_EventTile .mx_MessageTimestamp { - color: $primary-fg-color; + color: $primary-content; font-size: $font-12px; display: inline; } @@ -118,7 +118,7 @@ limitations under the License. } .mx_NotificationPanel .mx_EventTile:hover .mx_EventTile_line { - background-color: $primary-bg-color; + background-color: $background; } .mx_NotificationPanel .mx_EventTile_content { diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 3222fe936c..5316cba61d 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -43,7 +43,7 @@ limitations under the License. .mx_RightPanel_headerButtonGroup { height: 100%; display: flex; - background-color: $primary-bg-color; + background-color: $background; padding: 0 9px; align-items: center; } diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index ec07500af5..fb0f7d10e1 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -28,7 +28,7 @@ limitations under the License. .mx_RoomDirectory { margin-bottom: 12px; - color: $primary-fg-color; + color: $primary-content; word-break: break-word; display: flex; flex-direction: column; @@ -71,14 +71,14 @@ limitations under the License. font-weight: $font-semi-bold; font-size: $font-15px; line-height: $font-18px; - color: $primary-fg-color; + color: $primary-content; } > p { margin: 40px auto 60px; font-size: $font-14px; line-height: $font-20px; - color: $secondary-fg-color; + color: $secondary-content; max-width: 464px; // easier reading } @@ -97,7 +97,7 @@ limitations under the License. } .mx_RoomDirectory_table { - color: $primary-fg-color; + color: $primary-content; display: grid; font-size: $font-12px; grid-template-columns: max-content auto max-content max-content max-content; diff --git a/res/css/structures/_RoomSearch.scss b/res/css/structures/_RoomSearch.scss index 7fdafab5a6..bbd60a5ff3 100644 --- a/res/css/structures/_RoomSearch.scss +++ b/res/css/structures/_RoomSearch.scss @@ -33,14 +33,14 @@ limitations under the License. height: 16px; mask: url('$(res)/img/element-icons/roomlist/search.svg'); mask-repeat: no-repeat; - background-color: $secondary-fg-color; + background-color: $secondary-content; margin-left: 7px; } .mx_RoomSearch_input { border: none !important; // !important to override default app-wide styles flex: 1 !important; // !important to override default app-wide styles - color: $primary-fg-color !important; // !important to override default app-wide styles + color: $primary-content !important; // !important to override default app-wide styles padding: 0; height: 100%; width: 100%; @@ -48,12 +48,12 @@ limitations under the License. line-height: $font-16px; &:not(.mx_RoomSearch_inputExpanded)::placeholder { - color: $tertiary-fg-color !important; // !important to override default app-wide styles + color: $tertiary-content !important; // !important to override default app-wide styles } } &.mx_RoomSearch_hasQuery { - border-color: $secondary-fg-color; + border-color: $secondary-content; } &.mx_RoomSearch_focused { @@ -62,7 +62,7 @@ limitations under the License. } &.mx_RoomSearch_focused, &.mx_RoomSearch_hasQuery { - background-color: $roomlist-filter-active-bg-color; + background-color: $background; .mx_RoomSearch_clearButton { width: 16px; @@ -71,7 +71,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background-color: $secondary-fg-color; + background-color: $secondary-content; margin-right: 8px; } } diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index de9e049165..bdfbca1afa 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -27,7 +27,7 @@ limitations under the License. .mx_RoomStatusBar_typingIndicatorAvatars .mx_BaseAvatar_image { margin-right: -12px; - border: 1px solid $primary-bg-color; + border: 1px solid $background; } .mx_RoomStatusBar_typingIndicatorAvatars .mx_BaseAvatar_initial { @@ -39,7 +39,7 @@ limitations under the License. display: inline-block; color: #acacac; background-color: #ddd; - border: 1px solid $primary-bg-color; + border: 1px solid $background; border-radius: 40px; width: 24px; height: 24px; @@ -171,14 +171,14 @@ limitations under the License. } .mx_RoomStatusBar_connectionLostBar_desc { - color: $primary-fg-color; + color: $primary-content; font-size: $font-13px; opacity: 0.5; padding-bottom: 20px; } .mx_RoomStatusBar_resend_link { - color: $primary-fg-color !important; + color: $primary-content !important; text-decoration: underline !important; cursor: pointer; } @@ -187,7 +187,7 @@ limitations under the License. height: 50px; line-height: $font-50px; - color: $primary-fg-color; + color: $primary-content; opacity: 0.5; overflow-y: hidden; display: block; diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 831f186ed4..86c2efeb4a 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -14,10 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_RoomView_wrapper { + display: flex; + flex-direction: column; + flex: 1; + position: relative; + justify-content: center; + // Contain the amount of layers rendered by constraining what actually needs re-layering via css + contain: strict; +} + .mx_RoomView { word-wrap: break-word; display: flex; flex-direction: column; + flex: 1; + position: relative; } @@ -40,7 +52,7 @@ limitations under the License. pointer-events: none; - background-color: $primary-bg-color; + background-color: $background; opacity: 0.95; position: absolute; @@ -87,7 +99,7 @@ limitations under the License. left: 0; right: 0; z-index: 3000; - background-color: $primary-bg-color; + background-color: $background; } .mx_RoomView_auxPanel_hiddenHighlights { @@ -153,7 +165,6 @@ limitations under the License. flex: 1; display: flex; flex-direction: column; - contain: content; } .mx_RoomView_statusArea { @@ -161,7 +172,7 @@ limitations under the License. flex: 0 0 auto; max-height: 0px; - background-color: $primary-bg-color; + background-color: $background; z-index: 1000; overflow: hidden; @@ -246,7 +257,7 @@ hr.mx_RoomView_myReadMarker { } .mx_RoomView_callStatusBar .mx_UploadBar_uploadProgressInner { - background-color: $primary-bg-color; + background-color: $background; } .mx_RoomView_callStatusBar .mx_UploadBar_uploadFilename { diff --git a/res/css/structures/_ScrollPanel.scss b/res/css/structures/_ScrollPanel.scss index 7b75c69e86..a668594bba 100644 --- a/res/css/structures/_ScrollPanel.scss +++ b/res/css/structures/_ScrollPanel.scss @@ -15,7 +15,6 @@ limitations under the License. */ .mx_ScrollPanel { - .mx_RoomView_MessageList { position: relative; display: flex; diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceHierarchy.scss similarity index 80% rename from res/css/structures/_SpaceRoomDirectory.scss rename to res/css/structures/_SpaceHierarchy.scss index 7925686bf1..a5d589f9c2 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceHierarchy.scss @@ -14,21 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SpaceRoomDirectory_dialogWrapper > .mx_Dialog { - max-width: 960px; - height: 100%; -} - -.mx_SpaceRoomDirectory { - height: 100%; - margin-bottom: 12px; - color: $primary-fg-color; - word-break: break-word; - display: flex; - flex-direction: column; -} - -.mx_SpaceRoomDirectory, .mx_SpaceRoomView_landing { .mx_Dialog_title { display: flex; @@ -52,7 +37,7 @@ limitations under the License. > div { font-weight: 400; - color: $secondary-fg-color; + color: $secondary-content; font-size: $font-15px; line-height: $font-24px; } @@ -61,29 +46,36 @@ limitations under the License. .mx_AccessibleButton_kind_link { padding: 0; + font-size: inherit; } .mx_SearchBox { margin: 24px 0 16px; } - .mx_SpaceRoomDirectory_noResults { + .mx_SpaceHierarchy_noResults { text-align: center; > div { font-size: $font-15px; line-height: $font-24px; - color: $secondary-fg-color; + color: $secondary-content; } } - .mx_SpaceRoomDirectory_listHeader { + .mx_SpaceHierarchy_listHeader { display: flex; min-height: 32px; align-items: center; font-size: $font-15px; line-height: $font-24px; - color: $primary-fg-color; + color: $primary-content; + margin-bottom: 12px; + + > h4 { + font-weight: $font-semi-bold; + margin: 0; + } .mx_AccessibleButton { padding: 4px 12px; @@ -104,7 +96,7 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_error { + .mx_SpaceHierarchy_error { position: relative; font-weight: $font-semi-bold; color: $notice-primary-color; @@ -123,43 +115,44 @@ limitations under the License. background-image: url("$(res)/img/element-icons/warning-badge.svg"); } } -} -.mx_SpaceRoomDirectory_list { - margin-top: 16px; - padding-bottom: 40px; + .mx_SpaceHierarchy_list { + list-style: none; + padding: 0; + margin: 0; + } - .mx_SpaceRoomDirectory_roomCount { + .mx_SpaceHierarchy_roomCount { > h3 { display: inline; font-weight: $font-semi-bold; font-size: $font-18px; line-height: $font-22px; - color: $primary-fg-color; + color: $primary-content; } > span { margin-left: 8px; font-size: $font-15px; line-height: $font-24px; - color: $secondary-fg-color; + color: $secondary-content; } } - .mx_SpaceRoomDirectory_subspace { + .mx_SpaceHierarchy_subspace { .mx_BaseAvatar_image { border-radius: 8px; } } - .mx_SpaceRoomDirectory_subspace_toggle { + .mx_SpaceHierarchy_subspace_toggle { position: absolute; left: -1px; top: 10px; height: 16px; width: 16px; border-radius: 4px; - background-color: $primary-bg-color; + background-color: $background; &::before { content: ''; @@ -170,27 +163,26 @@ limitations under the License. width: 16px; mask-repeat: no-repeat; mask-position: center; - background-color: $tertiary-fg-color; + background-color: $tertiary-content; mask-size: 16px; transform: rotate(270deg); mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); } - &.mx_SpaceRoomDirectory_subspace_toggle_shown::before { + &.mx_SpaceHierarchy_subspace_toggle_shown::before { transform: rotate(0deg); } } - .mx_SpaceRoomDirectory_subspace_children { + .mx_SpaceHierarchy_subspace_children { position: relative; padding-left: 12px; } - .mx_SpaceRoomDirectory_roomTile { + .mx_SpaceHierarchy_roomTile { position: relative; padding: 8px 16px; border-radius: 8px; - min-height: 56px; box-sizing: border-box; display: grid; @@ -204,7 +196,7 @@ limitations under the License. grid-column: 1; } - .mx_SpaceRoomDirectory_roomTile_name { + .mx_SpaceHierarchy_roomTile_name { font-weight: $font-semi-bold; font-size: $font-15px; line-height: $font-18px; @@ -214,7 +206,7 @@ limitations under the License. .mx_InfoTooltip { display: inline; margin-left: 12px; - color: $tertiary-fg-color; + color: $tertiary-content; font-size: $font-12px; line-height: $font-15px; @@ -232,10 +224,10 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_roomTile_info { + .mx_SpaceHierarchy_roomTile_info { font-size: $font-14px; line-height: $font-18px; - color: $secondary-fg-color; + color: $secondary-content; grid-row: 2; grid-column: 1/3; display: -webkit-box; @@ -244,7 +236,7 @@ limitations under the License. overflow: hidden; } - .mx_SpaceRoomDirectory_actions { + .mx_SpaceHierarchy_actions { text-align: right; margin-left: 20px; grid-column: 3; @@ -269,7 +261,7 @@ limitations under the License. } } - &:hover { + &:hover, &:focus-within { background-color: $groupFilterPanel-bg-color; .mx_AccessibleButton { @@ -278,8 +270,12 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_roomTile, - .mx_SpaceRoomDirectory_subspace_children { + li.mx_SpaceHierarchy_roomTileWrapper { + list-style: none; + } + + .mx_SpaceHierarchy_roomTile, + .mx_SpaceHierarchy_subspace_children { &::before { content: ""; position: absolute; @@ -291,12 +287,12 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_actions { - .mx_SpaceRoomDirectory_actionsText { + .mx_SpaceHierarchy_actions { + .mx_SpaceHierarchy_actionsText { font-weight: normal; font-size: $font-12px; line-height: $font-15px; - color: $secondary-fg-color; + color: $secondary-content; } } @@ -307,7 +303,7 @@ limitations under the License. margin: 20px 0; } - .mx_SpaceRoomDirectory_createRoom { + .mx_SpaceHierarchy_createRoom { display: block; margin: 16px auto 0; width: max-content; diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index e64057d16c..bbb1867f16 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -20,13 +20,16 @@ $gutterSize: 16px; $activeBorderTransparentGap: 1px; $activeBackgroundColor: $roomtile-selected-bg-color; -$activeBorderColor: $secondary-fg-color; +$activeBorderColor: $secondary-content; .mx_SpacePanel { - flex: 0 0 auto; background-color: $groupFilterPanel-bg-color; + flex: 0 0 auto; padding: 0; margin: 0; + position: relative; + // Fix for the blurred avatar-background + z-index: 1; // Create another flexbox so the Panel fills the container display: flex; @@ -111,6 +114,7 @@ $activeBorderColor: $secondary-fg-color; align-items: center; padding: 4px 4px 4px 0; width: 100%; + cursor: pointer; &.mx_SpaceButton_active { &:not(.mx_SpaceButton_narrow) .mx_SpaceButton_selectionWrapper { @@ -235,7 +239,7 @@ $activeBorderColor: $secondary-fg-color; mask-size: contain; mask-repeat: no-repeat; mask-image: url('$(res)/img/element-icons/context-menu.svg'); - background: $primary-fg-color; + background: $primary-content; } } } @@ -297,7 +301,7 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceButton:hover, .mx_SpaceButton:focus-within, .mx_SpaceButton_hasMenuOpen { - &:not(.mx_SpaceButton_home):not(.mx_SpaceButton_invite) { + &:not(.mx_SpaceButton_invite) { // Hide the badge container on hover because it'll be a menu button .mx_SpacePanel_badgeContainer { width: 0; @@ -368,6 +372,14 @@ $activeBorderColor: $secondary-fg-color; .mx_SpacePanel_iconExplore::before { mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); } + + .mx_SpacePanel_noIcon { + display: none; + + & + .mx_IconizedContextMenu_label { + padding-left: 5px !important; // override default iconized label style to align with header + } + } } diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 48b565be7f..39eabe2e07 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -32,7 +32,7 @@ $SpaceRoomViewInnerWidth: 428px; } > span { - color: $secondary-fg-color; + color: $secondary-content; } &::before { @@ -45,7 +45,7 @@ $SpaceRoomViewInnerWidth: 428px; mask-position: center; mask-repeat: no-repeat; mask-size: 24px; - background-color: $tertiary-fg-color; + background-color: $tertiary-content; } &:hover { @@ -56,12 +56,15 @@ $SpaceRoomViewInnerWidth: 428px; } > span { - color: $primary-fg-color; + color: $primary-content; } } } .mx_SpaceRoomView { + overflow-y: auto; + flex: 1; + .mx_MainSplit > div:first-child { padding: 80px 60px; flex-grow: 1; @@ -72,13 +75,13 @@ $SpaceRoomViewInnerWidth: 428px; margin: 0; font-size: $font-24px; font-weight: $font-semi-bold; - color: $primary-fg-color; + color: $primary-content; width: max-content; } .mx_SpaceRoomView_description { font-size: $font-15px; - color: $secondary-fg-color; + color: $secondary-content; margin-top: 12px; margin-bottom: 24px; max-width: $SpaceRoomViewInnerWidth; @@ -154,7 +157,7 @@ $SpaceRoomViewInnerWidth: 428px; font-weight: $font-semi-bold; font-size: $font-14px; line-height: $font-24px; - color: $primary-fg-color; + color: $primary-content; margin-top: 24px; position: relative; padding-left: 24px; @@ -176,7 +179,19 @@ $SpaceRoomViewInnerWidth: 428px; mask-position: center; mask-size: contain; mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); - background-color: $secondary-fg-color; + background-color: $secondary-content; + } + } + + .mx_SpaceRoomView_preview_migratedCommunity { + margin-bottom: 16px; + padding: 8px 12px; + border-radius: 8px; + border: 1px solid $input-border-color; + width: max-content; + + .mx_BaseAvatar { + margin-right: 4px; } } @@ -195,7 +210,7 @@ $SpaceRoomViewInnerWidth: 428px; .mx_SpaceRoomView_preview_inviter_mxid { line-height: $font-24px; - color: $secondary-fg-color; + color: $secondary-content; } } } @@ -212,7 +227,7 @@ $SpaceRoomViewInnerWidth: 428px; .mx_SpaceRoomView_preview_topic { font-size: $font-14px; line-height: $font-22px; - color: $secondary-fg-color; + color: $secondary-content; margin: 20px 0; max-height: 160px; overflow-y: auto; @@ -234,6 +249,10 @@ $SpaceRoomViewInnerWidth: 428px; } .mx_SpaceRoomView_landing { + display: flex; + flex-direction: column; + min-width: 0; + > .mx_BaseAvatar_image, > .mx_BaseAvatar > .mx_BaseAvatar_image { border-radius: 12px; @@ -242,7 +261,7 @@ $SpaceRoomViewInnerWidth: 428px; .mx_SpaceRoomView_landing_name { margin: 24px 0 16px; font-size: $font-15px; - color: $secondary-fg-color; + color: $secondary-content; > span { display: inline-block; @@ -315,7 +334,7 @@ $SpaceRoomViewInnerWidth: 428px; top: 0; height: 24px; width: 24px; - background: $tertiary-fg-color; + background: $tertiary-content; mask-position: center; mask-size: contain; mask-repeat: no-repeat; @@ -332,23 +351,17 @@ $SpaceRoomViewInnerWidth: 428px; word-wrap: break-word; } - > hr { - border: none; - height: 1px; - background-color: $groupFilterPanel-bg-color; - } - .mx_SearchBox { margin: 0 0 20px; + flex: 0; } .mx_SpaceFeedbackPrompt { - margin-bottom: 16px; - - // hide the HR as we have our own - & + hr { - display: none; - } + padding: 7px; // 8px - 1px border + border: 1px solid rgba($primary-content, .1); + border-radius: 8px; + width: max-content; + margin: 0 0 -40px auto; // collapse its own height to not push other components down } } @@ -374,7 +387,7 @@ $SpaceRoomViewInnerWidth: 428px; width: 432px; border-radius: 8px; background-color: $info-plinth-bg-color; - color: $secondary-fg-color; + color: $secondary-content; box-sizing: border-box; > h3 { @@ -401,7 +414,7 @@ $SpaceRoomViewInnerWidth: 428px; position: absolute; top: 14px; left: 14px; - background-color: $secondary-fg-color; + background-color: $secondary-content; } } @@ -424,7 +437,7 @@ $SpaceRoomViewInnerWidth: 428px; } .mx_SpaceRoomView_inviteTeammates_buttons { - color: $secondary-fg-color; + color: $secondary-content; margin-top: 28px; .mx_AccessibleButton { @@ -440,7 +453,7 @@ $SpaceRoomViewInnerWidth: 428px; width: 24px; top: 0; left: 0; - background-color: $secondary-fg-color; + background-color: $secondary-content; mask-repeat: no-repeat; mask-position: center; mask-size: contain; @@ -459,7 +472,7 @@ $SpaceRoomViewInnerWidth: 428px; } .mx_SpaceRoomView_info { - color: $secondary-fg-color; + color: $secondary-content; font-size: $font-15px; line-height: $font-24px; margin: 20px 0; @@ -478,7 +491,7 @@ $SpaceRoomViewInnerWidth: 428px; left: -2px; mask-position: center; mask-repeat: no-repeat; - background-color: $tertiary-fg-color; + background-color: $tertiary-content; } } @@ -504,66 +517,3 @@ $SpaceRoomViewInnerWidth: 428px; } } } - -.mx_SpaceFeedbackPrompt { - margin-top: 18px; - margin-bottom: 12px; - - > hr { - border: none; - border-top: 1px solid $input-border-color; - margin-bottom: 12px; - } - - > div { - display: flex; - flex-direction: row; - font-size: $font-15px; - line-height: $font-24px; - - > span { - color: $secondary-fg-color; - position: relative; - padding-left: 32px; - font-size: inherit; - line-height: inherit; - margin-right: auto; - - &::before { - content: ''; - position: absolute; - left: 0; - top: 2px; - height: 20px; - width: 20px; - background-color: $secondary-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); - mask-position: center; - } - } - - .mx_AccessibleButton_kind_link { - color: $accent-color; - position: relative; - padding: 0 0 0 24px; - margin-left: 8px; - font-size: inherit; - line-height: inherit; - - &::before { - content: ''; - position: absolute; - left: 0; - height: 16px; - width: 16px; - background-color: $accent-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/chat-bubbles.svg'); - mask-position: center; - } - } - } -} diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss index 833450a25b..e185197f25 100644 --- a/res/css/structures/_TabbedView.scss +++ b/res/css/structures/_TabbedView.scss @@ -80,7 +80,7 @@ limitations under the License. .mx_TabbedView_tabLabel_text { font-size: 15px; - color: $tertiary-fg-color; + color: $tertiary-content; } } diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index d248568740..6024df5dc0 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -28,7 +28,7 @@ limitations under the License. margin: 0 4px; grid-row: 2 / 4; grid-column: 1; - background-color: $dark-panel-bg-color; + background-color: $system; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; } @@ -36,8 +36,8 @@ limitations under the License. .mx_Toast_toast { grid-row: 1 / 3; grid-column: 1; - color: $primary-fg-color; - background-color: $dark-panel-bg-color; + background-color: $system; + color: $primary-content; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; overflow: hidden; @@ -63,7 +63,7 @@ limitations under the License. &.mx_Toast_icon_verification::after { mask-image: url("$(res)/img/e2e/normal.svg"); - background-color: $primary-fg-color; + background-color: $primary-content; } &.mx_Toast_icon_verification_warning { @@ -82,7 +82,7 @@ limitations under the License. &.mx_Toast_icon_secure_backup::after { mask-image: url('$(res)/img/feather-customised/secure-backup.svg'); - background-color: $primary-fg-color; + background-color: $primary-content; } .mx_Toast_title, .mx_Toast_body { @@ -163,7 +163,7 @@ limitations under the License. } .mx_Toast_detail { - color: $secondary-fg-color; + color: $secondary-content; } .mx_Toast_deviceID { diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index 17e6ad75df..c10e7f60df 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -35,7 +35,7 @@ limitations under the License. // we cheat opacity on the theme colour with an after selector here &::after { content: ''; - border-bottom: 1px solid $primary-fg-color; // XXX: Variable abuse + border-bottom: 1px solid $primary-content; opacity: 0.2; display: block; padding-top: 8px; @@ -58,7 +58,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $tertiary-fg-color; + background: $tertiary-content; mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); } } @@ -176,7 +176,7 @@ limitations under the License. width: 85%; opacity: 0.2; border: none; - border-bottom: 1px solid $primary-fg-color; // XXX: Variable abuse + border-bottom: 1px solid $primary-content; } &.mx_IconizedContextMenu { @@ -292,7 +292,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $primary-fg-color; + background: $primary-content; } } diff --git a/res/css/structures/_ViewSource.scss b/res/css/structures/_ViewSource.scss index 248eab5d88..e3d6135ef3 100644 --- a/res/css/structures/_ViewSource.scss +++ b/res/css/structures/_ViewSource.scss @@ -24,7 +24,7 @@ limitations under the License. .mx_ViewSource_heading { font-size: $font-17px; font-weight: 400; - color: $primary-fg-color; + color: $primary-content; margin-top: 0.7em; } diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss index 9c98ca3a1c..c4aaaca1d0 100644 --- a/res/css/structures/auth/_Login.scss +++ b/res/css/structures/auth/_Login.scss @@ -96,3 +96,10 @@ div.mx_AccessibleButton_kind_link.mx_Login_forgot { cursor: not-allowed; } } +.mx_Login_spinner { + display: flex; + justify-content: center; + align-items: center; + align-content: center; + padding: 14px; +} diff --git a/res/css/views/audio_messages/_AudioPlayer.scss b/res/css/views/audio_messages/_AudioPlayer.scss index 77dcebbb9a..3c2551e36a 100644 --- a/res/css/views/audio_messages/_AudioPlayer.scss +++ b/res/css/views/audio_messages/_AudioPlayer.scss @@ -33,7 +33,7 @@ limitations under the License. } .mx_AudioPlayer_mediaName { - color: $primary-fg-color; + color: $primary-content; font-size: $font-15px; line-height: $font-15px; text-overflow: ellipsis; diff --git a/res/css/views/audio_messages/_SeekBar.scss b/res/css/views/audio_messages/_SeekBar.scss index d13fe4ac6a..03449d009b 100644 --- a/res/css/views/audio_messages/_SeekBar.scss +++ b/res/css/views/audio_messages/_SeekBar.scss @@ -27,7 +27,7 @@ limitations under the License. width: 100%; height: 1px; - background: $quaternary-fg-color; + background: $quaternary-content; outline: none; // remove blue selection border position: relative; // for before+after pseudo elements later on @@ -42,7 +42,7 @@ limitations under the License. width: 8px; height: 8px; border-radius: 8px; - background-color: $tertiary-fg-color; + background-color: $tertiary-content; cursor: pointer; } @@ -50,7 +50,7 @@ limitations under the License. width: 8px; height: 8px; border-radius: 8px; - background-color: $tertiary-fg-color; + background-color: $tertiary-content; cursor: pointer; // Firefox adds a border on the thumb @@ -63,7 +63,7 @@ limitations under the License. // 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; + background-color: $tertiary-content; // Absolute positioning to ensure it overlaps with the existing bar position: absolute; @@ -81,7 +81,7 @@ limitations under the License. // This is firefox's built-in support for the above, with 100% less hacks. &::-moz-range-progress { - background-color: $tertiary-fg-color; + background-color: $tertiary-content; height: 1px; } diff --git a/res/css/views/auth/_AuthButtons.scss b/res/css/views/auth/_AuthButtons.scss index 8deb0f80ac..3a2ad2adf8 100644 --- a/res/css/views/auth/_AuthButtons.scss +++ b/res/css/views/auth/_AuthButtons.scss @@ -39,7 +39,7 @@ limitations under the License. min-width: 80px; background-color: $accent-color; - color: $primary-bg-color; + color: $background; cursor: pointer; diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss index ffaad3cd7a..ec07b765fd 100644 --- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss +++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss @@ -85,7 +85,7 @@ limitations under the License. .mx_InteractiveAuthEntryComponents_termsPolicy { display: flex; flex-direction: row; - justify-content: start; + justify-content: flex-start; align-items: center; } diff --git a/res/css/views/avatars/_BaseAvatar.scss b/res/css/views/avatars/_BaseAvatar.scss index 65e4493f19..cbddd97e18 100644 --- a/res/css/views/avatars/_BaseAvatar.scss +++ b/res/css/views/avatars/_BaseAvatar.scss @@ -27,7 +27,6 @@ limitations under the License. // https://bugzilla.mozilla.org/show_bug.cgi?id=255139 display: inline-block; user-select: none; - line-height: 1; } .mx_BaseAvatar_initial { diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss index 257b512579..4922068462 100644 --- a/res/css/views/avatars/_DecoratedRoomAvatar.scss +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -47,7 +47,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $secondary-fg-color; + background: $secondary-content; mask-image: url('$(res)/img/globe.svg'); } diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss index 2af4e79ecd..ff6910852c 100644 --- a/res/css/views/beta/_BetaCard.scss +++ b/res/css/views/beta/_BetaCard.scss @@ -29,7 +29,7 @@ limitations under the License. font-weight: $font-semi-bold; font-size: $font-18px; line-height: $font-22px; - color: $primary-fg-color; + color: $primary-content; margin: 4px 0 14px; .mx_BetaCard_betaPill { @@ -40,7 +40,7 @@ limitations under the License. .mx_BetaCard_caption { font-size: $font-15px; line-height: $font-20px; - color: $secondary-fg-color; + color: $secondary-content; margin-bottom: 20px; } @@ -54,7 +54,7 @@ limitations under the License. .mx_BetaCard_disclaimer { font-size: $font-12px; line-height: $font-15px; - color: $secondary-fg-color; + color: $secondary-content; margin-top: 20px; } } @@ -72,13 +72,13 @@ limitations under the License. margin: 16px 0 0; font-size: $font-15px; line-height: $font-24px; - color: $primary-fg-color; + color: $primary-content; .mx_SettingsFlag_microcopy { margin-top: 4px; font-size: $font-12px; line-height: $font-15px; - color: $secondary-fg-color; + color: $secondary-content; } } } diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss index 204435995f..ca40f18cd4 100644 --- a/res/css/views/context_menus/_IconizedContextMenu.scss +++ b/res/css/views/context_menus/_IconizedContextMenu.scss @@ -36,7 +36,7 @@ limitations under the License. // // Therefore, we just hack in a line and border the thing ourselves &::before { - border-top: 1px solid $primary-fg-color; + border-top: 1px solid $primary-content; opacity: 0.1; content: ''; @@ -63,7 +63,7 @@ limitations under the License. padding-top: 12px; padding-bottom: 12px; text-decoration: none; - color: $primary-fg-color; + color: $primary-content; font-size: $font-15px; line-height: $font-24px; @@ -99,6 +99,10 @@ limitations under the License. .mx_IconizedContextMenu_icon + .mx_IconizedContextMenu_label { padding-left: 14px; } + + .mx_BetaCard_betaPill { + margin-left: 16px; + } } } @@ -115,7 +119,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $primary-fg-color; + background: $primary-content; } } @@ -145,12 +149,17 @@ limitations under the License. } } - .mx_IconizedContextMenu_checked { + .mx_IconizedContextMenu_checked, + .mx_IconizedContextMenu_unchecked { margin-left: 16px; margin-right: -5px; + } - &::before { - mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); - } + .mx_IconizedContextMenu_checked::before { + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); + } + + .mx_IconizedContextMenu_unchecked::before { + content: unset; } } diff --git a/res/css/views/context_menus/_MessageContextMenu.scss b/res/css/views/context_menus/_MessageContextMenu.scss index 338841cce4..5af748e28d 100644 --- a/res/css/views/context_menus/_MessageContextMenu.scss +++ b/res/css/views/context_menus/_MessageContextMenu.scss @@ -30,7 +30,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $primary-fg-color; + background: $primary-content; } } diff --git a/res/css/views/context_menus/_StatusMessageContextMenu.scss b/res/css/views/context_menus/_StatusMessageContextMenu.scss index fceb7fba34..1a97fb56c7 100644 --- a/res/css/views/context_menus/_StatusMessageContextMenu.scss +++ b/res/css/views/context_menus/_StatusMessageContextMenu.scss @@ -27,7 +27,7 @@ input.mx_StatusMessageContextMenu_message { border-radius: 4px; border: 1px solid $input-border-color; padding: 6.5px 11px; - background-color: $primary-bg-color; + background-color: $background; font-weight: normal; margin: 0 0 10px; } diff --git a/res/css/views/context_menus/_TagTileContextMenu.scss b/res/css/views/context_menus/_TagTileContextMenu.scss index d707f4ce7c..14f5ec817e 100644 --- a/res/css/views/context_menus/_TagTileContextMenu.scss +++ b/res/css/views/context_menus/_TagTileContextMenu.scss @@ -51,6 +51,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/hide.svg'); } +.mx_TagTileContextMenu_createSpace::before { + mask-image: url('$(res)/img/element-icons/message/fwd.svg'); +} + .mx_TagTileContextMenu_separator { margin-top: 0; margin-bottom: 0; diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 2776c477fc..444b29c9bf 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -44,70 +44,17 @@ limitations under the License. > h3 { margin: 0; - color: $secondary-fg-color; + color: $secondary-content; font-size: $font-12px; font-weight: $font-semi-bold; line-height: $font-15px; } - .mx_AddExistingToSpace_entry { - display: flex; - margin-top: 12px; - - // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling - .mx_DecoratedRoomAvatar { - margin-right: 12px; - } - - .mx_AddExistingToSpace_entry_name { - font-size: $font-15px; - line-height: 30px; - flex-grow: 1; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin-right: 12px; - } - - .mx_Checkbox { - align-items: center; - } - } - } - - .mx_AddExistingToSpace_section_spaces { - .mx_BaseAvatar { - margin-right: 12px; - } - - .mx_BaseAvatar_image { - border-radius: 8px; - } - } - - .mx_AddExistingToSpace_section_experimental { - position: relative; - border-radius: 8px; - margin: 12px 0; - padding: 8px 8px 8px 42px; - background-color: $header-panel-bg-color; - - font-size: $font-12px; - line-height: $font-15px; - color: $secondary-fg-color; - - &::before { - content: ''; - position: absolute; - left: 10px; - top: calc(50% - 8px); // vertical centering - height: 16px; - width: 16px; - background-color: $secondary-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); - mask-position: center; + .mx_AccessibleButton_kind_link { + font-size: $font-12px; + line-height: $font-15px; + margin-top: 8px; + padding: 0; } } @@ -119,7 +66,7 @@ limitations under the License. flex-grow: 1; font-size: $font-12px; line-height: $font-15px; - color: $secondary-fg-color; + color: $secondary-content; .mx_ProgressBar { height: 8px; @@ -132,7 +79,7 @@ limitations under the License. margin-top: 8px; font-size: $font-15px; line-height: $font-24px; - color: $primary-fg-color; + color: $primary-content; } > * { @@ -158,7 +105,7 @@ limitations under the License. margin-top: 4px; font-size: $font-12px; line-height: $font-15px; - color: $primary-fg-color; + color: $primary-content; } } @@ -179,7 +126,7 @@ limitations under the License. &::before { content: ''; position: absolute; - background-color: $primary-fg-color; + background-color: $primary-content; mask-repeat: no-repeat; mask-position: center; mask-size: contain; @@ -198,84 +145,113 @@ limitations under the License. .mx_AddExistingToSpaceDialog { width: 480px; - color: $primary-fg-color; + color: $primary-content; display: flex; flex-direction: column; flex-wrap: nowrap; min-height: 0; height: 80vh; - .mx_Dialog_title { - display: flex; - - .mx_BaseAvatar_image { - border-radius: 8px; - margin: 0; - vertical-align: unset; - } - - .mx_BaseAvatar { - display: inline-flex; - margin: auto 16px auto 5px; - vertical-align: middle; - } - - > div { - > h1 { - font-weight: $font-semi-bold; - font-size: $font-18px; - line-height: $font-22px; - margin: 0; - } - - .mx_AddExistingToSpaceDialog_onlySpace { - color: $secondary-fg-color; - font-size: $font-15px; - line-height: $font-24px; - } - } - - .mx_Dropdown_input { - border: none; - - > .mx_Dropdown_option { - padding-left: 0; - flex: unset; - height: unset; - color: $secondary-fg-color; - font-size: $font-15px; - line-height: $font-24px; - - .mx_BaseAvatar { - display: none; - } - } - - .mx_Dropdown_menu { - .mx_AddExistingToSpaceDialog_dropdownOptionActive { - color: $accent-color; - padding-right: 32px; - position: relative; - - &::before { - content: ''; - width: 20px; - height: 20px; - top: 8px; - right: 0; - position: absolute; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background-color: $accent-color; - mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); - } - } - } - } - } - .mx_AddExistingToSpace { display: contents; } } + +.mx_SubspaceSelector { + display: flex; + + .mx_BaseAvatar_image { + border-radius: 8px; + margin: 0; + vertical-align: unset; + } + + .mx_BaseAvatar { + display: inline-flex; + margin: auto 16px auto 5px; + vertical-align: middle; + } + + > div { + > h1 { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + margin: 0; + } + } + + .mx_Dropdown_input { + border: none; + + > .mx_Dropdown_option { + padding-left: 0; + flex: unset; + height: unset; + color: $secondary-content; + font-size: $font-15px; + line-height: $font-24px; + + .mx_BaseAvatar { + display: none; + } + } + + .mx_Dropdown_menu { + .mx_SubspaceSelector_dropdownOptionActive { + color: $accent-color; + padding-right: 32px; + position: relative; + + &::before { + content: ''; + width: 20px; + height: 20px; + top: 8px; + right: 0; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background-color: $accent-color; + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); + } + } + } + } + + .mx_SubspaceSelector_onlySpace { + color: $secondary-content; + font-size: $font-15px; + line-height: $font-24px; + } +} + +.mx_AddExistingToSpace_entry { + display: flex; + margin-top: 12px; + + .mx_DecoratedRoomAvatar, // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling + .mx_BaseAvatar.mx_RoomAvatar_isSpaceRoom { + margin-right: 12px; + } + + img.mx_RoomAvatar_isSpaceRoom, + .mx_RoomAvatar_isSpaceRoom img { + border-radius: 8px; + } + + .mx_AddExistingToSpace_entry_name { + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-right: 12px; + } + + .mx_Checkbox { + align-items: center; + } +} diff --git a/res/css/views/dialogs/_AddressPickerDialog.scss b/res/css/views/dialogs/_AddressPickerDialog.scss index 136e497994..a1147e6fbc 100644 --- a/res/css/views/dialogs/_AddressPickerDialog.scss +++ b/res/css/views/dialogs/_AddressPickerDialog.scss @@ -29,7 +29,6 @@ limitations under the License. .mx_AddressPickerDialog_input:focus { height: 26px; font-size: $font-14px; - font-family: $font-family; padding-left: 12px; padding-right: 12px; margin: 0 !important; diff --git a/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss b/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss index beae03f00f..5d6c817b14 100644 --- a/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss +++ b/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss @@ -65,7 +65,7 @@ limitations under the License. .mx_CommunityPrototypeInviteDialog_personName { font-weight: 600; font-size: $font-14px; - color: $primary-fg-color; + color: $primary-content; margin-left: 7px; } diff --git a/res/css/views/dialogs/_ConfirmUserActionDialog.scss b/res/css/views/dialogs/_ConfirmUserActionDialog.scss index 823f4d1e28..5ac0f07b14 100644 --- a/res/css/views/dialogs/_ConfirmUserActionDialog.scss +++ b/res/css/views/dialogs/_ConfirmUserActionDialog.scss @@ -34,10 +34,9 @@ limitations under the License. } .mx_ConfirmUserActionDialog_reasonField { - font-family: $font-family; font-size: $font-14px; - color: $primary-fg-color; - background-color: $primary-bg-color; + color: $primary-content; + background-color: $background; border-radius: 3px; border: solid 1px $input-border-color; diff --git a/res/css/views/dialogs/_CreateGroupDialog.scss b/res/css/views/dialogs/_CreateGroupDialog.scss index f7bfc61a98..ef9c2b73d4 100644 --- a/res/css/views/dialogs/_CreateGroupDialog.scss +++ b/res/css/views/dialogs/_CreateGroupDialog.scss @@ -29,8 +29,8 @@ limitations under the License. border-radius: 3px; border: 1px solid $input-border-color; padding: 9px; - color: $primary-fg-color; - background-color: $primary-bg-color; + color: $primary-content; + background-color: $background; } .mx_CreateGroupDialog_input_hasPrefixAndSuffix { diff --git a/res/css/views/dialogs/_CreateRoomDialog.scss b/res/css/views/dialogs/_CreateRoomDialog.scss index 2678f7b4ad..9cfa8ce25a 100644 --- a/res/css/views/dialogs/_CreateRoomDialog.scss +++ b/res/css/views/dialogs/_CreateRoomDialog.scss @@ -55,8 +55,8 @@ limitations under the License. border-radius: 3px; border: 1px solid $input-border-color; padding: 9px; - color: $primary-fg-color; - background-color: $primary-bg-color; + color: $primary-content; + background-color: $background; width: 100%; } @@ -65,7 +65,7 @@ limitations under the License. .mx_CreateRoomDialog_aliasContainer { display: flex; // put margin on container so it can collapse with siblings - margin: 10px 0; + margin: 24px 0 10px; .mx_RoomAliasField { margin: 0; @@ -101,10 +101,6 @@ limitations under the License. margin-left: 30px; } - .mx_CreateRoomDialog_topic { - margin-bottom: 36px; - } - .mx_Dialog_content > .mx_SettingsFlag { margin-top: 24px; } @@ -114,4 +110,3 @@ limitations under the License. font-size: $font-12px; } } - diff --git a/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss new file mode 100644 index 0000000000..6ff328f6ab --- /dev/null +++ b/res/css/views/dialogs/_CreateSpaceFromCommunityDialog.scss @@ -0,0 +1,187 @@ +/* +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_CreateSpaceFromCommunityDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + } +} + +.mx_CreateSpaceFromCommunityDialog { + width: 480px; + color: $primary-content; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + + .mx_CreateSpaceFromCommunityDialog_content { + > p { + font-size: $font-15px; + line-height: $font-24px; + + &:first-of-type { + margin-top: 0; + } + + &.mx_CreateSpaceFromCommunityDialog_flairNotice { + font-size: $font-12px; + line-height: $font-15px; + } + } + + .mx_SpaceBasicSettings { + > p { + font-size: $font-12px; + line-height: $font-15px; + margin: 16px 0; + } + + .mx_Field_textarea { + margin-bottom: 0; + } + } + + .mx_JoinRuleDropdown .mx_Dropdown_menu { + width: auto !important; // override fixed width + } + + .mx_CreateSpaceFromCommunityDialog_nonPublicSpacer { + height: 63px; // balance the height of the missing room alias field to prevent modal bouncing + } + } + + .mx_CreateSpaceFromCommunityDialog_footer { + display: flex; + margin-top: 20px; + + > span { + flex-grow: 1; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-content; + + .mx_ProgressBar { + height: 8px; + width: 100%; + + @mixin ProgressBarBorderRadius 8px; + } + + .mx_CreateSpaceFromCommunityDialog_progressText { + margin-top: 8px; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-content; + } + + > * { + vertical-align: middle; + } + } + + .mx_CreateSpaceFromCommunityDialog_error { + padding-left: 12px; + + > img { + align-self: center; + } + + .mx_CreateSpaceFromCommunityDialog_errorHeading { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $notice-primary-color; + } + + .mx_CreateSpaceFromCommunityDialog_errorCaption { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $primary-content; + } + } + + .mx_AccessibleButton { + display: inline-block; + align-self: center; + } + + .mx_AccessibleButton_kind_primary { + padding: 8px 36px; + margin-left: 24px; + } + + .mx_AccessibleButton_kind_primary_outline { + margin-left: auto; + } + + .mx_CreateSpaceFromCommunityDialog_retryButton { + margin-left: 12px; + padding-left: 24px; + position: relative; + + &::before { + content: ''; + position: absolute; + background-color: $primary-content; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/retry.svg'); + width: 18px; + height: 18px; + left: 0; + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + } +} + +.mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog { + .mx_InfoDialog { + max-width: 500px; + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + + .mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog_checkmark { + position: relative; + border-radius: 50%; + border: 3px solid $accent-color; + width: 68px; + height: 68px; + margin: 12px auto 32px; + + &::before { + width: inherit; + height: inherit; + content: ''; + position: absolute; + background-color: $accent-color; + mask-repeat: no-repeat; + mask-position: center; + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); + mask-size: 48px; + } + } +} diff --git a/res/css/views/dialogs/_CreateSubspaceDialog.scss b/res/css/views/dialogs/_CreateSubspaceDialog.scss new file mode 100644 index 0000000000..1ed10df35c --- /dev/null +++ b/res/css/views/dialogs/_CreateSubspaceDialog.scss @@ -0,0 +1,81 @@ +/* +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_CreateSubspaceDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + } +} + +.mx_CreateSubspaceDialog { + width: 480px; + color: $primary-content; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + + .mx_CreateSubspaceDialog_content { + flex-grow: 1; + + .mx_CreateSubspaceDialog_betaNotice { + padding: 12px 16px; + border-radius: 8px; + background-color: $header-panel-bg-color; + + .mx_BetaCard_betaPill { + margin-right: 8px; + vertical-align: middle; + } + } + + .mx_JoinRuleDropdown + p { + color: $muted-fg-color; + font-size: $font-12px; + } + } + + .mx_CreateSubspaceDialog_footer { + display: flex; + margin-top: 20px; + + .mx_CreateSubspaceDialog_footer_prompt { + flex-grow: 1; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-content; + + > * { + vertical-align: middle; + } + } + + .mx_AccessibleButton { + display: inline-block; + align-self: center; + } + + .mx_AccessibleButton_kind_primary { + margin-left: 16px; + padding: 8px 36px; + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + } +} diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 8fee740016..4d35e8d569 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -55,22 +55,6 @@ limitations under the License. padding-right: 24px; } -.mx_DevTools_inputCell { - display: table-cell; - width: 240px; -} - -.mx_DevTools_inputCell input { - display: inline-block; - border: 0; - border-bottom: 1px solid $input-underline-color; - padding: 0; - width: 240px; - color: $input-fg-color; - font-family: $font-family; - font-size: $font-16px; -} - .mx_DevTools_textarea { font-size: $font-12px; max-width: 684px; @@ -139,7 +123,6 @@ limitations under the License. + .mx_DevTools_tgl-btn { padding: 2px; transition: all .2s ease; - font-family: sans-serif; perspective: 100px; &::after, &::before { diff --git a/res/css/views/dialogs/_FeedbackDialog.scss b/res/css/views/dialogs/_FeedbackDialog.scss index fd225dd882..74733f7220 100644 --- a/res/css/views/dialogs/_FeedbackDialog.scss +++ b/res/css/views/dialogs/_FeedbackDialog.scss @@ -33,7 +33,7 @@ limitations under the License. padding-left: 52px; > p { - color: $tertiary-fg-color; + color: $tertiary-content; } .mx_AccessibleButton_kind_link { diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index e018f60172..da8ce3de5b 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_ForwardDialog { width: 520px; - color: $primary-fg-color; + color: $primary-content; display: flex; flex-direction: column; flex-wrap: nowrap; @@ -25,7 +25,7 @@ limitations under the License. > h3 { margin: 0 0 6px; - color: $secondary-fg-color; + color: $secondary-content; font-size: $font-12px; font-weight: $font-semi-bold; line-height: $font-15px; diff --git a/res/css/views/dialogs/_BetaFeedbackDialog.scss b/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss similarity index 87% rename from res/css/views/dialogs/_BetaFeedbackDialog.scss rename to res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss index 9f5f6b512e..ab7496249d 100644 --- a/res/css/views/dialogs/_BetaFeedbackDialog.scss +++ b/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_BetaFeedbackDialog { - .mx_BetaFeedbackDialog_subheading { - color: $primary-fg-color; +.mx_GenericFeatureFeedbackDialog { + .mx_GenericFeatureFeedbackDialog_subheading { + color: $primary-content; font-size: $font-14px; line-height: $font-20px; margin-bottom: 24px; diff --git a/res/css/views/dialogs/_HostSignupDialog.scss b/res/css/views/dialogs/_HostSignupDialog.scss index ac4bc41951..d8a6652a39 100644 --- a/res/css/views/dialogs/_HostSignupDialog.scss +++ b/res/css/views/dialogs/_HostSignupDialog.scss @@ -70,11 +70,11 @@ limitations under the License. } .mx_HostSignupDialog_text_dark { - color: $primary-fg-color; + color: $primary-content; } .mx_HostSignupDialog_text_light { - color: $secondary-fg-color; + color: $secondary-content; } .mx_HostSignup_maximize_button { diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index 9fc4b7a15c..3a2918f9ec 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -56,7 +56,7 @@ limitations under the License. box-sizing: border-box; min-width: 40%; flex: 1 !important; - color: $primary-fg-color !important; + color: $primary-content !important; } } @@ -94,7 +94,7 @@ limitations under the License. } > span { - color: $primary-fg-color; + color: $primary-content; } .mx_InviteDialog_subname { @@ -110,7 +110,7 @@ limitations under the License. font-size: $font-14px; > span { - color: $primary-fg-color; + color: $primary-content; font-weight: 600; } @@ -220,7 +220,7 @@ limitations under the License. .mx_InviteDialog_roomTile_name { font-weight: 600; font-size: $font-14px; - color: $primary-fg-color; + color: $primary-content; margin-left: 7px; } @@ -352,7 +352,7 @@ limitations under the License. border-right: 0; border-radius: 0; margin-top: 0; - border-color: $quaternary-fg-color; + border-color: $quaternary-content; input { font-size: 18px; @@ -418,7 +418,7 @@ limitations under the License. > h4 { font-size: $font-15px; line-height: $font-24px; - color: $secondary-fg-color; + color: $secondary-content; font-weight: normal; } @@ -432,14 +432,14 @@ limitations under the License. font-size: $font-15px; line-height: $font-24px; font-weight: $font-semi-bold; - color: $primary-fg-color; + color: $primary-content; } .mx_InviteDialog_multiInviterError_entry_userId { margin-left: 6px; font-size: $font-12px; line-height: $font-15px; - color: $tertiary-fg-color; + color: $tertiary-content; } } diff --git a/res/css/views/dialogs/_JoinRuleDropdown.scss b/res/css/views/dialogs/_JoinRuleDropdown.scss new file mode 100644 index 0000000000..91691cf53b --- /dev/null +++ b/res/css/views/dialogs/_JoinRuleDropdown.scss @@ -0,0 +1,67 @@ +/* +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_JoinRuleDropdown { + margin-bottom: 8px; + font-weight: normal; + font-family: $font-family; + font-size: $font-14px; + color: $primary-content; + + .mx_Dropdown_input { + border: 1px solid $input-border-color; + } + + .mx_Dropdown_option { + font-size: $font-14px; + line-height: $font-32px; + height: 32px; + min-height: 32px; + + > div { + padding-left: 30px; + position: relative; + + &::before { + content: ""; + position: absolute; + height: 16px; + width: 16px; + left: 6px; + top: 8px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $secondary-content; + } + } + } + + .mx_JoinRuleDropdown_invite::before { + mask-image: url('$(res)/img/element-icons/lock.svg'); + mask-size: contain; + } + + .mx_JoinRuleDropdown_public::before { + mask-image: url('$(res)/img/globe.svg'); + mask-size: 12px; + } + + .mx_JoinRuleDropdown_restricted::before { + mask-image: url('$(res)/img/element-icons/community-members.svg'); + mask-size: contain; + } +} + diff --git a/res/css/views/dialogs/_LeaveSpaceDialog.scss b/res/css/views/dialogs/_LeaveSpaceDialog.scss new file mode 100644 index 0000000000..0d85a87faf --- /dev/null +++ b/res/css/views/dialogs/_LeaveSpaceDialog.scss @@ -0,0 +1,96 @@ +/* +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_LeaveSpaceDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + padding: 24px 32px; + } +} + +.mx_LeaveSpaceDialog { + width: 440px; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + max-height: 520px; + + .mx_Dialog_content { + flex-grow: 1; + margin: 0; + overflow-y: auto; + + .mx_RadioButton + .mx_RadioButton { + margin-top: 16px; + } + + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + border-radius: 8px; + } + + .mx_LeaveSpaceDialog_noResults { + display: block; + margin-top: 24px; + } + + .mx_LeaveSpaceDialog_section { + margin: 16px 0; + } + + .mx_LeaveSpaceDialog_section_warning { + position: relative; + border-radius: 8px; + margin: 12px 0 0; + padding: 12px 8px 12px 42px; + background-color: $header-panel-bg-color; + + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-content; + + &::before { + content: ''; + position: absolute; + left: 10px; + top: calc(50% - 8px); // vertical centering + height: 16px; + width: 16px; + background-color: $secondary-content; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } + + > p { + color: $primary-content; + } + } + + .mx_Dialog_buttons { + margin-top: 20px; + + .mx_Dialog_primary { + background-color: $notice-primary-color !important; // override default colour + border-color: $notice-primary-color; + } + } +} diff --git a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss new file mode 100644 index 0000000000..9a05e7f20a --- /dev/null +++ b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss @@ -0,0 +1,150 @@ +/* +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_ManageRestrictedJoinRuleDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + } +} + +.mx_ManageRestrictedJoinRuleDialog { + width: 480px; + color: $primary-content; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + height: 60vh; + + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + } + + .mx_ManageRestrictedJoinRuleDialog_content { + flex-grow: 1; + } + + .mx_ManageRestrictedJoinRuleDialog_noResults { + display: block; + margin-top: 24px; + } + + .mx_ManageRestrictedJoinRuleDialog_section { + &:not(:first-child) { + margin-top: 24px; + } + + > h3 { + margin: 0; + color: $secondary-content; + font-size: $font-12px; + font-weight: $font-semi-bold; + line-height: $font-15px; + } + + .mx_ManageRestrictedJoinRuleDialog_entry { + display: flex; + margin-top: 12px; + + > div { + flex-grow: 1; + } + + img.mx_RoomAvatar_isSpaceRoom, + .mx_RoomAvatar_isSpaceRoom img { + border-radius: 4px; + } + + .mx_ManageRestrictedJoinRuleDialog_entry_name { + margin: 0 8px; + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .mx_ManageRestrictedJoinRuleDialog_entry_description { + margin-top: 8px; + font-size: $font-12px; + line-height: $font-15px; + color: $tertiary-content; + } + + .mx_Checkbox { + align-items: center; + } + } + } + + .mx_ManageRestrictedJoinRuleDialog_section_spaces { + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + } + + .mx_ManageRestrictedJoinRuleDialog_section_info { + position: relative; + border-radius: 8px; + margin: 12px 0; + padding: 8px 8px 8px 42px; + background-color: $header-panel-bg-color; + + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-content; + + &::before { + content: ''; + position: absolute; + left: 10px; + top: calc(50% - 8px); // vertical centering + height: 16px; + width: 16px; + background-color: $secondary-content; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } + + .mx_ManageRestrictedJoinRuleDialog_footer { + margin-top: 20px; + + .mx_ManageRestrictedJoinRuleDialog_footer_buttons { + display: flex; + width: max-content; + margin-left: auto; + + .mx_AccessibleButton { + display: inline-block; + + & + .mx_AccessibleButton { + margin-left: 24px; + } + } + } + } +} diff --git a/res/css/views/dialogs/_MessageEditHistoryDialog.scss b/res/css/views/dialogs/_MessageEditHistoryDialog.scss index e9d777effd..4574344a28 100644 --- a/res/css/views/dialogs/_MessageEditHistoryDialog.scss +++ b/res/css/views/dialogs/_MessageEditHistoryDialog.scss @@ -37,7 +37,7 @@ limitations under the License. list-style-type: none; font-size: $font-14px; padding: 0; - color: $primary-fg-color; + color: $primary-content; span.mx_EditHistoryMessage_deletion, span.mx_EditHistoryMessage_insertion { padding: 0px 2px; diff --git a/res/css/views/dialogs/_RegistrationEmailPromptDialog.scss b/res/css/views/dialogs/_RegistrationEmailPromptDialog.scss index 31fc6d7a04..02c89e2e42 100644 --- a/res/css/views/dialogs/_RegistrationEmailPromptDialog.scss +++ b/res/css/views/dialogs/_RegistrationEmailPromptDialog.scss @@ -19,7 +19,7 @@ limitations under the License. .mx_Dialog_content { margin-bottom: 24px; - color: $tertiary-fg-color; + color: $tertiary-content; } .mx_Dialog_primary { diff --git a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss index c97a3b69b7..f18b4917cf 100644 --- a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss +++ b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss @@ -72,7 +72,7 @@ limitations under the License. margin-top: 0px; margin-bottom: 0px; font-size: 16pt; - color: $primary-fg-color; + color: $primary-content; } > * { @@ -81,7 +81,7 @@ limitations under the License. } .workspace-channel-details { - color: $primary-fg-color; + color: $primary-content; font-weight: 600; .channel { diff --git a/res/css/views/dialogs/_ServerOfflineDialog.scss b/res/css/views/dialogs/_ServerOfflineDialog.scss index ae4b70beb3..7a1b0bbcab 100644 --- a/res/css/views/dialogs/_ServerOfflineDialog.scss +++ b/res/css/views/dialogs/_ServerOfflineDialog.scss @@ -17,10 +17,10 @@ limitations under the License. .mx_ServerOfflineDialog { .mx_ServerOfflineDialog_content { padding-right: 85px; - color: $primary-fg-color; + color: $primary-content; hr { - border-color: $primary-fg-color; + border-color: $primary-content; opacity: 0.1; border-bottom: none; } diff --git a/res/css/views/dialogs/_ServerPickerDialog.scss b/res/css/views/dialogs/_ServerPickerDialog.scss index b01b49d7af..9a05751f91 100644 --- a/res/css/views/dialogs/_ServerPickerDialog.scss +++ b/res/css/views/dialogs/_ServerPickerDialog.scss @@ -22,7 +22,7 @@ limitations under the License. margin-bottom: 0; > p { - color: $secondary-fg-color; + color: $secondary-content; font-size: $font-14px; margin: 16px 0; @@ -38,7 +38,7 @@ limitations under the License. > h4 { font-size: $font-15px; font-weight: $font-semi-bold; - color: $secondary-fg-color; + color: $secondary-content; margin-left: 8px; } diff --git a/res/css/views/dialogs/_SetEmailDialog.scss b/res/css/views/dialogs/_SetEmailDialog.scss index 37bee7a9ff..a39d51dfce 100644 --- a/res/css/views/dialogs/_SetEmailDialog.scss +++ b/res/css/views/dialogs/_SetEmailDialog.scss @@ -19,7 +19,7 @@ limitations under the License. border: 1px solid $input-border-color; padding: 9px; color: $input-fg-color; - background-color: $primary-bg-color; + background-color: $background; font-size: $font-15px; width: 100%; max-width: 280px; diff --git a/res/css/views/dialogs/_SpaceSettingsDialog.scss b/res/css/views/dialogs/_SpaceSettingsDialog.scss index fa074fdbe8..a1fa9d52a8 100644 --- a/res/css/views/dialogs/_SpaceSettingsDialog.scss +++ b/res/css/views/dialogs/_SpaceSettingsDialog.scss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_SpaceSettingsDialog { - color: $primary-fg-color; + color: $primary-content; .mx_SpaceSettings_errorText { font-weight: $font-semi-bold; @@ -50,13 +50,13 @@ limitations under the License. .mx_RadioButton_content { font-weight: $font-semi-bold; line-height: $font-18px; - color: $primary-fg-color; + color: $primary-content; } & + span { font-size: $font-15px; line-height: $font-18px; - color: $secondary-fg-color; + color: $secondary-content; margin-left: 26px; } } diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss index ec3bea0ef7..98edbf8ad8 100644 --- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss +++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss @@ -44,7 +44,7 @@ limitations under the License. margin-right: 8px; position: relative; top: 5px; - background-color: $primary-fg-color; + background-color: $primary-content; } .mx_AccessSecretStorageDialog_resetBadge::before { diff --git a/res/css/views/dialogs/security/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/security/_CreateSecretStorageDialog.scss index d30803b1f0..b14206ff6d 100644 --- a/res/css/views/dialogs/security/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/security/_CreateSecretStorageDialog.scss @@ -56,7 +56,7 @@ limitations under the License. margin-right: 8px; position: relative; top: 5px; - background-color: $primary-fg-color; + background-color: $primary-content; } .mx_CreateSecretStorageDialog_secureBackupTitle::before { @@ -101,7 +101,7 @@ limitations under the License. margin-right: 8px; position: relative; top: 5px; - background-color: $primary-fg-color; + background-color: $primary-content; } .mx_CreateSecretStorageDialog_optionIcon_securePhrase { diff --git a/res/css/views/dialogs/security/_KeyBackupFailedDialog.scss b/res/css/views/dialogs/security/_KeyBackupFailedDialog.scss index 05ce158413..4a48012672 100644 --- a/res/css/views/dialogs/security/_KeyBackupFailedDialog.scss +++ b/res/css/views/dialogs/security/_KeyBackupFailedDialog.scss @@ -26,7 +26,7 @@ limitations under the License. &::before { mask: url("$(res)/img/e2e/lock-warning-filled.svg"); mask-repeat: no-repeat; - background-color: $primary-fg-color; + background-color: $primary-content; content: ""; position: absolute; top: -6px; diff --git a/res/css/views/directory/_NetworkDropdown.scss b/res/css/views/directory/_NetworkDropdown.scss index ae0927386a..93cecd8676 100644 --- a/res/css/views/directory/_NetworkDropdown.scss +++ b/res/css/views/directory/_NetworkDropdown.scss @@ -34,7 +34,7 @@ limitations under the License. box-sizing: border-box; border-radius: 4px; border: 1px solid $dialog-close-fg-color; - background-color: $primary-bg-color; + background-color: $background; max-height: calc(100vh - 20px); // allow 10px padding on both top and bottom overflow-y: auto; } @@ -153,7 +153,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-image: url('$(res)/img/feather-customised/chevron-down-thin.svg'); - background-color: $primary-fg-color; + background-color: $primary-content; } .mx_NetworkDropdown_handle_server { diff --git a/res/css/views/elements/_AddressSelector.scss b/res/css/views/elements/_AddressSelector.scss index 087504390c..a7d463353b 100644 --- a/res/css/views/elements/_AddressSelector.scss +++ b/res/css/views/elements/_AddressSelector.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_AddressSelector { position: absolute; - background-color: $primary-bg-color; + background-color: $background; width: 485px; max-height: 116px; overflow-y: auto; @@ -31,8 +31,8 @@ limitations under the License. } .mx_AddressSelector_addressListElement .mx_AddressTile { - background-color: $primary-bg-color; - border: solid 1px $primary-bg-color; + background-color: $background; + border: solid 1px $background; } .mx_AddressSelector_addressListElement.mx_AddressSelector_selected { diff --git a/res/css/views/elements/_AddressTile.scss b/res/css/views/elements/_AddressTile.scss index c42f52f8f4..90c40842f7 100644 --- a/res/css/views/elements/_AddressTile.scss +++ b/res/css/views/elements/_AddressTile.scss @@ -20,7 +20,7 @@ limitations under the License. background-color: rgba(74, 73, 74, 0.1); border: solid 1px $input-border-color; line-height: $font-26px; - color: $primary-fg-color; + color: $primary-content; font-size: $font-14px; font-weight: normal; margin-right: 4px; diff --git a/res/css/views/elements/_DesktopCapturerSourcePicker.scss b/res/css/views/elements/_DesktopCapturerSourcePicker.scss index 69dde5925e..b4a2c69b86 100644 --- a/res/css/views/elements/_DesktopCapturerSourcePicker.scss +++ b/res/css/views/elements/_DesktopCapturerSourcePicker.scss @@ -16,57 +16,41 @@ limitations under the License. .mx_desktopCapturerSourcePicker { overflow: hidden; -} -.mx_desktopCapturerSourcePicker_tabLabels { - display: flex; - padding: 0 0 8px 0; -} + .mx_desktopCapturerSourcePicker_tab { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + height: 500px; + overflow: overlay; -.mx_desktopCapturerSourcePicker_tabLabel, -.mx_desktopCapturerSourcePicker_tabLabel_selected { - width: 100%; - text-align: center; - border-radius: 8px; - padding: 8px 0; - font-size: $font-13px; -} + .mx_desktopCapturerSourcePicker_source { + width: 50%; + display: flex; + flex-direction: column; -.mx_desktopCapturerSourcePicker_tabLabel_selected { - background-color: $tab-label-active-bg-color; - color: $tab-label-active-fg-color; -} + .mx_desktopCapturerSourcePicker_source_thumbnail { + margin: 4px; + padding: 4px; + border-width: 2px; + border-radius: 8px; + border-style: solid; + border-color: transparent; -.mx_desktopCapturerSourcePicker_panel { - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: flex-start; - height: 500px; - overflow: overlay; -} + &.mx_desktopCapturerSourcePicker_source_thumbnail_selected, + &:hover, + &:focus { + border-color: $accent-color; + } + } -.mx_desktopCapturerSourcePicker_stream_button { - display: flex; - flex-direction: column; - margin: 8px; - border-radius: 4px; -} - -.mx_desktopCapturerSourcePicker_stream_button:hover, -.mx_desktopCapturerSourcePicker_stream_button:focus { - background: $roomtile-selected-bg-color; -} - -.mx_desktopCapturerSourcePicker_stream_thumbnail { - margin: 4px; - width: 312px; -} - -.mx_desktopCapturerSourcePicker_stream_name { - margin: 0 4px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - width: 312px; + .mx_desktopCapturerSourcePicker_source_name { + margin: 0 4px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + } } diff --git a/res/css/views/elements/_Dropdown.scss b/res/css/views/elements/_Dropdown.scss index 2a2508c17c..1acac70e42 100644 --- a/res/css/views/elements/_Dropdown.scss +++ b/res/css/views/elements/_Dropdown.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_Dropdown { position: relative; - color: $primary-fg-color; + color: $primary-content; } .mx_Dropdown_disabled { @@ -27,7 +27,7 @@ limitations under the License. display: flex; align-items: center; position: relative; - border-radius: 3px; + border-radius: 4px; border: 1px solid $strong-input-border-color; font-size: $font-12px; user-select: none; @@ -52,7 +52,7 @@ limitations under the License. padding-right: 9px; mask: url('$(res)/img/feather-customised/dropdown-arrow.svg'); mask-repeat: no-repeat; - background: $primary-fg-color; + background: $primary-content; } .mx_Dropdown_option { @@ -109,9 +109,9 @@ input.mx_Dropdown_option:focus { z-index: 2; margin: 0; padding: 0px; - border-radius: 3px; + border-radius: 4px; border: 1px solid $input-focused-border-color; - background-color: $primary-bg-color; + background-color: $background; max-height: 200px; overflow-y: auto; } diff --git a/res/css/views/elements/_EventTilePreview.scss b/res/css/views/elements/_EventTilePreview.scss new file mode 100644 index 0000000000..6bb726168f --- /dev/null +++ b/res/css/views/elements/_EventTilePreview.scss @@ -0,0 +1,22 @@ +/* +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_EventTilePreview_loader { + &.mx_IRCLayout, + &.mx_GroupLayout { + padding: 9px 0; + } +} diff --git a/res/css/views/elements/_FacePile.scss b/res/css/views/elements/_FacePile.scss index c691baffb5..875e0e34d5 100644 --- a/res/css/views/elements/_FacePile.scss +++ b/res/css/views/elements/_FacePile.scss @@ -25,7 +25,7 @@ limitations under the License. } .mx_BaseAvatar_image { - border: 1px solid $primary-bg-color; + border: 1px solid $background; } .mx_BaseAvatar_initial { @@ -47,7 +47,7 @@ limitations under the License. left: 0; height: inherit; width: inherit; - background: $tertiary-fg-color; + background: $tertiary-content; mask-position: center; mask-size: 20px; mask-repeat: no-repeat; @@ -60,6 +60,6 @@ limitations under the License. margin-left: 12px; font-size: $font-14px; line-height: $font-24px; - color: $tertiary-fg-color; + color: $tertiary-content; } } diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index f67da6477b..d74c985d4c 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -38,16 +38,16 @@ limitations under the License. .mx_Field input, .mx_Field select, .mx_Field textarea { + font-family: inherit; font-weight: normal; - font-family: $font-family; font-size: $font-14px; border: none; // Even without a border here, we still need this avoid overlapping the rounded // corners on the field above. border-radius: 4px; padding: 8px 9px; - color: $primary-fg-color; - background-color: $primary-bg-color; + color: $primary-content; + background-color: $background; flex: 1; min-width: 0; } @@ -67,7 +67,7 @@ limitations under the License. height: 6px; mask: url('$(res)/img/feather-customised/dropdown-arrow.svg'); mask-repeat: no-repeat; - background-color: $primary-fg-color; + background-color: $primary-content; z-index: 1; pointer-events: none; } @@ -100,7 +100,7 @@ limitations under the License. color 0.25s ease-out 0.1s, top 0.25s ease-out 0.1s, background-color 0.25s ease-out 0.1s; - color: $primary-fg-color; + color: $primary-content; background-color: transparent; font-size: $font-14px; position: absolute; diff --git a/res/css/views/elements/_InviteReason.scss b/res/css/views/elements/_InviteReason.scss index 2c2e5687e6..8024ed59a3 100644 --- a/res/css/views/elements/_InviteReason.scss +++ b/res/css/views/elements/_InviteReason.scss @@ -32,12 +32,12 @@ limitations under the License. justify-content: center; align-items: center; cursor: pointer; - color: $secondary-fg-color; + color: $secondary-content; &::before { content: ""; margin-right: 8px; - background-color: $secondary-fg-color; + background-color: $secondary-content; mask-image: url('$(res)/img/feather-customised/eye.svg'); display: inline-block; width: 18px; diff --git a/res/css/views/elements/_MiniAvatarUploader.scss b/res/css/views/elements/_MiniAvatarUploader.scss index df4676ab56..46ffd9a01c 100644 --- a/res/css/views/elements/_MiniAvatarUploader.scss +++ b/res/css/views/elements/_MiniAvatarUploader.scss @@ -37,7 +37,7 @@ limitations under the License. right: -6px; bottom: -6px; - background-color: $primary-bg-color; + background-color: $background; border-radius: 50%; z-index: 1; @@ -45,7 +45,7 @@ limitations under the License. height: 100%; width: 100%; - background-color: $secondary-fg-color; + background-color: $secondary-content; mask-position: center; mask-repeat: no-repeat; mask-image: url('$(res)/img/element-icons/camera.svg'); diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss index d60282695c..b9d845ea7a 100644 --- a/res/css/views/elements/_RichText.scss +++ b/res/css/views/elements/_RichText.scss @@ -43,7 +43,7 @@ a.mx_Pill { /* More specific to override `.markdown-body a` color */ .mx_EventTile_content .markdown-body a.mx_UserPill, .mx_UserPill { - color: $primary-fg-color; + color: $primary-content; background-color: $other-user-pill-bg-color; } diff --git a/res/css/views/elements/_SSOButtons.scss b/res/css/views/elements/_SSOButtons.scss index e02816780f..a98e7b4024 100644 --- a/res/css/views/elements/_SSOButtons.scss +++ b/res/css/views/elements/_SSOButtons.scss @@ -35,7 +35,7 @@ limitations under the License. font-size: $font-14px; font-weight: $font-semi-bold; border: 1px solid $input-border-color; - color: $primary-fg-color; + color: $primary-content; > img { object-fit: contain; diff --git a/res/css/views/elements/_ServerPicker.scss b/res/css/views/elements/_ServerPicker.scss index 188eb5d655..d828d7cb88 100644 --- a/res/css/views/elements/_ServerPicker.scss +++ b/res/css/views/elements/_ServerPicker.scss @@ -74,7 +74,7 @@ limitations under the License. .mx_ServerPicker_desc { margin-top: -12px; - color: $tertiary-fg-color; + color: $tertiary-content; grid-column: 1 / 2; grid-row: 3; margin-bottom: 16px; diff --git a/res/css/views/elements/_Spinner.scss b/res/css/views/elements/_Spinner.scss index 93d5e2d96c..2df46687af 100644 --- a/res/css/views/elements/_Spinner.scss +++ b/res/css/views/elements/_Spinner.scss @@ -37,7 +37,7 @@ limitations under the License. } .mx_Spinner_icon { - background-color: $primary-fg-color; + background-color: $primary-content; mask: url('$(res)/img/spinner.svg'); mask-size: contain; animation: 1.1s steps(12, end) infinite spin; diff --git a/res/css/views/elements/_TagComposer.scss b/res/css/views/elements/_TagComposer.scss index 2ffd601765..f5bdb8d2d5 100644 --- a/res/css/views/elements/_TagComposer.scss +++ b/res/css/views/elements/_TagComposer.scss @@ -25,7 +25,7 @@ limitations under the License. .mx_AccessibleButton { min-width: 70px; - padding: 0; // override from button styles + padding: 0 8px; // override from button styles margin-left: 16px; // distance from } @@ -50,7 +50,7 @@ limitations under the License. &::before { content: ''; border-radius: 20px; - background-color: $tertiary-fg-color; + background-color: $tertiary-content; opacity: 0.15; position: absolute; top: 0; diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index d90c818f94..6c5a7da55a 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -84,7 +84,7 @@ limitations under the License. // These tooltips use an older style with a chevron .mx_Field_tooltip { background-color: $menu-bg-color; - color: $primary-fg-color; + color: $primary-content; border: 1px solid $menu-border-color; text-align: unset; diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 400e40e233..91c68158c9 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -57,7 +57,7 @@ limitations under the License. } .mx_EmojiPicker_anchor::before { - background-color: $primary-fg-color; + background-color: $primary-content; content: ''; display: inline-block; mask-size: 100%; @@ -89,7 +89,7 @@ limitations under the License. margin: 8px; border-radius: 4px; border: 1px solid $input-border-color; - background-color: $primary-bg-color; + background-color: $background; display: flex; input { @@ -126,7 +126,7 @@ limitations under the License. .mx_EmojiPicker_search_icon::after { mask: url('$(res)/img/emojipicker/search.svg') no-repeat; mask-size: 100%; - background-color: $primary-fg-color; + background-color: $primary-content; content: ''; display: inline-block; width: 100%; diff --git a/res/css/views/groups/_GroupRoomList.scss b/res/css/views/groups/_GroupRoomList.scss index fefd17849c..2f6559f7c4 100644 --- a/res/css/views/groups/_GroupRoomList.scss +++ b/res/css/views/groups/_GroupRoomList.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_GroupRoomTile { position: relative; - color: $primary-fg-color; + color: $primary-content; cursor: pointer; display: flex; align-items: center; diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 54c7df3e0b..7934f8f3c2 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -14,118 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_CallEvent { +.mx_CallEvent_wrapper { display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; + width: 100%; - background-color: $dark-panel-bg-color; - border-radius: 8px; - margin: 10px auto; - max-width: 75%; - box-sizing: border-box; - height: 60px; - - &.mx_CallEvent_voice { - .mx_CallEvent_type_icon::before, - .mx_CallEvent_content_button_callBack span::before, - .mx_CallEvent_content_button_answer span::before { - mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); - } - } - - &.mx_CallEvent_video { - .mx_CallEvent_type_icon::before, - .mx_CallEvent_content_button_callBack span::before, - .mx_CallEvent_content_button_answer span::before { - mask-image: url('$(res)/img/element-icons/call/video-call.svg'); - } - } - - .mx_CallEvent_info { + .mx_CallEvent { + position: relative; display: flex; flex-direction: row; align-items: center; - margin-left: 12px; + justify-content: space-between; - .mx_CallEvent_info_basic { - display: flex; - flex-direction: column; - margin-left: 10px; // To match mx_CallEvent - - .mx_CallEvent_sender { - font-weight: 600; - font-size: 1.5rem; - line-height: 1.8rem; - margin-bottom: 3px; - } - - .mx_CallEvent_type { - font-weight: 400; - color: $secondary-fg-color; - font-size: 1.2rem; - line-height: $font-13px; - display: flex; - align-items: center; - - .mx_CallEvent_type_icon { - height: 13px; - width: 13px; - margin-right: 5px; - - &::before { - content: ''; - position: absolute; - height: 13px; - width: 13px; - background-color: $tertiary-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - } - } - } - } - } - - .mx_CallEvent_content { - display: flex; - flex-direction: row; - align-items: center; - color: $secondary-fg-color; - margin-right: 16px; - - .mx_CallEvent_content_button { - height: 24px; - padding: 0px 12px; - margin-left: 8px; - - span { - padding: 8px 0; - display: flex; - align-items: center; - - &::before { - content: ''; - display: inline-block; - background-color: $button-fg-color; - mask-position: center; - mask-repeat: no-repeat; - mask-size: 16px; - width: 16px; - height: 16px; - margin-right: 8px; - } - } - } - - .mx_CallEvent_content_button_reject span::before { - mask-image: url('$(res)/img/element-icons/call/hangup.svg'); - } - - .mx_CallEvent_content_tooltip { - margin-right: 5px; - } + background-color: $dark-panel-bg-color; + border-radius: 8px; + width: 65%; + box-sizing: border-box; + height: 60px; + margin: 4px 0; .mx_CallEvent_iconButton { display: inline-flex; @@ -136,7 +41,7 @@ limitations under the License. height: 16px; width: 16px; - background-color: $tertiary-fg-color; + background-color: $secondary-content; mask-repeat: no-repeat; mask-size: contain; mask-position: center; @@ -150,5 +55,165 @@ limitations under the License. .mx_CallEvent_unSilence::before { mask-image: url('$(res)/img/voip/un-silence.svg'); } + + &.mx_CallEvent_voice { + .mx_CallEvent_type_icon::before, + .mx_CallEvent_content_button_callBack span::before, + .mx_CallEvent_content_button_answer span::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } + } + + &.mx_CallEvent_video { + .mx_CallEvent_type_icon::before, + .mx_CallEvent_content_button_callBack span::before, + .mx_CallEvent_content_button_answer span::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } + } + + &.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-voice.svg'); + } + + &.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-video.svg'); + } + + &.mx_CallEvent_voice.mx_CallEvent_rejected .mx_CallEvent_type_icon::before, + &.mx_CallEvent_voice.mx_CallEvent_noAnswer .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/declined-voice.svg'); + } + + &.mx_CallEvent_video.mx_CallEvent_rejected .mx_CallEvent_type_icon::before, + &.mx_CallEvent_video.mx_CallEvent_noAnswer .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/declined-video.svg'); + } + + .mx_CallEvent_info { + display: flex; + flex-direction: row; + align-items: center; + margin-left: 12px; + min-width: 0; + + .mx_CallEvent_info_basic { + display: flex; + flex-direction: column; + margin-left: 10px; // To match mx_CallEvent + min-width: 0; + + .mx_CallEvent_sender { + font-weight: 600; + font-size: 1.5rem; + line-height: 1.8rem; + margin-bottom: 3px; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .mx_CallEvent_type { + font-weight: 400; + color: $secondary-content; + font-size: 1.2rem; + line-height: $font-13px; + display: flex; + align-items: center; + + .mx_CallEvent_type_icon { + height: 13px; + width: 13px; + margin-right: 5px; + + &::before { + content: ''; + position: absolute; + height: 13px; + width: 13px; + background-color: $secondary-content; + mask-repeat: no-repeat; + mask-size: contain; + } + } + } + } + } + + .mx_CallEvent_content { + display: flex; + flex-direction: row; + align-items: center; + color: $secondary-content; + margin-right: 16px; + gap: 8px; + min-width: max-content; + + .mx_CallEvent_content_button { + padding: 0px 12px; + + span { + padding: 1px 0; + display: flex; + align-items: center; + + &::before { + content: ''; + display: inline-block; + background-color: $button-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 16px; + width: 16px; + height: 16px; + margin-right: 8px; + + flex-shrink: 0; + } + } + } + + .mx_CallEvent_content_button_reject span::before { + mask-image: url('$(res)/img/element-icons/call/hangup.svg'); + } + + .mx_CallEvent_content_tooltip { + margin-right: 5px; + } + } + + &.mx_CallEvent_narrow { + height: unset; + width: 290px; + flex-direction: column; + align-items: unset; + gap: 16px; + + .mx_CallEvent_iconButton { + position: absolute; + margin-right: 0; + top: 12px; + right: 12px; + height: 16px; + width: 16px; + display: flex; + } + + .mx_CallEvent_info { + align-items: unset; + margin-top: 12px; + margin-right: 12px; + + .mx_CallEvent_sender { + margin-bottom: 8px; + } + } + + .mx_CallEvent_content { + margin-left: 54px; // mx_CallEvent margin (12px) + avatar (32px) + mx_CallEvent_info_basic margin (10px) + margin-bottom: 16px; + } + } } } diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss index 403f671673..d941a8132f 100644 --- a/res/css/views/messages/_MFileBody.scss +++ b/res/css/views/messages/_MFileBody.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016, 2021 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -60,6 +60,8 @@ limitations under the License. } .mx_MFileBody_info { + cursor: pointer; + .mx_MFileBody_info_icon { background-color: $message-body-panel-icon-bg-color; border-radius: 20px; diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index f5d8131e6e..920c3011f5 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -16,8 +16,10 @@ limitations under the License. $timelineImageBorderRadius: 4px; -.mx_MImageBody { - display: block; +.mx_MImageBody_thumbnail--blurhash { + position: absolute; + left: 0; + top: 0; } .mx_MImageBody_thumbnail { @@ -27,10 +29,17 @@ $timelineImageBorderRadius: 4px; display: flex; justify-content: center; align-items: center; + height: 100%; + width: 100%; - > canvas { + .mx_Blurhash > canvas { + animation: mx--anim-pulse 1.75s infinite cubic-bezier(.4, 0, .6, 1); border-radius: $timelineImageBorderRadius; } + + .mx_no-image-placeholder { + background-color: $primary-content; + } } .mx_MImageBody_thumbnail_container { @@ -91,5 +100,5 @@ $timelineImageBorderRadius: 4px; } .mx_EventTile:hover .mx_HiddenImagePlaceholder { - background-color: $primary-bg-color; + background-color: $background; } diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 69f3c672b7..6805036e3d 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -23,7 +23,7 @@ limitations under the License. height: 32px; line-height: $font-24px; border-radius: 8px; - background: $primary-bg-color; + background: $background; border: 1px solid $input-border-color; top: -32px; right: 8px; @@ -77,11 +77,11 @@ limitations under the License. mask-size: 18px; mask-repeat: no-repeat; mask-position: center; - background-color: $secondary-fg-color; + background-color: $secondary-content; } .mx_MessageActionBar_maskButton:hover::after { - background-color: $primary-fg-color; + background-color: $primary-content; } .mx_MessageActionBar_reactButton::after { @@ -92,6 +92,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/message-bar/reply.svg'); } +.mx_MessageActionBar_threadButton::after { + mask-image: url('$(res)/img/element-icons/message/thread.svg'); +} + .mx_MessageActionBar_editButton::after { mask-image: url('$(res)/img/element-icons/room/message-bar/edit.svg'); } diff --git a/res/css/views/messages/_ReactionsRow.scss b/res/css/views/messages/_ReactionsRow.scss index b2bca6dfb3..1b0b847932 100644 --- a/res/css/views/messages/_ReactionsRow.scss +++ b/res/css/views/messages/_ReactionsRow.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_ReactionsRow { margin: 6px 0; - color: $primary-fg-color; + color: $primary-content; .mx_ReactionsRow_addReactionButton { position: relative; @@ -36,7 +36,7 @@ limitations under the License. mask-size: 16px; mask-repeat: no-repeat; mask-position: center; - background-color: $tertiary-fg-color; + background-color: $tertiary-content; mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg'); } @@ -46,7 +46,7 @@ limitations under the License. &:hover, &.mx_ReactionsRow_addReactionButton_active { &::before { - background-color: $primary-fg-color; + background-color: $primary-content; } } } @@ -64,10 +64,10 @@ limitations under the License. vertical-align: middle; &:link, &:visited { - color: $tertiary-fg-color; + color: $tertiary-content; } &:hover { - color: $primary-fg-color; + color: $primary-content; } } diff --git a/res/css/views/messages/_ViewSourceEvent.scss b/res/css/views/messages/_ViewSourceEvent.scss index 66825030e0..b0e40a5152 100644 --- a/res/css/views/messages/_ViewSourceEvent.scss +++ b/res/css/views/messages/_ViewSourceEvent.scss @@ -43,8 +43,10 @@ limitations under the License. margin-bottom: 7px; mask-image: url('$(res)/img/feather-customised/minimise.svg'); } +} - &:hover .mx_ViewSourceEvent_toggle { +.mx_EventTile:hover { + .mx_ViewSourceEvent_toggle { visibility: visible; } } diff --git a/res/css/views/right_panel/_BaseCard.scss b/res/css/views/right_panel/_BaseCard.scss index 9a5a59bda8..8c1a55fe05 100644 --- a/res/css/views/right_panel/_BaseCard.scss +++ b/res/css/views/right_panel/_BaseCard.scss @@ -93,7 +93,7 @@ limitations under the License. } > h1 { - color: $tertiary-fg-color; + color: $tertiary-content; font-size: $font-12px; font-weight: 500; } @@ -145,7 +145,7 @@ limitations under the License. justify-content: space-around; .mx_AccessibleButton_kind_secondary { - color: $secondary-fg-color; + color: $secondary-content; background-color: rgba(141, 151, 165, 0.2); font-weight: $font-semi-bold; font-size: $font-14px; diff --git a/res/css/views/right_panel/_PinnedMessagesCard.scss b/res/css/views/right_panel/_PinnedMessagesCard.scss index 785aee09ca..f3861a3dec 100644 --- a/res/css/views/right_panel/_PinnedMessagesCard.scss +++ b/res/css/views/right_panel/_PinnedMessagesCard.scss @@ -48,7 +48,7 @@ limitations under the License. height: 32px; line-height: $font-24px; border-radius: 8px; - background: $primary-bg-color; + background: $background; border: 1px solid $input-border-color; padding: 1px; width: max-content; @@ -66,7 +66,7 @@ limitations under the License. z-index: 1; &::after { - background-color: $primary-fg-color; + background-color: $primary-content; } } } @@ -75,7 +75,7 @@ limitations under the License. font-weight: $font-semi-bold; font-size: $font-15px; line-height: $font-24px; - color: $primary-fg-color; + color: $primary-content; margin-top: 24px; margin-bottom: 20px; } @@ -83,7 +83,7 @@ limitations under the License. > span { font-size: $font-12px; line-height: $font-15px; - color: $secondary-fg-color; + color: $secondary-content; } } } diff --git a/res/css/views/right_panel/_RoomSummaryCard.scss b/res/css/views/right_panel/_RoomSummaryCard.scss index dc7804d072..7ac4787111 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.scss +++ b/res/css/views/right_panel/_RoomSummaryCard.scss @@ -27,7 +27,7 @@ limitations under the License. .mx_RoomSummaryCard_alias { font-size: $font-13px; - color: $secondary-fg-color; + color: $secondary-content; } h2, .mx_RoomSummaryCard_alias { @@ -115,7 +115,7 @@ limitations under the License. // as we will be applying it in its children padding: 0; height: auto; - color: $tertiary-fg-color; + color: $tertiary-content; .mx_RoomSummaryCard_icon_app { padding: 10px 48px 10px 12px; // based on typical mx_RoomSummaryCard_Button padding @@ -128,7 +128,7 @@ limitations under the License. } span { - color: $primary-fg-color; + color: $primary-content; } } @@ -232,6 +232,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/files.svg'); } +.mx_RoomSummaryCard_icon_threads::before { + mask-image: url('$(res)/img/element-icons/message/thread.svg'); +} + .mx_RoomSummaryCard_icon_share::before { mask-image: url('$(res)/img/element-icons/room/share.svg'); } diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 6632ccddf9..edc82cfdbf 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -55,7 +55,7 @@ limitations under the License. } .mx_UserInfo_separator { - border-bottom: 1px solid rgba($primary-fg-color, .1); + border-bottom: 1px solid rgba($primary-content, .1); } .mx_UserInfo_memberDetailsContainer { diff --git a/res/css/views/right_panel/_WidgetCard.scss b/res/css/views/right_panel/_WidgetCard.scss index a90e744a5a..824f1fcb2f 100644 --- a/res/css/views/right_panel/_WidgetCard.scss +++ b/res/css/views/right_panel/_WidgetCard.scss @@ -51,7 +51,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); - background-color: $secondary-fg-color; + background-color: $secondary-content; } } } diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index fd80836237..cfcb0c48a2 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -64,7 +64,7 @@ $MiniAppTileHeight: 200px; &:hover { .mx_AppsContainer_resizerHandle::after { opacity: 0.8; - background: $primary-fg-color; + background: $primary-content; } .mx_ResizeHandle_horizontal::before { @@ -79,7 +79,7 @@ $MiniAppTileHeight: 200px; content: ''; - background-color: $primary-fg-color; + background-color: $primary-content; opacity: 0.8; } } diff --git a/res/css/views/rooms/_Autocomplete.scss b/res/css/views/rooms/_Autocomplete.scss index f8e0a382b1..8d2b338d9d 100644 --- a/res/css/views/rooms/_Autocomplete.scss +++ b/res/css/views/rooms/_Autocomplete.scss @@ -4,27 +4,30 @@ z-index: 1001; width: 100%; border: 1px solid $primary-hairline-color; - background: $primary-bg-color; + background: $background; border-bottom: none; border-radius: 8px 8px 0 0; - max-height: 50vh; - overflow: auto; + max-height: 35vh; + overflow: clip; + display: flex; + flex-direction: column; box-shadow: 0px -16px 32px $composer-shadow-color; } .mx_Autocomplete_ProviderSection { border-bottom: 1px solid $primary-hairline-color; + width: 100%; } /* a "block" completion takes up a whole line */ .mx_Autocomplete_Completion_block { - height: 34px; + min-height: 34px; display: flex; padding: 0 12px; user-select: none; cursor: pointer; align-items: center; - color: $primary-fg-color; + color: $primary-content; } .mx_Autocomplete_Completion_block * { @@ -40,7 +43,7 @@ user-select: none; cursor: pointer; align-items: center; - color: $primary-fg-color; + color: $primary-content; } .mx_Autocomplete_Completion_pill > * { @@ -59,8 +62,8 @@ .mx_Autocomplete_Completion_container_pill { margin: 12px; - display: flex; - flex-flow: wrap; + height: 100%; + overflow-y: scroll; } .mx_Autocomplete_Completion_container_truncate { @@ -68,7 +71,6 @@ .mx_Autocomplete_Completion_subtitle, .mx_Autocomplete_Completion_description { /* Ellipsis for long names/subtitles/descriptions */ - max-width: 150px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -83,7 +85,7 @@ .mx_Autocomplete_provider_name { margin: 12px; - color: $primary-fg-color; + color: $primary-content; font-weight: 400; opacity: 0.4; } diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index e1ba468204..752d3b0a54 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -31,7 +31,7 @@ limitations under the License. @keyframes visualbell { from { background-color: $visual-bell-bg-color; } - to { background-color: $primary-bg-color; } + to { background-color: $background; } } &.mx_BasicMessageComposer_input_error { @@ -65,6 +65,14 @@ limitations under the License. font-size: $font-10-4px; } } + + span.mx_UserPill { + cursor: pointer; + } + + span.mx_RoomPill { + cursor: default; + } } &.mx_BasicMessageComposer_input_disabled { diff --git a/res/css/views/rooms/_EditMessageComposer.scss b/res/css/views/rooms/_EditMessageComposer.scss index 214bfc4a1a..bf3c7c9b42 100644 --- a/res/css/views/rooms/_EditMessageComposer.scss +++ b/res/css/views/rooms/_EditMessageComposer.scss @@ -28,7 +28,7 @@ limitations under the License. .mx_BasicMessageComposer_input { border-radius: 4px; border: solid 1px $primary-hairline-color; - background-color: $primary-bg-color; + background-color: $background; max-height: 200px; padding: 3px 6px; diff --git a/res/css/views/rooms/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss index 27a4e67089..a2ebd6c11b 100644 --- a/res/css/views/rooms/_EntityTile.scss +++ b/res/css/views/rooms/_EntityTile.scss @@ -18,7 +18,7 @@ limitations under the License. .mx_EntityTile { display: flex; align-items: center; - color: $primary-fg-color; + color: $primary-content; cursor: pointer; .mx_E2EIcon { @@ -86,12 +86,12 @@ limitations under the License. .mx_EntityTile_ellipsis .mx_EntityTile_name { font-style: italic; - color: $primary-fg-color; + color: $primary-content; } .mx_EntityTile_invitePlaceholder .mx_EntityTile_name { font-style: italic; - color: $primary-fg-color; + color: $primary-content; } .mx_EntityTile_unavailable .mx_EntityTile_avatar, diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index cac463d4db..41c9dad394 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_EventTile[data-layout=bubble], -.mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary { +.mx_EventListSummary[data-layout=bubble] { --avatarSize: 32px; --gutterSize: 11px; --cornerRadius: 12px; @@ -33,22 +33,35 @@ limitations under the License. margin-top: 2px; } + &.mx_EventTile_highlight { + &::before { + background-color: $event-highlight-bg-color; + } + + color: $event-highlight-fg-color; + } + /* For replies */ .mx_EventTile { padding-top: 0; } - &:hover { + &::before { + content: ''; + position: absolute; + top: -1px; + bottom: -1px; + left: -60px; + right: -60px; + z-index: -1; + border-radius: 4px; + } + + &:hover, + &.mx_EventTile_selected { + &::before { - content: ''; - position: absolute; - top: -1px; - bottom: -1px; - left: -60px; - right: -60px; - z-index: -1; background: $eventbubble-bg-hover; - border-radius: 4px; } .mx_EventTile_avatar { @@ -100,6 +113,8 @@ limitations under the License. .mx_ReplyTile .mx_SenderProfile { display: block; + top: unset; + left: unset; } .mx_ReactionsRow { @@ -155,12 +170,24 @@ limitations under the License. position: absolute; top: 0; line-height: 1; + z-index: 9; img { box-shadow: 0 0 0 3px $eventbubble-avatar-outline; border-radius: 50%; } } + &.mx_EventTile_noSender { + .mx_EventTile_avatar { + top: -19px; + } + } + + .mx_BaseAvatar, + .mx_EventTile_avatar { + line-height: 1; + } + &[data-has-reply=true] { > .mx_EventTile_line { flex-direction: column; @@ -171,8 +198,6 @@ limitations under the License. } .mx_ReplyThread { - margin: 0 calc(-1 * var(--gutterSize)); - .mx_EventTile_reply { max-width: 90%; padding: 0; @@ -206,94 +231,6 @@ limitations under the License. margin-left: -9px; } - .mx_ReplyThread { - border-left-width: 2px; - border-left-color: $eventbubble-reply-color; - } - - &.mx_EventTile_bubbleContainer, - &.mx_EventTile_info, - & ~ .mx_EventListSummary[data-expanded=false] { - --backgroundColor: transparent; - --gutterSize: 0; - - display: flex; - align-items: center; - justify-content: center; - - .mx_EventTile_avatar { - position: static; - order: -1; - margin-right: 5px; - } - } - - & ~ .mx_EventListSummary { - --maxWidth: 80%; - margin-left: calc(var(--avatarSize) + var(--gutterSize)); - margin-right: calc(var(--gutterSize) + var(--avatarSize)); - .mx_EventListSummary_toggle { - float: none; - margin: 0; - order: 9; - margin-left: 5px; - } - .mx_EventListSummary_avatars { - padding-top: 0; - } - - &::after { - content: ""; - clear: both; - } - - .mx_EventTile { - margin: 0 6px; - } - - .mx_EventTile_line { - margin: 0 5px; - > a { - left: auto; - right: 0; - transform: translateX(calc(100% + 5px)); - } - } - - .mx_MessageActionBar { - transform: translate3d(90%, 0, 0); - } - } - - & ~ .mx_EventListSummary[data-expanded=false] { - padding: 0 34px; - } - - /* events that do not require bubble layout */ - & ~ .mx_EventListSummary, - &.mx_EventTile_bad { - .mx_EventTile_line { - background: transparent; - } - - &:hover { - &::before { - background: transparent; - } - } - } - - & + .mx_EventListSummary { - .mx_EventTile { - margin-top: 0; - padding: 0; - } - } - - .mx_EventListSummary_toggle { - margin-right: 55px; - } - /* Special layout scenario for "Unable To Decrypt (UTD)" events */ &.mx_EventTile_bad > .mx_EventTile_line { display: grid; @@ -328,3 +265,94 @@ limitations under the License. max-width: 100%; } } + +.mx_EventTile.mx_EventTile_bubbleContainer[data-layout=bubble], +.mx_EventTile.mx_EventTile_leftAlignedBubble[data-layout=bubble], +.mx_EventTile.mx_EventTile_info[data-layout=bubble], +.mx_EventListSummary[data-layout=bubble][data-expanded=false] { + --backgroundColor: transparent; + --gutterSize: 0; + + display: flex; + align-items: center; + justify-content: flex-start; + padding: 5px 0; + + .mx_EventTile_avatar { + position: static; + order: -1; + margin-right: 5px; + } + + .mx_EventTile_line, + .mx_EventTile_info { + min-width: 100%; + } + + .mx_EventTile_e2eIcon { + margin-left: 9px; + } + + .mx_EventTile_line > a { + right: auto; + top: -15px; + left: -68px; + } +} + +.mx_EventListSummary[data-layout=bubble] { + --maxWidth: 70%; + margin-left: calc(var(--avatarSize) + var(--gutterSize)); + margin-right: 94px; + .mx_EventListSummary_toggle { + float: none; + margin: 0; + order: 9; + margin-left: 5px; + margin-right: 55px; + } + .mx_EventListSummary_avatars { + padding-top: 0; + } + + &::after { + content: ""; + clear: both; + } + + .mx_EventTile { + margin: 0 6px; + padding: 2px 0; + } + + .mx_EventTile_line { + margin: 0 5px; + > a { + left: auto; + right: 0; + transform: translateX(calc(100% + 5px)); + } + } + + .mx_MessageActionBar { + transform: translate3d(90%, 0, 0); + } +} + +.mx_EventListSummary[data-expanded=false][data-layout=bubble] { + padding: 0 34px; +} + +/* events that do not require bubble layout */ +.mx_EventListSummary[data-layout=bubble], +.mx_EventTile.mx_EventTile_bad[data-layout=bubble] { + .mx_EventTile_line { + background: transparent; + } + + &:hover { + &::before { + background: transparent; + } + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index bda4a15f45..4495ec4f29 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -55,11 +55,10 @@ $hover-select-border: 4px; } .mx_SenderProfile { - color: $primary-fg-color; + color: $primary-content; font-size: $font-14px; display: inline-block; /* anti-zalgo, with overflow hidden */ overflow: hidden; - cursor: pointer; padding-bottom: 0px; padding-top: 0px; margin: 0px; @@ -132,15 +131,6 @@ $hover-select-border: 4px; } } - &.mx_EventTile_info .mx_EventTile_line, - & ~ .mx_EventListSummary .mx_EventTile_avatar ~ .mx_EventTile_line { - padding-left: calc($left-gutter + 18px); - } - - & ~ .mx_EventListSummary .mx_EventTile_line { - padding-left: calc($left-gutter); - } - &.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { padding-left: calc($left-gutter + 18px - $hover-select-border); } @@ -171,7 +161,7 @@ $hover-select-border: 4px; // up with the other read receipts &::before { - background-color: $tertiary-fg-color; + background-color: $tertiary-content; mask-repeat: no-repeat; mask-position: center; mask-size: 14px; @@ -276,10 +266,19 @@ $hover-select-border: 4px; .mx_ReactionsRow { margin: 0; - padding: 6px 60px; + padding: 4px 64px; } } +.mx_EventTile:not([data-layout=bubble]).mx_EventTile_info .mx_EventTile_line, +.mx_EventListSummary:not([data-layout=bubble]) > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); +} + +.mx_EventListSummary:not([data-layout=bubble]) .mx_EventTile_line { + padding-left: calc($left-gutter); +} + /* all the overflow-y: hidden; are to trap Zalgos - but they introduce an implicit overflow-x: auto. so make that explicitly hidden too to avoid random @@ -311,17 +310,19 @@ $hover-select-border: 4px; } .mx_RoomView_timeline_rr_enabled { - - .mx_EventTile:not([data-layout=bubble]) { + .mx_EventTile[data-layout=group] { .mx_EventTile_line { /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ margin-right: 110px; } } - // on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter } +.mx_SenderProfile { + cursor: pointer; +} + .mx_EventTile_bubbleContainer { display: grid; grid-template-columns: 1fr 100px; @@ -456,8 +457,14 @@ $hover-select-border: 4px; /* Various markdown overrides */ -.mx_EventTile_body pre { - border: 1px solid transparent; +.mx_EventTile_body { + a:hover { + text-decoration: underline; + } + + pre { + border: 1px solid transparent; + } } .mx_EventTile_content .markdown-body { @@ -473,7 +480,7 @@ $hover-select-border: 4px; } pre code > * { - display: inline-block; + display: inline; } pre { @@ -482,6 +489,10 @@ $hover-select-border: 4px; // https://github.com/vector-im/vector-web/issues/754 overflow-x: overlay; overflow-y: visible; + + &::-webkit-scrollbar-corner { + background: transparent; + } } } @@ -503,7 +514,7 @@ $hover-select-border: 4px; .mx_EventTile:hover .mx_EventTile_body pre, .mx_EventTile.focus-visible:focus-within .mx_EventTile_body pre { - border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter + border: 1px solid $tertiary-content; } .mx_EventTile_pre_container { @@ -573,6 +584,12 @@ $hover-select-border: 4px; color: $accent-color-alt; } +.mx_EventTile_content .markdown-body blockquote { + border-left: 2px solid $blockquote-bar-color; + border-radius: 2px; + padding: 0 10px; +} + .mx_EventTile_content .markdown-body .hljs { display: inline !important; } @@ -601,7 +618,7 @@ $hover-select-border: 4px; } .mx_EventTile_keyRequestInfo_text a { - color: $primary-fg-color; + color: $primary-content; text-decoration: underline; cursor: pointer; } @@ -626,6 +643,7 @@ $hover-select-border: 4px; // Remove some of the default tile padding so that the error is centered margin-right: 0; + .mx_EventTile_line { padding-left: 0; margin-right: 0; @@ -657,3 +675,66 @@ $hover-select-border: 4px; margin-right: 0; } } + +.mx_ThreadInfo:hover { + cursor: pointer; +} + +.mx_ThreadView { + display: flex; + flex-direction: column; + + .mx_ScrollPanel { + margin-top: 20px; + + .mx_RoomView_MessageList { + padding: 0; + } + } + + .mx_EventTile_senderDetails { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 6px; + + a { + flex: 1; + min-width: none; + max-width: 100%; + display: flex; + align-items: center; + + .mx_SenderProfile { + flex: 1; + } + } + } + + .mx_ThreadView_List { + flex: 1; + overflow: scroll; + } + + .mx_EventTile_roomName { + display: none; + } + + .mx_EventTile_line { + padding-left: 0 !important; + order: 10 !important; + } + + .mx_EventTile { + width: 100%; + display: flex; + flex-direction: column; + margin-top: 0; + padding-bottom: 5px; + margin-bottom: 5px; + } + + .mx_MessageComposer_sendMessage { + margin-right: 0; + } +} diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 97190807ca..578c0325d2 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -116,6 +116,11 @@ $irc-line-height: $font-18px; .mx_EditMessageComposer_buttons { position: relative; } + + .mx_ReactionsRow { + padding-left: 0; + padding-right: 0; + } } .mx_EventTile_emote { diff --git a/res/css/views/rooms/_JumpToBottomButton.scss b/res/css/views/rooms/_JumpToBottomButton.scss index a8dc2ce11c..2b38b509de 100644 --- a/res/css/views/rooms/_JumpToBottomButton.scss +++ b/res/css/views/rooms/_JumpToBottomButton.scss @@ -56,7 +56,7 @@ limitations under the License. height: 38px; border-radius: 19px; box-sizing: border-box; - background: $primary-bg-color; + background: $background; border: 1.3px solid $muted-fg-color; cursor: pointer; } diff --git a/res/css/views/rooms/_LinkPreviewWidget.scss b/res/css/views/rooms/_LinkPreviewWidget.scss index b3eff7e960..24900ee14b 100644 --- a/res/css/views/rooms/_LinkPreviewWidget.scss +++ b/res/css/views/rooms/_LinkPreviewWidget.scss @@ -34,7 +34,7 @@ limitations under the License. .mx_LinkPreviewWidget_caption { margin-left: 15px; flex: 1 1 auto; - overflow-x: hidden; // cause it to wrap rather than clip + overflow: hidden; // cause it to wrap rather than clip } .mx_LinkPreviewWidget_title { diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss index 3f7f83d334..4abd9c7c30 100644 --- a/res/css/views/rooms/_MemberInfo.scss +++ b/res/css/views/rooms/_MemberInfo.scss @@ -111,7 +111,7 @@ limitations under the License. .mx_MemberInfo_field { cursor: pointer; font-size: $font-15px; - color: $primary-fg-color; + color: $primary-content; margin-left: 8px; line-height: $font-23px; } diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index e6c0cc3f46..9ba966c083 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -130,7 +130,7 @@ limitations under the License. @keyframes visualbell { from { background-color: $visual-bell-bg-color; } - to { background-color: $primary-bg-color; } + to { background-color: $background; } } .mx_MessageComposer_input_error { @@ -160,13 +160,11 @@ limitations under the License. resize: none; outline: none; box-shadow: none; - color: $primary-fg-color; - background-color: $primary-bg-color; + color: $primary-content; + background-color: $background; font-size: $font-14px; max-height: 120px; overflow: auto; - /* needed for FF */ - font-family: $font-family; } /* hack for FF as vertical alignment of custom placeholder text is broken */ @@ -188,11 +186,14 @@ limitations under the License. } .mx_MessageComposer_button { + --size: 26px; position: relative; margin-right: 6px; cursor: pointer; - height: 26px; - width: 26px; + height: var(--size); + line-height: var(--size); + width: auto; + padding-left: calc(var(--size) + 5px); border-radius: 100%; &::before { @@ -209,8 +210,22 @@ limitations under the License. mask-position: center; } - &:hover { - background: rgba($accent-color, 0.1); + &::after { + content: ''; + position: absolute; + left: 0; + top: 0; + z-index: 0; + width: var(--size); + height: var(--size); + border-radius: 50%; + } + + &:hover, + &.mx_MessageComposer_closeButtonMenu { + &::after { + background: rgba($accent-color, 0.1); + } &::before { background-color: $accent-color; @@ -239,10 +254,18 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg'); } +.mx_MessageComposer_buttonMenu::before { + mask-image: url('$(res)/img/image-view/more.svg'); +} + +.mx_MessageComposer_closeButtonMenu::before { + transform: rotate(90deg); + transform-origin: center; +} + .mx_MessageComposer_sendMessage { cursor: pointer; position: relative; - margin-right: 6px; width: 32px; height: 32px; border-radius: 100%; @@ -342,3 +365,28 @@ limitations under the License. height: 50px; } } + +/** + * Unstable compact mode + */ + +.mx_MessageComposer.mx_MessageComposer--compact { + margin-right: 0; + + .mx_MessageComposer_wrapper { + padding: 0 0 0 25px; + } + + .mx_MessageComposer_button:last-child { + margin-right: 0; + } + + .mx_MessageComposer_e2eIcon { + left: 0; + } +} + +.mx_MessageComposer_Menu .mx_CallContextMenu_item { + display: flex; + align-items: center; +} diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss index e0cccfa885..f0e471d384 100644 --- a/res/css/views/rooms/_NewRoomIntro.scss +++ b/res/css/views/rooms/_NewRoomIntro.scss @@ -67,6 +67,6 @@ limitations under the License. > p { margin: 0; font-size: $font-15px; - color: $secondary-fg-color; + color: $secondary-content; } } diff --git a/res/css/views/rooms/_NotificationBadge.scss b/res/css/views/rooms/_NotificationBadge.scss index 64b2623238..670e057cfa 100644 --- a/res/css/views/rooms/_NotificationBadge.scss +++ b/res/css/views/rooms/_NotificationBadge.scss @@ -42,7 +42,7 @@ limitations under the License. // These are the 3 background types &.mx_NotificationBadge_dot { - background-color: $primary-fg-color; // increased visibility + background-color: $primary-content; // increased visibility width: 6px; height: 6px; diff --git a/res/css/views/rooms/_PinnedEventTile.scss b/res/css/views/rooms/_PinnedEventTile.scss index 15b3c16faa..07978a8f65 100644 --- a/res/css/views/rooms/_PinnedEventTile.scss +++ b/res/css/views/rooms/_PinnedEventTile.scss @@ -67,7 +67,7 @@ limitations under the License. //left: 0; height: inherit; width: inherit; - background: $secondary-fg-color; + background: $secondary-content; mask-position: center; mask-size: 8px; mask-repeat: no-repeat; @@ -87,7 +87,7 @@ limitations under the License. .mx_PinnedEventTile_timestamp { font-size: inherit; line-height: inherit; - color: $secondary-fg-color; + color: $secondary-content; } .mx_AccessibleButton_kind_link { diff --git a/res/css/views/rooms/_ReplyPreview.scss b/res/css/views/rooms/_ReplyPreview.scss index 60feb39d11..70a820e412 100644 --- a/res/css/views/rooms/_ReplyPreview.scss +++ b/res/css/views/rooms/_ReplyPreview.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_ReplyPreview { border: 1px solid $primary-hairline-color; - background: $primary-bg-color; + background: $background; border-bottom: none; border-radius: 8px 8px 0 0; max-height: 50vh; @@ -28,7 +28,7 @@ limitations under the License. .mx_ReplyPreview_header { margin: 8px; - color: $primary-fg-color; + color: $primary-content; font-weight: 400; opacity: 0.4; } diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index f3e204e415..3ef6491ec9 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -42,7 +42,7 @@ limitations under the License. display: flex; flex-direction: column; text-decoration: none; - color: $primary-fg-color; + color: $primary-content; } .mx_RedactedBody { @@ -60,8 +60,6 @@ limitations under the License. $reply-lines: 2; $line-height: $font-22px; - pointer-events: none; - text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 4142b0a2ef..81dfa90c96 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_RoomHeader { flex: 0 0 50px; border-bottom: 1px solid $primary-hairline-color; - background-color: $roomheader-bg-color; + background-color: $background; .mx_RoomHeader_e2eIcon { height: 12px; @@ -74,7 +74,7 @@ limitations under the License. .mx_RoomHeader_buttons { display: flex; - background-color: $primary-bg-color; + background-color: $background; } .mx_RoomHeader_info { diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index 8eda25d0c9..7d967661a6 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -43,11 +43,11 @@ limitations under the License. div:first-child { font-weight: $font-semi-bold; line-height: $font-18px; - color: $primary-fg-color; + color: $primary-content; } .mx_AccessibleButton { - color: $primary-fg-color; + color: $primary-content; position: relative; padding: 8px 8px 8px 32px; font-size: inherit; @@ -64,7 +64,7 @@ limitations under the License. position: absolute; top: 8px; left: 8px; - background: $secondary-fg-color; + background: $secondary-content; mask-position: center; mask-size: contain; mask-repeat: no-repeat; diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss index 146b3edf71..3fffbfd64c 100644 --- a/res/css/views/rooms/_RoomSublist.scss +++ b/res/css/views/rooms/_RoomSublist.scss @@ -233,7 +233,7 @@ limitations under the License. &:hover, &.mx_RoomSublist_hasMenuOpen { .mx_RoomSublist_resizerHandle { opacity: 0.8; - background-color: $primary-fg-color; + background-color: $primary-content; } } } diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index b8f4aeb6e7..0c04f27115 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -124,7 +124,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $primary-fg-color; + background: $primary-content; } } diff --git a/res/css/views/rooms/_SearchBar.scss b/res/css/views/rooms/_SearchBar.scss index d9f730a8b6..e08168a122 100644 --- a/res/css/views/rooms/_SearchBar.scss +++ b/res/css/views/rooms/_SearchBar.scss @@ -47,7 +47,7 @@ limitations under the License. padding: 5px; font-size: $font-15px; cursor: pointer; - color: $primary-fg-color; + color: $primary-content; border-bottom: 2px solid $accent-color; font-weight: 600; } diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.scss b/res/css/views/rooms/_TopUnreadMessagesBar.scss index 8841b042a0..7c7d96e713 100644 --- a/res/css/views/rooms/_TopUnreadMessagesBar.scss +++ b/res/css/views/rooms/_TopUnreadMessagesBar.scss @@ -41,7 +41,7 @@ limitations under the License. height: 38px; border-radius: 19px; box-sizing: border-box; - background: $primary-bg-color; + background: $background; border: 1.3px solid $muted-fg-color; cursor: pointer; } @@ -62,7 +62,7 @@ limitations under the License. display: block; width: 18px; height: 18px; - background: $primary-bg-color; + background: $background; border: 1.3px solid $muted-fg-color; border-radius: 10px; margin: 5px auto; diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index 5501ab343e..69fe292c0a 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -20,7 +20,7 @@ limitations under the License. height: 28px; border: 2px solid $voice-record-stop-border-color; border-radius: 32px; - margin-right: 16px; // between us and the send button + margin-right: 8px; // between us and the waveform component position: relative; &::after { @@ -46,9 +46,28 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/trashcan.svg'); } +.mx_VoiceRecordComposerTile_uploadingState { + margin-right: 10px; + color: $secondary-content; +} + +.mx_VoiceRecordComposerTile_failedState { + margin-right: 21px; + + .mx_VoiceRecordComposerTile_uploadState_badge { + display: inline-block; + margin-right: 4px; + vertical-align: middle; + } +} + .mx_MessageComposer_row .mx_VoiceMessagePrimaryContainer { // Note: remaining class properties are in the PlayerContainer CSS. + // fixed height to reduce layout jumps with the play button appearing + // https://github.com/vector-im/element-web/issues/18431 + height: 32px; + margin: 6px; // force the composer area to put a gutter around us margin-right: 12px; // isolate from stop/send button @@ -68,7 +87,7 @@ limitations under the License. height: 10px; position: absolute; left: 12px; // 12px from the left edge for container padding - top: 18px; // vertically center (middle align with clock) + top: 17px; // vertically center (middle align with clock) border-radius: 10px; } } diff --git a/res/css/views/rooms/_WhoIsTypingTile.scss b/res/css/views/rooms/_WhoIsTypingTile.scss index 1c0dabbeb5..49655742bb 100644 --- a/res/css/views/rooms/_WhoIsTypingTile.scss +++ b/res/css/views/rooms/_WhoIsTypingTile.scss @@ -36,7 +36,7 @@ limitations under the License. } .mx_WhoIsTypingTile_avatars .mx_BaseAvatar { - border: 1px solid $primary-bg-color; + border: 1px solid $background; border-radius: 40px; } @@ -45,7 +45,7 @@ limitations under the License. display: inline-block; color: #acacac; background-color: #ddd; - border: 1px solid $primary-bg-color; + border: 1px solid $background; border-radius: 40px; width: 24px; height: 24px; diff --git a/res/css/views/settings/_LayoutSwitcher.scss b/res/css/views/settings/_LayoutSwitcher.scss new file mode 100644 index 0000000000..00fb8aba56 --- /dev/null +++ b/res/css/views/settings/_LayoutSwitcher.scss @@ -0,0 +1,91 @@ +/* +Copyright 2020 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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_LayoutSwitcher { + .mx_LayoutSwitcher_RadioButtons { + display: flex; + flex-direction: row; + gap: 24px; + + color: $primary-content; + + > .mx_LayoutSwitcher_RadioButton { + flex-grow: 0; + flex-shrink: 1; + display: flex; + flex-direction: column; + + width: 300px; + + border: 1px solid $appearance-tab-border-color; + border-radius: 10px; + + .mx_EventTile_msgOption, + .mx_MessageActionBar { + display: none; + } + + .mx_LayoutSwitcher_RadioButton_preview { + flex-grow: 1; + display: flex; + align-items: center; + padding: 10px; + pointer-events: none; + } + + .mx_RadioButton { + flex-grow: 0; + padding: 10px; + } + + .mx_EventTile_content { + margin-right: 0; + } + + &.mx_LayoutSwitcher_RadioButton_selected { + border-color: $accent-color; + } + } + + .mx_RadioButton { + border-top: 1px solid $appearance-tab-border-color; + + > input + div { + border-color: rgba($muted-fg-color, 0.2); + } + } + + .mx_RadioButton_checked { + background-color: rgba($accent-color, 0.08); + } + + .mx_EventTile { + margin: 0; + &[data-layout=bubble] { + margin-right: 40px; + } + &[data-layout=irc] { + > a { + display: none; + } + } + .mx_EventTile_line { + max-width: 90%; + } + } + } +} diff --git a/res/css/views/settings/_Notifications.scss b/res/css/views/settings/_Notifications.scss index f93e0a53a8..a0e46c0071 100644 --- a/res/css/views/settings/_Notifications.scss +++ b/res/css/views/settings/_Notifications.scss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_UserNotifSettings { - color: $primary-fg-color; // override from default settings page styles + color: $primary-content; // override from default settings page styles .mx_UserNotifSettings_pushRulesTable { width: calc(100% + 12px); // +12px to line up center of 'Noisy' column with toggle switches @@ -34,7 +34,7 @@ limitations under the License. } tr > th:nth-child(n + 2) { - color: $secondary-fg-color; + color: $secondary-content; font-size: $font-12px; vertical-align: middle; width: 66px; diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss index 4cbcb8e708..63a5fa7edf 100644 --- a/res/css/views/settings/_ProfileSettings.scss +++ b/res/css/views/settings/_ProfileSettings.scss @@ -16,6 +16,7 @@ limitations under the License. .mx_ProfileSettings_controls_topic { & > textarea { + font-family: inherit; resize: vertical; } } diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 892f5fe744..5aa9db7e86 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -25,7 +25,7 @@ limitations under the License. .mx_SettingsTab_heading { font-size: $font-20px; font-weight: 600; - color: $primary-fg-color; + color: $primary-content; margin-bottom: 10px; } @@ -36,9 +36,8 @@ limitations under the License. .mx_SettingsTab_subheading { font-size: $font-16px; display: block; - font-family: $font-family; font-weight: 600; - color: $primary-fg-color; + color: $primary-content; margin-bottom: 10px; margin-top: 12px; } @@ -47,19 +46,25 @@ limitations under the License. color: $settings-subsection-fg-color; font-size: $font-14px; display: block; - margin: 10px 100px 10px 0; // Align with the rest of the view + margin: 10px 80px 10px 0; // Align with the rest of the view } .mx_SettingsTab_section { + $right-gutter: 80px; + margin-bottom: 24px; .mx_SettingsFlag { - margin-right: 100px; + margin-right: $right-gutter; margin-bottom: 10px; } + > p { + margin-right: $right-gutter; + } + &.mx_SettingsTab_subsectionText .mx_SettingsFlag { - margin-right: 0px !important; + margin-right: 0 !important; } } @@ -67,12 +72,19 @@ limitations under the License. vertical-align: middle; display: inline-block; font-size: $font-14px; - color: $primary-fg-color; + color: $primary-content; max-width: calc(100% - $font-48px); // Force word wrap instead of colliding with the switch box-sizing: border-box; padding-right: 10px; } +.mx_SettingsTab_section .mx_SettingsFlag .mx_SettingsFlag_microcopy { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-content; +} + .mx_SettingsTab_section .mx_SettingsFlag .mx_ToggleSwitch { float: right; } diff --git a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss index 23dcc532b2..8fd0f14418 100644 --- a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss +++ b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss @@ -14,6 +14,44 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_SecurityRoomSettingsTab { + .mx_SettingsTab_showAdvanced { + padding: 0; + margin-bottom: 16px; + } + + .mx_SecurityRoomSettingsTab_spacesWithAccess { + > h4 { + color: $secondary-content; + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + text-transform: uppercase; + } + + > span { + font-weight: 500; + font-size: $font-14px; + line-height: 32px; // matches height of avatar for v-align + color: $secondary-content; + display: inline-block; + + img.mx_RoomAvatar_isSpaceRoom, + .mx_RoomAvatar_isSpaceRoom img { + border-radius: 8px; + } + + .mx_BaseAvatar { + margin-right: 8px; + } + + & + span { + margin-left: 16px; + } + } + } +} + .mx_SecurityRoomSettingsTab_warning { display: block; @@ -26,5 +64,51 @@ limitations under the License. } .mx_SecurityRoomSettingsTab_encryptionSection { - margin-bottom: 25px; + padding-bottom: 24px; + border-bottom: 1px solid $menu-border-color; + margin-bottom: 32px; +} + +.mx_SecurityRoomSettingsTab_upgradeRequired { + margin-left: 16px; + padding: 4px 16px; + border: 1px solid $accent-color; + border-radius: 8px; + color: $accent-color; + font-size: $font-12px; + line-height: $font-15px; +} + +.mx_SecurityRoomSettingsTab_joinRule { + .mx_RadioButton { + padding-top: 16px; + margin-bottom: 8px; + + .mx_RadioButton_content { + margin-left: 14px; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-content; + display: block; + } + } + + > span { + display: inline-block; + margin-left: 34px; + margin-bottom: 16px; + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-content; + + & + .mx_RadioButton { + border-top: 1px solid $menu-border-color; + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + font-size: inherit; + } } diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index ca5a6f0a66..57c6e9b865 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -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. @@ -24,7 +24,7 @@ limitations under the License. } .mx_AppearanceUserSettingsTab_fontScaling { - color: $primary-fg-color; + color: $primary-content; } .mx_AppearanceUserSettingsTab_fontSlider { @@ -81,7 +81,7 @@ limitations under the License. .mx_AppearanceUserSettingsTab_themeSection { $radio-bg-color: $input-darker-bg-color; - color: $primary-fg-color; + color: $primary-content; > .mx_ThemeSelectors { display: flex; @@ -155,81 +155,8 @@ limitations under the License. margin-left: calc($font-16px + 10px); } -.mx_AppearanceUserSettingsTab_Layout_RadioButtons { - display: flex; - flex-direction: row; - gap: 24px; - - color: $primary-fg-color; - - > .mx_AppearanceUserSettingsTab_Layout_RadioButton { - flex-grow: 0; - flex-shrink: 1; - display: flex; - flex-direction: column; - - width: 300px; - - border: 1px solid $appearance-tab-border-color; - border-radius: 10px; - - .mx_EventTile_msgOption, - .mx_MessageActionBar { - display: none; - } - - .mx_AppearanceUserSettingsTab_Layout_RadioButton_preview { - flex-grow: 1; - display: flex; - align-items: center; - padding: 10px; - pointer-events: none; - } - - .mx_RadioButton { - flex-grow: 0; - padding: 10px; - } - - .mx_EventTile_content { - margin-right: 0; - } - - &.mx_AppearanceUserSettingsTab_Layout_RadioButton_selected { - border-color: $accent-color; - } - } - - .mx_RadioButton { - border-top: 1px solid $appearance-tab-border-color; - - > input + div { - border-color: rgba($muted-fg-color, 0.2); - } - } - - .mx_RadioButton_checked { - background-color: rgba($accent-color, 0.08); - } - - .mx_EventTile { - margin: 0; - &[data-layout=bubble] { - margin-right: 40px; - } - &[data-layout=irc] { - > a { - display: none; - } - } - .mx_EventTile_line { - max-width: 90%; - } - } -} - .mx_AppearanceUserSettingsTab_Advanced { - color: $primary-fg-color; + color: $primary-content; > * { margin-bottom: 16px; diff --git a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss index 0f879d209e..3e61e80a9d 100644 --- a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss @@ -28,28 +28,33 @@ limitations under the License. user-select: all; } -.mx_HelpUserSettingsTab_accessToken { +.mx_HelpUserSettingsTab_copy { display: flex; - justify-content: space-between; border-radius: 5px; border: solid 1px $light-fg-color; margin-bottom: 10px; margin-top: 10px; padding: 10px; -} + width: max-content; + max-width: 100%; -.mx_HelpUserSettingsTab_accessToken_copy { - flex-shrink: 0; - cursor: pointer; - margin-left: 20px; - display: inherit; -} + .mx_HelpUserSettingsTab_copyButton { + flex-shrink: 0; + width: 20px; + height: 20px; + cursor: pointer; + margin-left: 20px; + display: block; -.mx_HelpUserSettingsTab_accessToken_copy > div { - mask-image: url($copy-button-url); - background-color: $message-action-bar-fg-color; - margin-left: 5px; - width: 20px; - height: 20px; - background-repeat: no-repeat; + &::before { + content: ""; + + mask-image: url($copy-button-url); + background-color: $message-action-bar-fg-color; + width: 20px; + height: 20px; + display: block; + background-repeat: no-repeat; + } + } } diff --git a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss index be0af9123b..d1076205ad 100644 --- a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss @@ -22,4 +22,25 @@ limitations under the License. .mx_SettingsTab_section { margin-bottom: 30px; } + + .mx_PreferencesUserSettingsTab_CommunityMigrator { + margin-right: 200px; + + > div { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $primary-content; + margin: 16px 0; + + .mx_BaseAvatar { + margin-right: 12px; + vertical-align: middle; + } + + .mx_AccessibleButton { + float: right; + } + } + } } diff --git a/res/css/views/spaces/_SpaceBasicSettings.scss b/res/css/views/spaces/_SpaceBasicSettings.scss index c73e0715dd..bff574ded3 100644 --- a/res/css/views/spaces/_SpaceBasicSettings.scss +++ b/res/css/views/spaces/_SpaceBasicSettings.scss @@ -27,7 +27,7 @@ limitations under the License. position: relative; height: 80px; width: 80px; - background-color: $tertiary-fg-color; + background-color: $tertiary-content; border-radius: 16px; } diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index 88b9d8f693..3f526a6bba 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -28,7 +28,7 @@ $spacePanelWidth: 71px; padding: 24px; width: 480px; box-sizing: border-box; - background-color: $primary-bg-color; + background-color: $background; position: relative; > div { @@ -40,16 +40,14 @@ $spacePanelWidth: 71px; > p { font-size: $font-15px; - color: $secondary-fg-color; - margin: 0; + color: $secondary-content; } - } - // XXX remove this when spaces leaves Beta - .mx_BetaCard_betaPill { - position: absolute; - top: 24px; - right: 24px; + .mx_SpaceFeedbackPrompt { + border-top: 1px solid $input-border-color; + padding-top: 12px; + margin-top: 16px; + } } .mx_SpaceCreateMenuType { @@ -78,7 +76,7 @@ $spacePanelWidth: 71px; width: 28px; top: 0; left: 0; - background-color: $tertiary-fg-color; + background-color: $tertiary-content; transform: rotate(90deg); mask-repeat: no-repeat; mask-position: 2px 3px; @@ -94,8 +92,35 @@ $spacePanelWidth: 71px; width: min-content; } + .mx_AccessibleButton_kind_link { + padding: 0; + font-size: inherit; + } + .mx_AccessibleButton_disabled { cursor: not-allowed; } } } + +.mx_SpaceFeedbackPrompt { + font-size: $font-15px; + line-height: $font-24px; + + > span { + color: $secondary-content; + position: relative; + font-size: inherit; + line-height: inherit; + margin-right: auto; + } + + .mx_AccessibleButton_kind_link { + color: $accent-color; + position: relative; + padding: 0; + margin-left: 8px; + font-size: inherit; + line-height: inherit; + } +} diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss new file mode 100644 index 0000000000..cb05b1a977 --- /dev/null +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -0,0 +1,156 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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_IncomingCallToast { + display: flex; + flex-direction: row; + pointer-events: initial; // restore pointer events so the user can accept/decline + + .mx_IncomingCallToast_content { + display: flex; + flex-direction: column; + margin-left: 8px; + + .mx_CallEvent_caller { + font-weight: bold; + font-size: $font-15px; + line-height: $font-18px; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + margin-top: 2px; + margin-right: 6px; + + max-width: 200px; + } + + .mx_CallEvent_type { + font-size: $font-12px; + line-height: $font-15px; + color: $tertiary-content; + + margin-top: 4px; + margin-bottom: 6px; + + display: flex; + flex-direction: row; + align-items: center; + + .mx_CallEvent_type_icon { + height: 16px; + width: 16px; + margin-right: 6px; + + &::before { + content: ''; + position: absolute; + height: inherit; + width: inherit; + background-color: $tertiary-content; + mask-repeat: no-repeat; + mask-size: contain; + } + } + } + + &.mx_IncomingCallToast_content_voice { + .mx_CallEvent_type .mx_CallEvent_type_icon::before, + .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } + } + + &.mx_IncomingCallToast_content_video { + .mx_CallEvent_type .mx_CallEvent_type_icon::before, + .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } + } + + .mx_IncomingCallToast_buttons { + margin-top: 8px; + display: flex; + flex-direction: row; + gap: 12px; + + .mx_IncomingCallToast_button { + height: 24px; + padding: 0px 8px; + flex-shrink: 0; + flex-grow: 1; + margin-right: 0; + font-size: $font-15px; + line-height: $font-24px; + + span { + padding: 8px 0; + display: flex; + align-items: center; + + &::before { + content: ''; + display: inline-block; + background-color: $button-fg-color; + mask-position: center; + mask-repeat: no-repeat; + margin-right: 8px; + } + } + + &.mx_IncomingCallToast_button_accept span::before { + mask-size: 13px; + width: 13px; + height: 13px; + } + + &.mx_IncomingCallToast_button_decline span::before { + mask-image: url('$(res)/img/element-icons/call/hangup.svg'); + mask-size: 16px; + width: 16px; + height: 16px; + } + } + } + } + + .mx_IncomingCallToast_iconButton { + display: flex; + height: 20px; + width: 20px; + + &::before { + content: ''; + + height: inherit; + width: inherit; + background-color: $tertiary-content; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + } + + .mx_IncomingCallToast_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } + + .mx_IncomingCallToast_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } +} diff --git a/res/css/views/voip/CallView/_CallViewButtons.scss b/res/css/views/voip/CallView/_CallViewButtons.scss new file mode 100644 index 0000000000..8e343f0ff3 --- /dev/null +++ b/res/css/views/voip/CallView/_CallViewButtons.scss @@ -0,0 +1,102 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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_CallViewButtons { + position: absolute; + display: flex; + justify-content: center; + bottom: 5px; + opacity: 1; + transition: opacity 0.5s; + z-index: 200; // To be above _all_ feeds + + &.mx_CallViewButtons_hidden { + opacity: 0.001; // opacity 0 can cause a re-layout + pointer-events: none; + } + + .mx_CallViewButtons_button { + cursor: pointer; + margin-left: 2px; + margin-right: 2px; + + + &::before { + content: ''; + display: inline-block; + + height: 48px; + width: 48px; + + background-repeat: no-repeat; + background-size: contain; + background-position: center; + } + + + &.mx_CallViewButtons_dialpad::before { + background-image: url('$(res)/img/voip/dialpad.svg'); + } + + &.mx_CallViewButtons_button_micOn::before { + background-image: url('$(res)/img/voip/mic-on.svg'); + } + + &.mx_CallViewButtons_button_micOff::before { + background-image: url('$(res)/img/voip/mic-off.svg'); + } + + &.mx_CallViewButtons_button_vidOn::before { + background-image: url('$(res)/img/voip/vid-on.svg'); + } + + &.mx_CallViewButtons_button_vidOff::before { + background-image: url('$(res)/img/voip/vid-off.svg'); + } + + &.mx_CallViewButtons_button_screensharingOn::before { + background-image: url('$(res)/img/voip/screensharing-on.svg'); + } + + &.mx_CallViewButtons_button_screensharingOff::before { + background-image: url('$(res)/img/voip/screensharing-off.svg'); + } + + &.mx_CallViewButtons_button_sidebarOn::before { + background-image: url('$(res)/img/voip/sidebar-on.svg'); + } + + &.mx_CallViewButtons_button_sidebarOff::before { + background-image: url('$(res)/img/voip/sidebar-off.svg'); + } + + &.mx_CallViewButtons_button_hangup::before { + background-image: url('$(res)/img/voip/hangup.svg'); + } + + &.mx_CallViewButtons_button_more::before { + background-image: url('$(res)/img/voip/more.svg'); + } + + &.mx_CallViewButtons_button_invisible { + visibility: hidden; + pointer-events: none; + position: absolute; + } + } +} diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss index 0c09070334..a0137b18e8 100644 --- a/res/css/views/voip/_CallContainer.scss +++ b/res/css/views/voip/_CallContainer.scss @@ -26,101 +26,7 @@ limitations under the License. // different level. pointer-events: none; - .mx_CallPreview { - pointer-events: initial; // restore pointer events so the user can leave/interact - cursor: pointer; - - .mx_VideoFeed_remote.mx_VideoFeed_voice { - min-height: 150px; - } - - .mx_VideoFeed_local { - border-radius: 8px; - overflow: hidden; - } - } - .mx_AppTile_persistedWrapper div { min-width: 350px; } - - .mx_IncomingCallBox { - min-width: 250px; - background-color: $voipcall-plinth-color; - padding: 8px; - box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); - border-radius: 8px; - - pointer-events: initial; // restore pointer events so the user can accept/decline - cursor: pointer; - - .mx_IncomingCallBox_CallerInfo { - display: flex; - direction: row; - - img, .mx_BaseAvatar_initial { - margin: 8px; - } - - > div { - display: flex; - flex-direction: column; - - justify-content: center; - } - - h1, p { - margin: 0px; - padding: 0px; - font-size: $font-14px; - line-height: $font-16px; - } - - h1 { - font-weight: bold; - } - } - - .mx_IncomingCallBox_buttons { - padding: 8px; - display: flex; - flex-direction: row; - - > .mx_IncomingCallBox_spacer { - width: 8px; - } - - > * { - flex-shrink: 0; - flex-grow: 1; - margin-right: 0; - font-size: $font-15px; - line-height: $font-24px; - } - } - - .mx_IncomingCallBox_iconButton { - position: absolute; - right: 8px; - - &::before { - content: ''; - - height: 20px; - width: 20px; - background-color: $icon-button-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - } - } - - .mx_IncomingCallBox_silence::before { - mask-image: url('$(res)/img/voip/silence.svg'); - } - - .mx_IncomingCallBox_unSilence::before { - mask-image: url('$(res)/img/voip/un-silence.svg'); - } - } } diff --git a/res/css/views/voip/_CallPreview.scss b/res/css/views/voip/_CallPreview.scss index 92348fb465..0fd97d4676 100644 --- a/res/css/views/voip/_CallPreview.scss +++ b/res/css/views/voip/_CallPreview.scss @@ -18,4 +18,15 @@ limitations under the License. position: fixed; left: 0; top: 0; + + pointer-events: initial; // restore pointer events so the user can leave/interact + + .mx_VideoFeed_remote.mx_VideoFeed_voice { + min-height: 150px; + } + + .mx_VideoFeed_local { + border-radius: 8px; + overflow: hidden; + } } diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 205d431752..aa0aa4e2a6 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -39,19 +39,20 @@ limitations under the License. .mx_CallView_pip { width: 320px; padding-bottom: 8px; - background-color: $voipcall-plinth-color; + background-color: $system; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); border-radius: 8px; + .mx_CallView_video_hold, .mx_CallView_voice { height: 180px; } - .mx_CallView_callControls { + .mx_CallViewButtons { bottom: 0px; } - .mx_CallView_callControls_button { + .mx_CallViewButtons_button { &::before { width: 36px; height: 36px; @@ -67,7 +68,30 @@ limitations under the License. .mx_CallView_content { position: relative; display: flex; + justify-content: center; border-radius: 8px; + + > .mx_VideoFeed { + width: 100%; + height: 100%; + border-width: 0 !important; // Override mx_VideoFeed_speaking + + &.mx_VideoFeed_voice { + display: flex; + justify-content: center; + align-items: center; + } + + .mx_VideoFeed_video { + height: 100%; + background-color: #000; + } + + .mx_VideoFeed_mic { + left: 10px; + bottom: 10px; + } + } } .mx_CallView_voice { @@ -176,202 +200,22 @@ limitations under the License. } } -.mx_CallView_header { - height: 44px; - display: flex; - flex-direction: row; - align-items: center; - justify-content: left; - flex-shrink: 0; -} -.mx_CallView_header_callType { - font-size: 1.2rem; - font-weight: bold; - vertical-align: middle; -} - -.mx_CallView_header_secondaryCallInfo { - &::before { - content: '·'; - margin-left: 6px; - margin-right: 6px; - } -} - -.mx_CallView_header_controls { - margin-left: auto; -} - -.mx_CallView_header_button { - display: inline-block; - vertical-align: middle; - cursor: pointer; - - &::before { - content: ''; - display: inline-block; - height: 20px; - width: 20px; - vertical-align: middle; - background-color: $secondary-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - } -} - -.mx_CallView_header_button_fullscreen { - &::before { - mask-image: url('$(res)/img/element-icons/call/fullscreen.svg'); - } -} - -.mx_CallView_header_button_expand { - &::before { - mask-image: url('$(res)/img/element-icons/call/expand.svg'); - } -} - -.mx_CallView_header_callInfo { - margin-left: 12px; - margin-right: 16px; -} - -.mx_CallView_header_roomName { - font-weight: bold; - font-size: 12px; - line-height: initial; - height: 15px; -} - -.mx_CallView_secondaryCall_roomName { - margin-left: 4px; -} - -.mx_CallView_header_callTypeSmall { - font-size: 12px; - color: $secondary-fg-color; - line-height: initial; - height: 15px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - max-width: 240px; -} - -.mx_CallView_header_phoneIcon { - display: inline-block; - margin-right: 6px; - height: 16px; - width: 16px; - vertical-align: middle; - - &::before { - content: ''; - display: inline-block; - vertical-align: top; - - height: 16px; - width: 16px; - background-color: $warning-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); - } -} - -.mx_CallView_callControls { - position: absolute; - display: flex; - justify-content: center; - bottom: 5px; - width: 100%; +.mx_CallView_presenting { opacity: 1; transition: opacity 0.5s; + + position: absolute; + margin-top: 18px; + padding: 4px 8px; + border-radius: 4px; + + // Same on both themes + color: white; + background-color: #17191c; } -.mx_CallView_callControls_hidden { +.mx_CallView_presenting_hidden { opacity: 0.001; // opacity 0 can cause a re-layout pointer-events: none; } - -.mx_CallView_callControls_button { - cursor: pointer; - margin-left: 8px; - margin-right: 8px; - - - &::before { - content: ''; - display: inline-block; - - height: 48px; - width: 48px; - - background-repeat: no-repeat; - background-size: contain; - background-position: center; - } -} - -.mx_CallView_callControls_dialpad { - margin-right: auto; - &::before { - background-image: url('$(res)/img/voip/dialpad.svg'); - } -} - -.mx_CallView_callControls_button_dialpad_hidden { - margin-right: auto; - cursor: initial; -} - -.mx_CallView_callControls_button_micOn { - &::before { - background-image: url('$(res)/img/voip/mic-on.svg'); - } -} - -.mx_CallView_callControls_button_micOff { - &::before { - background-image: url('$(res)/img/voip/mic-off.svg'); - } -} - -.mx_CallView_callControls_button_vidOn { - &::before { - background-image: url('$(res)/img/voip/vid-on.svg'); - } -} - -.mx_CallView_callControls_button_vidOff { - &::before { - background-image: url('$(res)/img/voip/vid-off.svg'); - } -} - -.mx_CallView_callControls_button_hangup { - &::before { - background-image: url('$(res)/img/voip/hangup.svg'); - } -} - -.mx_CallView_callControls_button_more { - margin-left: auto; - &::before { - background-image: url('$(res)/img/voip/more.svg'); - } -} - -.mx_CallView_callControls_button_more_hidden { - margin-left: auto; - cursor: initial; -} - -.mx_CallView_callControls_button_invisible { - visibility: hidden; - pointer-events: none; - position: absolute; -} diff --git a/res/css/views/voip/_CallViewForRoom.scss b/res/css/views/voip/_CallViewForRoom.scss index 769e00338e..d23fcc18bc 100644 --- a/res/css/views/voip/_CallViewForRoom.scss +++ b/res/css/views/voip/_CallViewForRoom.scss @@ -39,7 +39,7 @@ limitations under the License. width: 100%; max-width: 64px; - background-color: $primary-fg-color; + background-color: $primary-content; } } } diff --git a/res/css/views/voip/_CallViewHeader.scss b/res/css/views/voip/_CallViewHeader.scss new file mode 100644 index 0000000000..0575f4f535 --- /dev/null +++ b/res/css/views/voip/_CallViewHeader.scss @@ -0,0 +1,129 @@ +/* +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_CallViewHeader { + height: 44px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: left; + flex-shrink: 0; + cursor: pointer; +} + +.mx_CallViewHeader_callType { + font-size: 1.2rem; + font-weight: bold; + vertical-align: middle; +} + +.mx_CallViewHeader_secondaryCallInfo { + &::before { + content: '·'; + margin-left: 6px; + margin-right: 6px; + } +} + +.mx_CallViewHeader_controls { + margin-left: auto; +} + +.mx_CallViewHeader_button { + display: inline-block; + vertical-align: middle; + cursor: pointer; + + &::before { + content: ''; + display: inline-block; + height: 20px; + width: 20px; + vertical-align: middle; + background-color: $secondary-content; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } +} + +.mx_CallViewHeader_button_fullscreen { + &::before { + mask-image: url('$(res)/img/element-icons/call/fullscreen.svg'); + } +} + +.mx_CallViewHeader_button_expand { + &::before { + mask-image: url('$(res)/img/element-icons/call/expand.svg'); + } +} + +.mx_CallViewHeader_callInfo { + margin-left: 12px; + margin-right: 16px; +} + +.mx_CallViewHeader_roomName { + font-weight: bold; + font-size: 12px; + line-height: initial; + height: 15px; +} + +.mx_CallView_secondaryCall_roomName { + margin-left: 4px; +} + +.mx_CallViewHeader_callTypeSmall { + font-size: 12px; + color: $secondary-content; + line-height: initial; + height: 15px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 240px; +} + +.mx_CallViewHeader_callTypeIcon { + display: inline-block; + margin-right: 6px; + height: 16px; + width: 16px; + vertical-align: middle; + + &::before { + content: ''; + display: inline-block; + vertical-align: top; + + height: 16px; + width: 16px; + background-color: $secondary-content; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + + &.mx_CallViewHeader_callTypeIcon_voice::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } + + &.mx_CallViewHeader_callTypeIcon_video::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } +} diff --git a/res/css/views/voip/_CallViewSidebar.scss b/res/css/views/voip/_CallViewSidebar.scss new file mode 100644 index 0000000000..fd9c76defc --- /dev/null +++ b/res/css/views/voip/_CallViewSidebar.scss @@ -0,0 +1,60 @@ +/* +Copyright 2021 Šimon Brandner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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_CallViewSidebar { + position: absolute; + right: 16px; + bottom: 16px; + z-index: 100; // To be above the primary feed + + overflow: auto; + + height: calc(100% - 32px); // Subtract the top and bottom padding + width: 20%; + + display: flex; + flex-direction: column-reverse; + justify-content: flex-start; + align-items: flex-end; + gap: 12px; + + > .mx_VideoFeed { + width: 100%; + border-radius: 4px; + + &.mx_VideoFeed_voice { + display: flex; + align-items: center; + justify-content: center; + } + + .mx_VideoFeed_video { + border-radius: 4px; + } + + .mx_VideoFeed_mic { + left: 6px; + bottom: 6px; + } + } + + &.mx_CallViewSidebar_pipMode { + top: 16px; + bottom: unset; + justify-content: flex-end; + gap: 4px; + } +} diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss index eefd2e9ba5..288f1f5d31 100644 --- a/res/css/views/voip/_DialPad.scss +++ b/res/css/views/voip/_DialPad.scss @@ -33,7 +33,7 @@ limitations under the License. width: 40px; height: 40px; - background-color: $dialpad-button-bg-color; + background-color: $quinary-content; border-radius: 40px; font-size: 18px; font-weight: 600; diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index 0019994e72..d2014241e9 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -30,7 +30,7 @@ limitations under the License. margin-right: 20px; /* a separator between the input line and the dial buttons */ - border-bottom: 1px solid $quaternary-fg-color; + border-bottom: 1px solid $quaternary-content; transition: border-bottom 0.25s; } @@ -69,7 +69,6 @@ limitations under the License. overflow: hidden; max-width: 185px; text-align: left; - direction: rtl; padding: 8px 0px; background-color: rgb(0, 0, 0, 0); } diff --git a/res/css/views/voip/_DialPadModal.scss b/res/css/views/voip/_DialPadModal.scss index b8042f77ae..f378507f90 100644 --- a/res/css/views/voip/_DialPadModal.scss +++ b/res/css/views/voip/_DialPadModal.scss @@ -30,7 +30,7 @@ limitations under the License. margin-right: 40px; /* a separator between the input line and the dial buttons */ - border-bottom: 1px solid $quaternary-fg-color; + border-bottom: 1px solid $quaternary-content; transition: border-bottom 0.25s; } diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 4a3fbdf597..1f17a54692 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -14,37 +14,65 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_VideoFeed_voice { - background-color: $inverted-bg-color; -} - - -.mx_VideoFeed_remote { - width: 100%; - height: 100%; +.mx_VideoFeed { + overflow: hidden; + position: relative; + box-sizing: border-box; + border: transparent 2px solid; display: flex; - justify-content: center; - align-items: center; - &.mx_VideoFeed_video { - background-color: #000; + &.mx_VideoFeed_voice { + background-color: $inverted-bg-color; + aspect-ratio: 16 / 9; } -} -.mx_VideoFeed_local { - max-width: 25%; - max-height: 25%; - position: absolute; - right: 10px; - top: 10px; - z-index: 100; - border-radius: 4px; + &.mx_VideoFeed_speaking { + border: $accent-color 2px solid; - &.mx_VideoFeed_video { + .mx_VideoFeed_video { + border-radius: 0; + } + } + + .mx_VideoFeed_video { + width: 100%; background-color: transparent; + + &.mx_VideoFeed_video_mirror { + transform: scale(-1, 1); + } + } + + .mx_VideoFeed_mic { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + + width: 24px; + height: 24px; + + background-color: rgba(0, 0, 0, 0.5); // Same on both themes + border-radius: 100%; + + &::before { + position: absolute; + content: ""; + width: 16px; + height: 16px; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + background-color: white; // Same on both themes + border-radius: 7px; + } + + &.mx_VideoFeed_mic_muted::before { + mask-image: url('$(res)/img/voip/mic-muted.svg'); + } + + &.mx_VideoFeed_mic_unmuted::before { + mask-image: url('$(res)/img/voip/mic-unmuted.svg'); + } } } - -.mx_VideoFeed_mirror { - transform: scale(-1, 1); -} diff --git a/res/img/element-icons/message/thread.svg b/res/img/element-icons/message/thread.svg new file mode 100644 index 0000000000..b4a7cc0066 --- /dev/null +++ b/res/img/element-icons/message/thread.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/room/pin.svg b/res/img/element-icons/room/pin.svg index 2448fc61c5..f090f60be8 100644 --- a/res/img/element-icons/room/pin.svg +++ b/res/img/element-icons/room/pin.svg @@ -1,7 +1,3 @@ - - - - - + diff --git a/res/img/feather-customised/globe.svg b/res/img/feather-customised/globe.svg deleted file mode 100644 index 8af7dc41dc..0000000000 --- a/res/img/feather-customised/globe.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/res/img/voip/declined-video.svg b/res/img/voip/declined-video.svg new file mode 100644 index 0000000000..509ffa8fd1 --- /dev/null +++ b/res/img/voip/declined-video.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/declined-voice.svg b/res/img/voip/declined-voice.svg new file mode 100644 index 0000000000..78e8d90cdf --- /dev/null +++ b/res/img/voip/declined-voice.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/voip/mic-muted.svg b/res/img/voip/mic-muted.svg new file mode 100644 index 0000000000..0cb7ad1c9e --- /dev/null +++ b/res/img/voip/mic-muted.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/voip/mic-unmuted.svg b/res/img/voip/mic-unmuted.svg new file mode 100644 index 0000000000..8334cafa0a --- /dev/null +++ b/res/img/voip/mic-unmuted.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/voip/missed-video.svg b/res/img/voip/missed-video.svg new file mode 100644 index 0000000000..a2f3bc73ac --- /dev/null +++ b/res/img/voip/missed-video.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/missed-voice.svg b/res/img/voip/missed-voice.svg new file mode 100644 index 0000000000..5e3993584e --- /dev/null +++ b/res/img/voip/missed-voice.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/voip/screensharing-off.svg b/res/img/voip/screensharing-off.svg new file mode 100644 index 0000000000..dc19e9892e --- /dev/null +++ b/res/img/voip/screensharing-off.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/screensharing-on.svg b/res/img/voip/screensharing-on.svg new file mode 100644 index 0000000000..a8e7fe308e --- /dev/null +++ b/res/img/voip/screensharing-on.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/sidebar-off.svg b/res/img/voip/sidebar-off.svg new file mode 100644 index 0000000000..7637a9ab55 --- /dev/null +++ b/res/img/voip/sidebar-off.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/sidebar-on.svg b/res/img/voip/sidebar-on.svg new file mode 100644 index 0000000000..a625334be4 --- /dev/null +++ b/res/img/voip/sidebar-on.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 655492661c..4a6db5dd55 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -1,28 +1,37 @@ +// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A741 +$accent: #0DBD8B; +$alert: #FF5B55; +$links: #0086e6; +$primary-content: #ffffff; +$secondary-content: #A9B2BC; +$tertiary-content: #8E99A4; +$quaternary-content: #6F7882; +$quinary-content: #394049; +$system: #21262C; +$background: #15191E; +$panels: rgba($system, 0.9); +$panel-base: #8D97A5; // This color is not intended for use in the app +$panel-selected: rgba($panel-base, 0.3); +$panel-hover: rgba($panel-base, 0.1); +$panel-actions: rgba($panel-base, 0.2); +$space-nav: rgba($panel-base, 0.1); + +// TODO: Move userId colors here + // unified palette // try to use these colors when possible -$bg-color: #15191E; -$base-color: $bg-color; -$base-text-color: #ffffff; $header-panel-bg-color: #20252B; $header-panel-border-color: #000000; $header-panel-text-primary-color: #B9BEC6; $header-panel-text-secondary-color: #c8c8cd; -$text-primary-color: #ffffff; $text-secondary-color: #B9BEC6; -$quaternary-fg-color: #6F7882; $search-bg-color: #181b21; $search-placeholder-color: #61708b; $room-highlight-color: #343a46; // typical text (dark-on-white in light skin) -$primary-fg-color: $text-primary-color; -$primary-bg-color: $bg-color; $muted-fg-color: $header-panel-text-primary-color; -// additional text colors -$secondary-fg-color: #A9B2BC; -$tertiary-fg-color: #8E99A4; - // used for dialog box text $light-fg-color: $header-panel-text-secondary-color; @@ -41,13 +50,13 @@ $info-plinth-fg-color: #888; $preview-bar-bg-color: $header-panel-bg-color; $groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82); -$inverted-bg-color: $base-color; +$inverted-bg-color: $background; // used by AddressSelector $selected-color: $room-highlight-color; // selected for hoverover & selected event tiles -$event-selected-color: #21262c; +$event-selected-color: $system; // used for the hairline dividers in RoomView $primary-hairline-color: transparent; @@ -62,7 +71,7 @@ $input-focused-border-color: #238cf5; $input-valid-border-color: $accent-color; $input-invalid-border-color: $warning-color; -$field-focused-label-bg-color: $bg-color; +$field-focused-label-bg-color: $background; $resend-button-divider-color: #b9bec64a; // muted-text with a 4A opacity. @@ -73,15 +82,15 @@ $scrollbar-track-color: transparent; // context menus $menu-border-color: $header-panel-border-color; $menu-bg-color: $header-panel-bg-color; -$menu-box-shadow-color: $bg-color; +$menu-box-shadow-color: $background; $menu-selected-color: $room-highlight-color; $avatar-initial-color: #ffffff; -$avatar-bg-color: $bg-color; +$avatar-bg-color: $background; -$h3-color: $primary-fg-color; +$h3-color: $primary-content; -$dialog-title-fg-color: $base-text-color; +$dialog-title-fg-color: $primary-content; $dialog-backdrop-color: #000; $dialog-shadow-color: rgba(0, 0, 0, 0.48); $dialog-close-fg-color: #9fa9ba; @@ -91,45 +100,39 @@ $lightbox-background-bg-color: #000; $lightbox-background-bg-opacity: 0.85; $settings-grey-fg-color: #a2a2a2; -$settings-profile-placeholder-bg-color: #21262c; +$settings-profile-placeholder-bg-color: $system; $settings-profile-overlay-placeholder-fg-color: #454545; $settings-profile-button-bg-color: #e7e7e7; $settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color; $settings-subsection-fg-color: $text-secondary-color; -$topleftmenu-color: $text-primary-color; -$roomheader-color: $text-primary-color; -$roomheader-bg-color: $bg-color; +$topleftmenu-color: $primary-content; +$roomheader-color: $primary-content; $roomheader-addroom-bg-color: rgba(92, 100, 112, 0.3); -$roomheader-addroom-fg-color: $text-primary-color; +$roomheader-addroom-fg-color: $primary-content; $groupFilterPanel-button-color: $header-panel-text-primary-color; $groupheader-button-color: $header-panel-text-primary-color; $rightpanel-button-color: $header-panel-text-primary-color; -$icon-button-color: #8E99A4; +$icon-button-color: $tertiary-content; $roomtopic-color: $text-secondary-color; $eventtile-meta-color: $roomtopic-color; $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; -// this probably shouldn't have it's own colour -$voipcall-plinth-color: #394049; - // ******************** $theme-button-bg-color: #e3e8f0; -$dialpad-button-bg-color: #394049; $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons -$roomlist-filter-active-bg-color: $bg-color; $roomlist-bg-color: rgba(33, 38, 44, 0.90); -$roomlist-header-color: $tertiary-fg-color; -$roomsublist-divider-color: $primary-fg-color; +$roomlist-header-color: $tertiary-content; +$roomsublist-divider-color: $primary-content; $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); $groupFilterPanel-divider-color: $roomlist-header-color; -$roomtile-preview-color: $secondary-fg-color; +$roomtile-preview-color: $secondary-content; $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: rgba(141, 151, 165, 0.2); @@ -153,20 +156,20 @@ $event-highlight-bg-color: #25271F; $event-timestamp-color: $text-secondary-color; // Tabbed views -$tab-label-fg-color: $text-primary-color; -$tab-label-active-fg-color: $text-primary-color; +$tab-label-fg-color: $primary-content; +$tab-label-active-fg-color: $primary-content; $tab-label-bg-color: transparent; $tab-label-active-bg-color: $accent-color; -$tab-label-icon-bg-color: $text-primary-color; -$tab-label-active-icon-bg-color: $text-primary-color; +$tab-label-icon-bg-color: $primary-content; +$tab-label-active-icon-bg-color: $primary-content; // Buttons -$button-primary-fg-color: #ffffff; +$button-primary-fg-color: $primary-content; $button-primary-bg-color: $accent-color; $button-secondary-bg-color: transparent; -$button-danger-fg-color: #ffffff; +$button-danger-fg-color: $primary-content; $button-danger-bg-color: $notice-primary-color; -$button-danger-disabled-fg-color: #ffffff; +$button-danger-disabled-fg-color: $primary-content; $button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color $button-link-fg-color: $accent-color; $button-link-bg-color: transparent; @@ -175,7 +178,7 @@ $button-link-bg-color: transparent; $togglesw-off-color: $room-highlight-color; $progressbar-fg-color: $accent-color; -$progressbar-bg-color: #21262c; +$progressbar-bg-color: $system; $visual-bell-bg-color: #800; @@ -198,40 +201,41 @@ $reaction-row-button-selected-border-color: $accent-color; $kbd-border-color: #000000; $tooltip-timeline-bg-color: $groupFilterPanel-bg-color; -$tooltip-timeline-fg-color: #ffffff; +$tooltip-timeline-fg-color: $primary-content; -$interactive-tooltip-bg-color: $base-color; -$interactive-tooltip-fg-color: #ffffff; +$interactive-tooltip-bg-color: $background; +$interactive-tooltip-fg-color: $primary-content; $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; -$message-body-panel-fg-color: $secondary-fg-color; -$message-body-panel-bg-color: #394049; // "Dark Tile" -$message-body-panel-icon-fg-color: $secondary-fg-color; -$message-body-panel-icon-bg-color: #21262C; // "System Dark" +$message-body-panel-fg-color: $secondary-content; +$message-body-panel-bg-color: $quinary-content; +$message-body-panel-icon-bg-color: $system; +$message-body-panel-icon-fg-color: $secondary-content; -$voice-record-stop-border-color: $quaternary-fg-color; -$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; -$voice-record-icon-color: $quaternary-fg-color; +$voice-record-stop-border-color: $quaternary-content; +$voice-record-waveform-incomplete-fg-color: $quaternary-content; +$voice-record-icon-color: $quaternary-content; $voice-playback-button-bg-color: $message-body-panel-icon-bg-color; $voice-playback-button-fg-color: $message-body-panel-icon-fg-color; // Appearance tab colors $appearance-tab-border-color: $room-highlight-color; -// blur amounts for left left panel (only for element theme, used in _mods.scss) -$roomlist-background-blur-amount: 60px; -$groupFilterPanel-background-blur-amount: 30px; +// blur amounts for left left panel (only for element theme) +:root { + --lp-background-blur: 45px; +} $composer-shadow-color: rgba(0, 0, 0, 0.28); // Bubble tiles -$eventbubble-self-bg: #143A34; -$eventbubble-others-bg: #394049; -$eventbubble-bg-hover: #433C23; -$eventbubble-avatar-outline: $bg-color; +$eventbubble-self-bg: #14322E; +$eventbubble-others-bg: $event-selected-color; +$eventbubble-bg-hover: #1C2026; +$eventbubble-avatar-outline: $background; $eventbubble-reply-color: #C1C6CD; // ***** Mixins! ***** diff --git a/res/themes/dark/css/dark.scss b/res/themes/dark/css/dark.scss index 600cfd528a..df83d6db88 100644 --- a/res/themes/dark/css/dark.scss +++ b/res/themes/dark/css/dark.scss @@ -2,10 +2,6 @@ @import "../../light/css/_paths.scss"; @import "../../light/css/_fonts.scss"; @import "../../light/css/_light.scss"; -// important this goes before _mods, -// as $groupFilterPanel-background-blur-amount and -// $roomlist-background-blur-amount -// are overridden in _dark.scss @import "_dark.scss"; @import "../../light/css/_mods.scss"; @import "../../../../res/css/_components.scss"; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 0c0197cfb0..d5bc5e6dd7 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -1,3 +1,6 @@ +// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A741 +$system: #21262C; + // unified palette // try to use these colors when possible $bg-color: #181b21; @@ -21,7 +24,12 @@ $primary-bg-color: $bg-color; $muted-fg-color: $header-panel-text-primary-color; // Legacy theme backports -$quaternary-fg-color: #6F7882; +$primary-content: $primary-fg-color; +$secondary-content: $secondary-fg-color; +$tertiary-content: $tertiary-fg-color; +$quaternary-content: #6F7882; +$quinary-content: $quaternary-content; +$background: $primary-bg-color; // used for dialog box text $light-fg-color: $header-panel-text-secondary-color; @@ -111,13 +119,9 @@ $eventtile-meta-color: $roomtopic-color; $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; -// this probably shouldn't have it's own colour -$voipcall-plinth-color: #394049; - // ******************** $theme-button-bg-color: #e3e8f0; -$dialpad-button-bg-color: #6F7882; $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons @@ -222,6 +226,13 @@ $appearance-tab-border-color: $room-highlight-color; $composer-shadow-color: tranparent; +// Bubble tiles +$eventbubble-self-bg: #14322E; +$eventbubble-others-bg: $event-selected-color; +$eventbubble-bg-hover: #1C2026; +$eventbubble-avatar-outline: $bg-color; +$eventbubble-reply-color: #C1C6CD; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index b7d45452ff..47247e5e23 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -8,9 +8,12 @@ /* Noto Color Emoji contains digits, in fixed-width, therefore causing digits in flowed text to stand out. TODO: Consider putting all emoji fonts to the end rather than the front. */ -$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji'; +$font-family: 'Nunito', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', sans-serif, 'Noto Color Emoji'; -$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji'; +$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', monospace, 'Noto Color Emoji'; + +// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0 +$system: #F4F6FA; // unified palette // try to use these colors when possible @@ -29,7 +32,12 @@ $primary-bg-color: #ffffff; $muted-fg-color: #61708b; // Commonly used in headings and relevant alt text // Legacy theme backports -$quaternary-fg-color: #C1C6CD; +$primary-content: $primary-fg-color; +$secondary-content: $secondary-fg-color; +$tertiary-content: $tertiary-fg-color; +$quaternary-content: #C1C6CD; +$quinary-content: #e3e8f0; +$background: $primary-bg-color; // used for dialog box text $light-fg-color: #747474; @@ -178,14 +186,11 @@ $eventtile-meta-color: $roomtopic-color; $composer-e2e-icon-color: #91a1c0; $header-divider-color: #91a1c0; -// this probably shouldn't have it's own colour -$voipcall-plinth-color: #F4F6FA; +$voipcall-plinth-color: $system; // ******************** $theme-button-bg-color: #e3e8f0; -$dialpad-button-bg-color: #e3e8f0; - $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $roomlist-button-bg-color; @@ -331,7 +336,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-bg-color: #E3E8F0; $message-body-panel-icon-fg-color: $secondary-fg-color; -$message-body-panel-icon-bg-color: #F4F6FA; +$message-body-panel-icon-bg-color: $system; // See non-legacy _light for variable information $voice-record-stop-symbol-color: #ff4b55; @@ -348,9 +353,9 @@ $appearance-tab-border-color: $input-darker-bg-color; $composer-shadow-color: tranparent; // Bubble tiles -$eventbubble-self-bg: #F8FDFC; -$eventbubble-others-bg: #F7F8F9; -$eventbubble-bg-hover: rgb(242, 242, 242); +$eventbubble-self-bg: #F0FBF8; +$eventbubble-others-bg: $system; +$eventbubble-bg-hover: #FAFBFD; $eventbubble-avatar-outline: #fff; $eventbubble-reply-color: #C1C6CD; @@ -390,7 +395,7 @@ $eventbubble-reply-color: #C1C6CD; @define-mixin mx_DialogButton_secondary { // flip colours for the secondary ones font-weight: 600; - border: 1px solid $accent-color ! important; + border: 1px solid $accent-color !important; color: $accent-color; background-color: $button-secondary-bg-color; } diff --git a/res/themes/light-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss index 1b9254d100..f4685fe8fa 100644 --- a/res/themes/light-custom/css/_custom.scss +++ b/res/themes/light-custom/css/_custom.scss @@ -38,7 +38,7 @@ $lightbox-border-color: var(--timeline-background-color); $menu-bg-color: var(--timeline-background-color); $avatar-bg-color: var(--timeline-background-color); $message-action-bar-bg-color: var(--timeline-background-color); -$primary-bg-color: var(--timeline-background-color); +$background: var(--timeline-background-color); $togglesw-ball-color: var(--timeline-background-color); $droptarget-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .5 $authpage-modal-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .59 @@ -69,7 +69,7 @@ $roomlist-bg-color: var(--roomlist-background-color); // // --timeline-text-color $message-action-bar-fg-color: var(--timeline-text-color); -$primary-fg-color: var(--timeline-text-color); +$primary-content: var(--timeline-text-color); $settings-profile-overlay-placeholder-fg-color: var(--timeline-text-color); $roomtopic-color: var(--timeline-text-color-50pct); $tab-label-fg-color: var(--timeline-text-color); @@ -139,4 +139,11 @@ $event-selected-color: var(--timeline-highlights-color); $event-highlight-bg-color: var(--timeline-highlights-color); // // redirect some variables away from their hardcoded values in the light theme -$settings-grey-fg-color: $primary-fg-color; +$settings-grey-fg-color: $primary-content; + +// --eventbubble colors +$eventbubble-self-bg: var(--eventbubble-self-bg, $eventbubble-self-bg); +$eventbubble-others-bg: var(--eventbubble-others-bg, $eventbubble-others-bg); +$eventbubble-bg-hover: var(--eventbubble-bg-hover, $eventbubble-bg-hover); +$eventbubble-avatar-outline: var(--eventbubble-avatar-outline, $eventbubble-avatar-outline); +$eventbubble-reply-color: var(--eventbubble-reply-color, $eventbubble-reply-color); diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 32722515d8..96e5fd7155 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -8,24 +8,38 @@ /* Noto Color Emoji contains digits, in fixed-width, therefore causing digits in flowed text to stand out. TODO: Consider putting all emoji fonts to the end rather than the front. */ -$font-family: Inter, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji'; +$font-family: 'Inter', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', sans-serif, 'Noto Color Emoji'; -$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji'; +$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', monospace, 'Noto Color Emoji'; + +// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A120 +$accent: #0DBD8B; +$alert: #FF5B55; +$links: #0086e6; +$primary-content: #17191C; +$secondary-content: #737D8C; +$tertiary-content: #8D97A5; +$quaternary-content: #c1c6cd; +$quinary-content: #E3E8F0; +$system: #F4F6FA; +$background: #ffffff; +$panels: rgba($system, 0.9); +$panel-selected: rgba($tertiary-content, 0.3); +$panel-hover: rgba($tertiary-content, 0.1); +$panel-actions: rgba($tertiary-content, 0.2); +$space-nav: rgba($tertiary-content, 0.15); + +// TODO: Move userId colors here // unified palette // try to use these colors when possible -$accent-color: #0DBD8B; +$accent-color: $accent; $accent-bg-color: rgba(3, 179, 129, 0.16); $notice-primary-color: #ff4b55; $notice-primary-bg-color: rgba(255, 75, 85, 0.16); -$primary-fg-color: #2e2f32; -$secondary-fg-color: #737D8C; -$tertiary-fg-color: #8D99A5; -$quaternary-fg-color: #C1C6CD; $header-panel-bg-color: #f3f8fd; // typical text (dark-on-white in light skin) -$primary-bg-color: #ffffff; $muted-fg-color: #61708b; // Commonly used in headings and relevant alt text // used for dialog box text @@ -35,12 +49,12 @@ $light-fg-color: #747474; $focus-bg-color: #dddddd; // button UI (white-on-green in light skin) -$accent-fg-color: #ffffff; +$accent-fg-color: $background; $accent-color-50pct: rgba($accent-color, 0.5); $accent-color-darker: #92caad; $accent-color-alt: #238CF5; -$selection-fg-color: $primary-bg-color; +$selection-fg-color: $background; $focus-brightness: 105%; @@ -79,7 +93,7 @@ $primary-hairline-color: transparent; // used for the border of input text fields $input-border-color: #e7e7e7; -$input-darker-bg-color: #e3e8f0; +$input-darker-bg-color: $quinary-content; $input-darker-fg-color: #9fa9ba; $input-lighter-bg-color: #f2f5f8; $input-lighter-fg-color: $input-darker-fg-color; @@ -87,7 +101,7 @@ $input-focused-border-color: #238cf5; $input-valid-border-color: $accent-color; $input-invalid-border-color: $warning-color; -$field-focused-label-bg-color: #ffffff; +$field-focused-label-bg-color: $background; $button-bg-color: $accent-color; $button-fg-color: white; @@ -109,8 +123,8 @@ $menu-bg-color: #fff; $menu-box-shadow-color: rgba(118, 131, 156, 0.6); $menu-selected-color: #f5f8fa; -$avatar-initial-color: #ffffff; -$avatar-bg-color: #ffffff; +$avatar-initial-color: $background; +$avatar-bg-color: $background; $h3-color: #3d3b39; @@ -138,7 +152,7 @@ $blockquote-bar-color: #ddd; $blockquote-fg-color: #777; $settings-grey-fg-color: #a2a2a2; -$settings-profile-placeholder-bg-color: #f4f6fa; +$settings-profile-placeholder-bg-color: $system; $settings-profile-overlay-placeholder-fg-color: #2e2f32; $settings-profile-button-bg-color: #e7e7e7; $settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color; @@ -154,44 +168,39 @@ $rte-group-pill-color: #aaa; $topleftmenu-color: #212121; $roomheader-color: #45474a; -$roomheader-bg-color: $primary-bg-color; $roomheader-addroom-bg-color: rgba(92, 100, 112, 0.2); $roomheader-addroom-fg-color: #5c6470; $groupFilterPanel-button-color: #91A1C0; $groupheader-button-color: #91A1C0; $rightpanel-button-color: #91A1C0; -$icon-button-color: #C1C6CD; +$icon-button-color: $quaternary-content; $roomtopic-color: #9e9e9e; $eventtile-meta-color: $roomtopic-color; $composer-e2e-icon-color: #91A1C0; $header-divider-color: #91A1C0; -// this probably shouldn't have it's own colour -$voipcall-plinth-color: #F4F6FA; +$voipcall-plinth-color: $system; // ******************** -$theme-button-bg-color: #e3e8f0; -$dialpad-button-bg-color: #e3e8f0; - +$theme-button-bg-color: $quinary-content; $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons -$roomlist-filter-active-bg-color: #ffffff; $roomlist-bg-color: rgba(245, 245, 245, 0.90); -$roomlist-header-color: $tertiary-fg-color; -$roomsublist-divider-color: $primary-fg-color; +$roomlist-header-color: $tertiary-content; +$roomsublist-divider-color: $primary-content; $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%); $groupFilterPanel-divider-color: $roomlist-header-color; -$roomtile-preview-color: $secondary-fg-color; +$roomtile-preview-color: $secondary-content; $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: #FFF; $presence-online: $accent-color; $presence-away: #d9b072; -$presence-offline: #E3E8F0; +$presence-offline: $quinary-content; // ******************** @@ -254,7 +263,7 @@ $lightbox-border-color: #ffffff; // Tabbed views $tab-label-fg-color: #45474a; -$tab-label-active-fg-color: #ffffff; +$tab-label-active-fg-color: $background; $tab-label-bg-color: transparent; $tab-label-active-bg-color: $accent-color; $tab-label-icon-bg-color: #454545; @@ -264,9 +273,9 @@ $tab-label-active-icon-bg-color: $tab-label-active-fg-color; $button-primary-fg-color: #ffffff; $button-primary-bg-color: $accent-color; $button-secondary-bg-color: $accent-fg-color; -$button-danger-fg-color: #ffffff; +$button-danger-fg-color: $background; $button-danger-bg-color: $notice-primary-color; -$button-danger-disabled-fg-color: #ffffff; +$button-danger-disabled-fg-color: $background; $button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color $button-link-fg-color: $accent-color; $button-link-bg-color: transparent; @@ -291,7 +300,7 @@ $memberstatus-placeholder-color: $muted-fg-color; $authpage-bg-color: #2e3649; $authpage-modal-bg-color: rgba(245, 245, 245, 0.90); -$authpage-body-bg-color: #ffffff; +$authpage-body-bg-color: $background; $authpage-focus-bg-color: #dddddd; $authpage-lang-color: #4e5054; $authpage-primary-color: #232f32; @@ -300,8 +309,8 @@ $authpage-secondary-color: #61708b; $dark-panel-bg-color: $secondary-accent-color; $panel-gradient: rgba(242, 245, 248, 0), rgba(242, 245, 248, 1); -$message-action-bar-bg-color: $primary-bg-color; -$message-action-bar-fg-color: $primary-fg-color; +$message-action-bar-bg-color: $background; +$message-action-bar-fg-color: $primary-content; $message-action-bar-border-color: #e9edf1; $message-action-bar-hover-border-color: $focus-bg-color; @@ -315,46 +324,46 @@ $kbd-border-color: $reaction-row-button-border-color; $inverted-bg-color: #27303a; $tooltip-timeline-bg-color: $inverted-bg-color; -$tooltip-timeline-fg-color: #ffffff; +$tooltip-timeline-fg-color: $background; $interactive-tooltip-bg-color: #27303a; -$interactive-tooltip-fg-color: #ffffff; +$interactive-tooltip-fg-color: $background; $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; -$message-body-panel-fg-color: $secondary-fg-color; -$message-body-panel-bg-color: #E3E8F0; // "Separator" -$message-body-panel-icon-fg-color: $secondary-fg-color; -$message-body-panel-icon-bg-color: #F4F6FA; +$message-body-panel-fg-color: $secondary-content; +$message-body-panel-bg-color: $quinary-content; +$message-body-panel-icon-bg-color: $system; +$message-body-panel-icon-fg-color: $secondary-content; // These two don't change between themes. They are the $warning-color, but we don't // want custom themes to affect them by accident. $voice-record-stop-symbol-color: #ff4b55; $voice-record-live-circle-color: #ff4b55; -$voice-record-stop-border-color: #E3E8F0; // "Separator" -$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; -$voice-record-icon-color: $tertiary-fg-color; +$voice-record-stop-border-color: $quinary-content; +$voice-record-waveform-incomplete-fg-color: $quaternary-content; +$voice-record-icon-color: $tertiary-content; $voice-playback-button-bg-color: $message-body-panel-icon-bg-color; $voice-playback-button-fg-color: $message-body-panel-icon-fg-color; // FontSlider colors $appearance-tab-border-color: $input-darker-bg-color; -// blur amounts for left left panel (only for element theme, used in _mods.scss) -$roomlist-background-blur-amount: 40px; -$groupFilterPanel-background-blur-amount: 20px; - +// blur amounts for left left panel (only for element theme) +:root { + --lp-background-blur: 40px; +} $composer-shadow-color: rgba(0, 0, 0, 0.04); // Bubble tiles -$eventbubble-self-bg: #F8FDFC; -$eventbubble-others-bg: #F7F8F9; -$eventbubble-bg-hover: #FEFCF5; -$eventbubble-avatar-outline: $primary-bg-color; -$eventbubble-reply-color: #C1C6CD; +$eventbubble-self-bg: #F0FBF8; +$eventbubble-others-bg: $system; +$eventbubble-bg-hover: #FAFBFD; +$eventbubble-avatar-outline: $background; +$eventbubble-reply-color: $quaternary-content; // ***** Mixins! ***** @@ -392,7 +401,7 @@ $eventbubble-reply-color: #C1C6CD; @define-mixin mx_DialogButton_secondary { // flip colours for the secondary ones font-weight: 600; - border: 1px solid $accent-color ! important; + border: 1px solid $accent-color !important; color: $accent-color; background-color: $button-secondary-bg-color; } diff --git a/res/themes/light/css/_mods.scss b/res/themes/light/css/_mods.scss index fbca58dfb1..15f6d4b0fe 100644 --- a/res/themes/light/css/_mods.scss +++ b/res/themes/light/css/_mods.scss @@ -4,27 +4,6 @@ // set the user avatar (if any) as a background so // it can be blurred by the tag panel and room list -@supports (backdrop-filter: none) { - .mx_LeftPanel { - background-image: var(--avatar-url, unset); - background-repeat: no-repeat; - background-size: cover; - background-position: left top; - } - - .mx_GroupFilterPanel { - backdrop-filter: blur($groupFilterPanel-background-blur-amount); - } - - .mx_SpacePanel { - backdrop-filter: blur($groupFilterPanel-background-blur-amount); - } - - .mx_LeftPanel .mx_LeftPanel_roomListContainer { - backdrop-filter: blur($roomlist-background-blur-amount); - } -} - .mx_RoomSublist_showNButton { background-color: transparent !important; } diff --git a/scripts/ci/js-sdk-to-release.js b/scripts/ci/js-sdk-to-release.js new file mode 100755 index 0000000000..e1fecfde03 --- /dev/null +++ b/scripts/ci/js-sdk-to-release.js @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +const fsProm = require('fs/promises'); + +const PKGJSON = 'node_modules/matrix-js-sdk/package.json'; + +async function main() { + const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8')); + for (const field of ['main', 'typings']) { + if (pkgJson["matrix_lib_"+field] !== undefined) { + pkgJson[field] = pkgJson["matrix_lib_"+field]; + } + } + await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2)); +} + +main(); diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 0990af70ce..ec021236d9 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -10,6 +10,7 @@ defbranch="$3" rm -r "$defrepo" || true +# A function that clones a branch of a repo based on the org, repo and branch clone() { org=$1 repo=$2 @@ -21,45 +22,38 @@ clone() { fi } -# Try the PR author's branch in case it exists on the deps as well. -# First we check if GITHUB_HEAD_REF is defined, -# Then we check if BUILDKITE_BRANCH is defined, -# if they aren't we can assume this is a Netlify build -if [ -n "$GITHUB_HEAD_REF" ]; then - head=$GITHUB_HEAD_REF -elif [ -n "$BUILDKITE_BRANCH" ]; then - head=$BUILDKITE_BRANCH -else - # Netlify doesn't give us info about the fork so we have to get it from GitHub API - apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" - apiEndpoint+=$REVIEW_ID - head=$(curl $apiEndpoint | jq -r '.head.label') -fi +# A function that gets info about a PR from the GitHub API based on its number +getPRInfo() { + number=$1 + if [ -n "$number" ]; then + echo "Getting info about a PR with number $number" -# If head is set, it will contain on Buildkite either: -# * "branch" when the author's branch and target branch are in the same repo -# * "fork:branch" when the author's branch is in their fork or if this is a Netlify build -# We can split on `:` into an array to check. -# For GitHub Actions we need to inspect GITHUB_REPOSITORY and GITHUB_ACTOR -# to determine whether the branch is from a fork or not -BRANCH_ARRAY=(${head//:/ }) -if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then + apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" + apiEndpoint+=$number - if [ -n "$GITHUB_HEAD_REF" ]; then - if [[ "$GITHUB_REPOSITORY" == "$deforg"* ]]; then - clone $deforg $defrepo $GITHUB_HEAD_REF - else - REPO_ARRAY=(${GITHUB_REPOSITORY//\// }) - clone $REPO_ARRAY[0] $defrepo $GITHUB_HEAD_REF - fi - else - clone $deforg $defrepo $BUILDKITE_BRANCH + head=$(curl $apiEndpoint | jq -r '.head.label') fi +} -elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then - clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]} +# Some CIs don't give us enough info, so we just get the PR number and ask the +# GH API for more info - "fork:branch". Some give us this directly. +if [ -n "$BUILDKITE_BRANCH" ]; then + # BuildKite + head=$BUILDKITE_BRANCH +elif [ -n "$PR_NUMBER" ]; then + # GitHub + getPRInfo $PR_NUMBER +elif [ -n "$REVIEW_ID" ]; then + # Netlify + getPRInfo $REVIEW_ID fi +# $head will always be in the format "fork:branch", so we split it by ":" into +# an array. The first element will then be the fork and the second the branch. +# Based on that we clone +BRANCH_ARRAY=(${head//:/ }) +clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]} + # Try the target branch of the push or PR. if [ -n $GITHUB_BASE_REF ]; then clone $deforg $defrepo $GITHUB_BASE_REF diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 9d6bc2c6fb..8ad93fa960 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -93,6 +93,26 @@ declare global { mxSetupEncryptionStore?: SetupEncryptionStore; mxRoomScrollStateStore?: RoomScrollStateStore; mxOnRecaptchaLoaded?: () => void; + electron?: Electron; + } + + interface DesktopCapturerSource { + id: string; + name: string; + thumbnailURL: string; + } + + interface GetSourcesOptions { + types: Array; + thumbnailSize?: { + height: number; + width: number; + }; + fetchWindowIcons?: boolean; + } + + interface Electron { + getDesktopCapturerSources(options: GetSourcesOptions): Promise>; } interface Document { diff --git a/src/ActiveRoomObserver.ts b/src/ActiveRoomObserver.ts index 0be49a24ea..c7423fab8f 100644 --- a/src/ActiveRoomObserver.ts +++ b/src/ActiveRoomObserver.ts @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import RoomViewStore from './stores/RoomViewStore'; import { EventSubscription } from 'fbemitter'; +import RoomViewStore from './stores/RoomViewStore'; type Listener = (isActive: boolean) => void; diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index e7ba1aa9fb..fe938c9929 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -1,7 +1,8 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017, 2018 New Vector Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -56,12 +57,10 @@ limitations under the License. import React from 'react'; import { MatrixClientPeg } from './MatrixClientPeg'; -import PlatformPeg from './PlatformPeg'; import Modal from './Modal'; import { _t } from './languageHandler'; import dis from './dispatcher/dispatcher'; import WidgetUtils from './utils/WidgetUtils'; -import WidgetEchoStore from './stores/WidgetEchoStore'; import SettingsStore from './settings/SettingsStore'; import { Jitsi } from "./widgets/Jitsi"; import { WidgetType } from "./widgets/WidgetType"; @@ -80,7 +79,6 @@ 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 { Action } from './dispatcher/actions'; import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; @@ -88,6 +86,12 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/ import EventEmitter from 'events'; import SdkConfig from './SdkConfig'; import { ensureDMExists, findDMForUser } from './createRoom'; +import { RuleId, TweakName, Tweaks } from "matrix-js-sdk/src/@types/PushRules"; +import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor'; +import { WidgetLayoutStore, Container } from './stores/widgets/WidgetLayoutStore'; +import { getIncomingCallToastKey } from './toasts/IncomingCallToast'; +import ToastStore from './stores/ToastStore'; +import IncomingCallToast from "./toasts/IncomingCallToast"; export const PROTOCOL_PSTN = 'm.protocol.pstn'; export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn'; @@ -129,14 +133,9 @@ interface ThirdpartyLookupResponse { fields: ThirdpartyLookupResponseFields; } -// Unlike 'CallType' in js-sdk, this one includes screen sharing -// (because a screen sharing call is only a screen sharing call to the caller, -// to the callee it's just a video call, at least as far as the current impl -// is concerned). export enum PlaceCallType { Voice = 'voice', Video = 'video', - ScreenSharing = 'screensharing', } export enum CallHandlerEvent { @@ -251,7 +250,15 @@ export default class CallHandler extends EventEmitter { * @returns {boolean} */ private areAnyCallsUnsilenced(): boolean { - return this.calls.size > this.silencedCalls.size; + for (const call of this.calls.values()) { + if ( + call.state === CallState.Ringing && + !this.isCallSilenced(call.callId) + ) { + return true; + } + } + return false; } private async checkProtocols(maxTries) { @@ -465,77 +472,7 @@ export default class CallHandler extends EventEmitter { this.removeCallForRoom(mappedRoomId); }); call.on(CallEvent.State, (newState: CallState, oldState: CallState) => { - if (!this.matchesCallForThisRoom(call)) return; - - this.setCallState(call, newState); - - switch (oldState) { - case CallState.Ringing: - this.pause(AudioID.Ring); - break; - case CallState.InviteSent: - this.pause(AudioID.Ringback); - break; - } - - if (newState !== CallState.Ringing) { - this.silencedCalls.delete(call.callId); - } - - switch (newState) { - case CallState.Ringing: - this.play(AudioID.Ring); - break; - case CallState.InviteSent: - this.play(AudioID.Ringback); - break; - case CallState.Ended: - { - Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason); - this.removeCallForRoom(mappedRoomId); - if (oldState === CallState.InviteSent && ( - call.hangupParty === CallParty.Remote || - (call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout) - )) { - this.play(AudioID.Busy); - let title; - let description; - if (call.hangupReason === CallErrorCode.UserHangup) { - title = _t("Call Declined"); - description = _t("The other party declined the call."); - } else if (call.hangupReason === CallErrorCode.UserBusy) { - title = _t("User Busy"); - description = _t("The user you called is busy."); - } else if (call.hangupReason === CallErrorCode.InviteTimeout) { - title = _t("Call Failed"); - // XXX: full stop appended as some relic here, but these - // strings need proper input from design anyway, so let's - // not change this string until we have a proper one. - description = _t('The remote side failed to pick up') + '.'; - } else { - title = _t("Call Failed"); - description = _t("The call could not be established"); - } - - Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { - title, description, - }); - } else if ( - call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting - ) { - Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { - title: _t("Answered Elsewhere"), - description: _t("The call was answered on another device."), - }); - } else if (oldState !== CallState.Fledgling && oldState !== CallState.Ringing) { - // don't play the end-call sound for calls that never got off the ground - this.play(AudioID.CallEnd); - } - - this.logCallStats(call, mappedRoomId); - break; - } - } + this.onCallStateChanged(newState, oldState, call); }); call.on(CallEvent.Replaced, (newCall: MatrixCall) => { if (!this.matchesCallForThisRoom(call)) return; @@ -548,8 +485,8 @@ export default class CallHandler extends EventEmitter { this.pause(AudioID.Ringback); } - this.calls.set(mappedRoomId, newCall); - this.emit(CallHandlerEvent.CallsChanged, this.calls); + this.removeCallForRoom(mappedRoomId); + this.addCallForRoom(mappedRoomId, newCall); this.setCallListeners(newCall); this.setCallState(newCall, newCall.state); }); @@ -584,13 +521,95 @@ export default class CallHandler extends EventEmitter { this.removeCallForRoom(mappedRoomId); mappedRoomId = newMappedRoomId; console.log("Moving call to room " + mappedRoomId); - this.calls.set(mappedRoomId, call); - this.emit(CallHandlerEvent.CallChangeRoom, call); + this.addCallForRoom(mappedRoomId, call, true); } } }); } + private onCallStateChanged = (newState: CallState, oldState: CallState, call: MatrixCall): void => { + if (!this.matchesCallForThisRoom(call)) return; + + const mappedRoomId = this.roomIdForCall(call); + this.setCallState(call, newState); + + switch (oldState) { + case CallState.Ringing: + this.pause(AudioID.Ring); + break; + case CallState.InviteSent: + this.pause(AudioID.Ringback); + break; + } + + if (newState !== CallState.Ringing) { + this.silencedCalls.delete(call.callId); + } + + switch (newState) { + case CallState.Ringing: { + const incomingCallPushRule = ( + new PushProcessor(MatrixClientPeg.get()).getPushRuleById(RuleId.IncomingCall) + ); + const pushRuleEnabled = incomingCallPushRule?.enabled; + const tweakSetToRing = incomingCallPushRule?.actions.some((action: Tweaks) => ( + action.set_tweak === TweakName.Sound && + action.value === "ring" + )); + + if (pushRuleEnabled && tweakSetToRing) { + this.play(AudioID.Ring); + } else { + this.silenceCall(call.callId); + } + break; + } + case CallState.InviteSent: { + this.play(AudioID.Ringback); + break; + } + case CallState.Ended: { + const hangupReason = call.hangupReason; + Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason); + this.removeCallForRoom(mappedRoomId); + if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) { + this.play(AudioID.Busy); + + // Don't show a modal when we got rejected/the call was hung up + if (!hangupReason || [CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) break; + + let title; + let description; + // TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...) + if (call.hangupReason === CallErrorCode.UserBusy) { + title = _t("User Busy"); + description = _t("The user you called is busy."); + } else { + title = _t("Call Failed"); + description = _t("The call could not be established"); + } + + Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { + title, description, + }); + } else if ( + hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting + ) { + Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { + title: _t("Answered Elsewhere"), + description: _t("The call was answered on another device."), + }); + } else if (oldState !== CallState.Fledgling && oldState !== CallState.Ringing) { + // don't play the end-call sound for calls that never got off the ground + this.play(AudioID.CallEnd); + } + + this.logCallStats(call, mappedRoomId); + break; + } + } + }; + private async logCallStats(call: MatrixCall, mappedRoomId: string) { const stats = await call.getCurrentCallStats(); logger.debug( @@ -641,6 +660,19 @@ export default class CallHandler extends EventEmitter { `Call state in ${mappedRoomId} changed to ${status}`, ); + const toastKey = getIncomingCallToastKey(call.callId); + if (status === CallState.Ringing) { + ToastStore.sharedInstance().addOrReplaceToast({ + key: toastKey, + priority: 100, + component: IncomingCallToast, + bodyClassName: "mx_IncomingCallToast", + props: { call }, + }); + } else { + ToastStore.sharedInstance().dismissToast(toastKey); + } + dis.dispatch({ action: 'call_state', room_id: mappedRoomId, @@ -723,9 +755,15 @@ export default class CallHandler extends EventEmitter { console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); const call = MatrixClientPeg.get().createCall(mappedRoomId); - console.log("Adding call for room ", roomId); - this.calls.set(roomId, call); - this.emit(CallHandlerEvent.CallsChanged, this.calls); + try { + this.addCallForRoom(roomId, call); + } catch (e) { + Modal.createTrackedDialog('Call Handler', 'Existing Call with user', ErrorDialog, { + title: _t('Already in call'), + description: _t("You're already in a call with this person."), + }); + return; + } if (transferee) { this.transferees[call.callId] = transferee; } @@ -738,25 +776,6 @@ export default class CallHandler extends EventEmitter { call.placeVoiceCall(); } else if (type === 'video') { call.placeVideoCall(); - } else if (type === PlaceCallType.ScreenSharing) { - const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString(); - if (screenCapErrorString) { - this.removeCallForRoom(roomId); - console.log("Can't capture screen: " + screenCapErrorString); - Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, { - title: _t('Unable to capture screen'), - description: screenCapErrorString, - }); - return; - } - - call.placeScreenSharingCall( - async (): Promise => { - const { finished } = Modal.createDialog(DesktopCapturerSourcePicker); - const [source] = await finished; - return source; - }, - ); } else { console.error("Unknown conf call type: " + type); } @@ -796,13 +815,8 @@ export default class CallHandler extends EventEmitter { return; } - if (this.getCallForRoom(room.roomId)) { - Modal.createTrackedDialog('Call Handler', 'Existing Call with user', ErrorDialog, { - title: _t('Already in call'), - description: _t("You're already in a call with this person."), - }); - return; - } + // We leave the check for whether there's already a call in this room until later, + // otherwise it can race. const members = room.getJoinedMembers(); if (members.length <= 1) { @@ -856,10 +870,11 @@ 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.emit(CallHandlerEvent.CallsChanged, this.calls); + + this.addCallForRoom(mappedRoomId, call); this.setCallListeners(call); + // Explicitly handle first state change + this.onCallStateChanged(call.state, null, call); // get ready to send encrypted events in the room, so if the user does answer // the call, we'll be ready to send. NB. This is the protocol-level room ID not @@ -870,6 +885,8 @@ export default class CallHandler extends EventEmitter { break; case 'hangup': case 'reject': + this.stopRingingIfPossible(this.calls.get(payload.room_id).callId); + if (!this.calls.get(payload.room_id)) { return; // no call to hangup } @@ -882,11 +899,15 @@ export default class CallHandler extends EventEmitter { // the hangup event away) break; case 'hangup_all': + this.stopRingingIfPossible(this.calls.get(payload.room_id).callId); + for (const call of this.calls.values()) { call.hangup(CallErrorCode.UserHangup, false); } break; case 'answer': { + this.stopRingingIfPossible(this.calls.get(payload.room_id).callId); + if (!this.calls.has(payload.room_id)) { return; // no call to answer } @@ -921,6 +942,12 @@ export default class CallHandler extends EventEmitter { } }; + private stopRingingIfPossible(callId: string): void { + this.silencedCalls.delete(callId); + if (this.areAnyCallsUnsilenced()) return; + this.pause(AudioID.Ring); + } + private async dialNumber(number: string) { const results = await this.pstnLookup(number); if (!results || results.length === 0 || !results[0].userid) { @@ -950,6 +977,8 @@ export default class CallHandler extends EventEmitter { action: 'view_room', room_id: roomId, }); + + await this.placeCall(roomId, PlaceCallType.Voice, null); } private async startTransferToPhoneNumber(call: MatrixCall, destination: string, consultFirst: boolean) { @@ -1029,14 +1058,10 @@ export default class CallHandler extends EventEmitter { // prevent double clicking the call button const room = MatrixClientPeg.get().getRoom(roomId); - const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI); - const hasJitsi = currentJitsiWidgets.length > 0 - || WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI); - if (hasJitsi) { - Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, { - title: _t('Call in Progress'), - description: _t('A call is currently being placed!'), - }); + const jitsiWidget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.JITSI.matches(app.type)); + if (jitsiWidget) { + // If there already is a Jitsi widget pin it + WidgetLayoutStore.instance.moveToContainer(room, jitsiWidget, Container.Top); return; } @@ -1124,4 +1149,21 @@ export default class CallHandler extends EventEmitter { messaging.transport.send(ElementWidgetActions.HangupCall, {}); }); } + + private addCallForRoom(roomId: string, call: MatrixCall, changedRooms = false): void { + if (this.calls.has(roomId)) { + console.log(`Couldn't add call to room ${roomId}: already have a call for this room`); + throw new Error("Already have a call for room " + roomId); + } + + console.log("setting call for room " + roomId); + this.calls.set(roomId, call); + + // Should we always emit CallsChanged too? + if (changedRooms) { + this.emit(CallHandlerEvent.CallChangeRoom, call); + } else { + this.emit(CallHandlerEvent.CallsChanged, this.calls); + } + } } diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index c5bcb226ff..40f8e307a5 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -39,6 +39,8 @@ import { import { IUpload } from "./models/IUpload"; import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials"; import { BlurhashEncoder } from "./BlurhashEncoder"; +import SettingsStore from "./settings/SettingsStore"; +import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics"; const MAX_WIDTH = 800; const MAX_HEIGHT = 600; @@ -209,6 +211,14 @@ async function loadImageElement(imageFile: File) { return { width, height, img }; } +// Minimum size for image files before we generate a thumbnail for them. +const IMAGE_SIZE_THRESHOLD_THUMBNAIL = 1 << 15; // 32KB +// Minimum size improvement for image thumbnails, if both are not met then don't bother uploading thumbnail. +const IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE = 1 << 16; // 1MB +const IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT = 0.1; // 10% +// We don't apply these thresholds to video thumbnails as a poster image is always useful +// and videos tend to be much larger. + /** * Read the metadata for an image file and create and upload a thumbnail of the image. * @@ -217,23 +227,33 @@ async function loadImageElement(imageFile: File) { * @param {File} imageFile The image to read and thumbnail. * @return {Promise} A promise that resolves with the attachment info. */ -function infoForImageFile(matrixClient, roomId, imageFile) { +async function infoForImageFile(matrixClient: MatrixClient, roomId: string, imageFile: File) { let thumbnailType = "image/png"; if (imageFile.type === "image/jpeg") { thumbnailType = "image/jpeg"; } - let imageInfo; - return loadImageElement(imageFile).then((r) => { - return createThumbnail(r.img, r.width, r.height, thumbnailType); - }).then((result) => { - imageInfo = result.info; - return uploadFile(matrixClient, roomId, result.thumbnail); - }).then((result) => { - imageInfo.thumbnail_url = result.url; - imageInfo.thumbnail_file = result.file; + const imageElement = await loadImageElement(imageFile); + + const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType); + const imageInfo = result.info; + + // we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from. + const sizeDifference = imageFile.size - imageInfo.thumbnail_info.size; + if ( + imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL || // image is small enough already + (sizeDifference <= IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE && // thumbnail is not sufficiently smaller than original + sizeDifference <= (imageFile.size * IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT)) + ) { + delete imageInfo["thumbnail_info"]; return imageInfo; - }); + } + + const uploadResult = await uploadFile(matrixClient, roomId, result.thumbnail); + + imageInfo["thumbnail_url"] = uploadResult.url; + imageInfo["thumbnail_file"] = uploadResult.file; + return imageInfo; } /** @@ -521,6 +541,10 @@ export default class ContentMessages { msgtype: "", // set later }; + if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) { + decorateStartSendingTime(content); + } + // if we have a mime type for the file, add it to the message metadata if (file.type) { content.info.mimetype = file.type; @@ -596,6 +620,11 @@ export default class ContentMessages { }).then(function() { if (upload.canceled) throw new UploadCanceledError(); const prom = matrixClient.sendMessage(roomId, content); + if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) { + prom.then(resp => { + sendRoundTripMetric(matrixClient, roomId, resp.event_id); + }); + } CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, false, content); return prom; }, function(err) { diff --git a/src/DateUtils.ts b/src/DateUtils.ts index e4a1175d88..c81099b893 100644 --- a/src/DateUtils.ts +++ b/src/DateUtils.ts @@ -123,6 +123,31 @@ export function formatTime(date: Date, showTwelveHour = false): string { return pad(date.getHours()) + ':' + pad(date.getMinutes()); } +export function formatCallTime(delta: Date): string { + const hours = delta.getUTCHours(); + const minutes = delta.getUTCMinutes(); + const seconds = delta.getUTCSeconds(); + + let output = ""; + if (hours) output += `${hours}h `; + if (minutes || output) output += `${minutes}m `; + if (seconds || output) output += `${seconds}s`; + + return output; +} + +export function formatSeconds(inSeconds: number): string { + const hours = Math.floor(inSeconds / (60 * 60)).toFixed(0).padStart(2, '0'); + const minutes = Math.floor((inSeconds % (60 * 60)) / 60).toFixed(0).padStart(2, '0'); + const seconds = Math.floor(((inSeconds % (60 * 60)) % 60)).toFixed(0).padStart(2, '0'); + + let output = ""; + if (hours !== "00") output += `${hours}:`; + output += `${minutes}:${seconds}`; + + return output; +} + const MILLIS_IN_DAY = 86400000; export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean { if (!nextEventDate || !prevEventDate) { diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 51c624e3c3..1a551d7813 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -100,6 +100,7 @@ export default class DeviceListener { * @param {String[]} deviceIds List of device IDs to dismiss notifications for */ async dismissUnverifiedSessions(deviceIds: Iterable) { + console.log("Dismissing unverified sessions: " + Array.from(deviceIds).join(',')); for (const d of deviceIds) { this.dismissed.add(d); } @@ -285,6 +286,9 @@ export default class DeviceListener { } } + console.log("Old unverified sessions: " + Array.from(oldUnverifiedDeviceIds).join(',')); + console.log("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(',')); + // Display or hide the batch toast for old unverified sessions if (oldUnverifiedDeviceIds.size > 0) { showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds); diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index af5d2b3019..2eee5214af 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -57,7 +57,33 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; -export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix']; +export const PERMITTED_URL_SCHEMES = [ + "bitcoin", + "ftp", + "geo", + "http", + "https", + "im", + "irc", + "ircs", + "magnet", + "mailto", + "matrix", + "mms", + "news", + "nntp", + "openpgp4fpr", + "sip", + "sftp", + "sms", + "smsto", + "ssh", + "tel", + "urn", + "webcal", + "wtai", + "xmpp", +]; const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/; diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index e91e1d72cf..ffece510de 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -146,23 +146,23 @@ export default class IdentityAuthClient { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '', QuestionDialog, { - title: _t("Identity server has no terms of service"), - description: ( -
-

{ _t( - "This action requires accessing the default identity server " + + title: _t("Identity server has no terms of service"), + description: ( +

+

{ _t( + "This action requires accessing the default identity server " + " to validate an email address or phone number, " + "but the server does not have any terms of service.", {}, - { - server: () => { abbreviateUrl(identityServerUrl) }, - }, - ) }

-

{ _t( - "Only continue if you trust the owner of the server.", - ) }

-
- ), - button: _t("Trust"), + { + server: () => { abbreviateUrl(identityServerUrl) }, + }, + ) }

+

{ _t( + "Only continue if you trust the owner of the server.", + ) }

+
+ ), + button: _t("Trust"), }); const [confirmed] = await finished; if (confirmed) { diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts index b2f70abff7..6169f431f4 100644 --- a/src/KeyBindingsDefaults.ts +++ b/src/KeyBindingsDefaults.ts @@ -161,31 +161,29 @@ const messageComposerBindings = (): KeyBinding[] => { const autocompleteBindings = (): KeyBinding[] => { return [ { - action: AutocompleteAction.CompleteOrNextSelection, + action: AutocompleteAction.ForceComplete, keyCombo: { key: Key.TAB, }, }, { - action: AutocompleteAction.CompleteOrNextSelection, + action: AutocompleteAction.ForceComplete, keyCombo: { key: Key.TAB, ctrlKey: true, }, }, { - action: AutocompleteAction.CompleteOrPrevSelection, + action: AutocompleteAction.Complete, keyCombo: { - key: Key.TAB, - shiftKey: true, + key: Key.ENTER, }, }, { - action: AutocompleteAction.CompleteOrPrevSelection, + action: AutocompleteAction.Complete, keyCombo: { - key: Key.TAB, + key: Key.ENTER, ctrlKey: true, - shiftKey: true, }, }, { diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts index 4225d2f449..3a893e2ec8 100644 --- a/src/KeyBindingsManager.ts +++ b/src/KeyBindingsManager.ts @@ -52,13 +52,11 @@ export enum MessageComposerAction { /** Actions for text editing autocompletion */ export enum AutocompleteAction { - /** - * Select previous selection or, if the autocompletion window is not shown, open the window and select the first - * selection. - */ - CompleteOrPrevSelection = 'ApplySelection', - /** Select next selection or, if the autocompletion window is not shown, open it and select the first selection */ - CompleteOrNextSelection = 'CompleteOrNextSelection', + /** Accepts chosen autocomplete selection */ + Complete = 'Complete', + /** Accepts chosen autocomplete selection or, + * if the autocompletion window is not shown, open the window and select the first selection */ + ForceComplete = 'ForceComplete', /** Move to the previous autocomplete selection */ PrevSelection = 'PrevSelection', /** Move to the next autocomplete selection */ diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 410124a637..e48fd52cb1 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -48,6 +48,7 @@ import { Jitsi } from "./widgets/Jitsi"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform"; import ThreepidInviteStore from "./stores/ThreepidInviteStore"; import CountlyAnalytics from "./CountlyAnalytics"; +import { PosthogAnalytics } from "./PosthogAnalytics"; import CallHandler from './CallHandler'; import LifecycleCustomisations from "./customisations/Lifecycle"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; @@ -573,6 +574,8 @@ async function doSetLoggedIn( await abortLogin(); } + PosthogAnalytics.instance.updateAnonymityFromSettings(credentials.userId); + Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl); MatrixClientPeg.replaceUsingCreds(credentials); @@ -700,6 +703,8 @@ export function logout(): void { CountlyAnalytics.instance.enable(/* anonymous = */ true); } + PosthogAnalytics.instance.logout(); + if (MatrixClientPeg.get().isGuest()) { // logout doesn't work for guest sessions // Also we sometimes want to re-log in a guest session if we abort the login. diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index f43351aab2..7d0ff560b7 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -213,6 +213,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.pendingEventOrdering = PendingEventOrdering.Detached; opts.lazyLoadMembers = true; opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours + opts.experimentalThreadSupport = SettingsStore.getValue("feature_thread"); // Connect the matrix client to the dispatcher and setting handlers MatrixActionCreators.start(this.matrixClient); diff --git a/src/PasswordReset.js b/src/PasswordReset.ts similarity index 89% rename from src/PasswordReset.js rename to src/PasswordReset.ts index 88ae00d088..76f54de245 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.ts @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { createClient } from 'matrix-js-sdk/src/matrix'; +import { createClient, IRequestTokenResponse, MatrixClient } from 'matrix-js-sdk/src/matrix'; import { _t } from './languageHandler'; /** @@ -26,12 +26,18 @@ import { _t } from './languageHandler'; * API on the homeserver in question with the new password. */ export default class PasswordReset { + private client: MatrixClient; + private clientSecret: string; + private identityServerDomain: string; + private password: string; + private sessionId: string; + /** * Configure the endpoints for password resetting. * @param {string} homeserverUrl The URL to the HS which has the account to reset. * @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping. */ - constructor(homeserverUrl, identityUrl) { + constructor(homeserverUrl: string, identityUrl: string) { this.client = createClient({ baseUrl: homeserverUrl, idBaseUrl: identityUrl, @@ -47,7 +53,7 @@ export default class PasswordReset { * @param {string} newPassword The new password for the account. * @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked(). */ - resetPassword(emailAddress, newPassword) { + public resetPassword(emailAddress: string, newPassword: string): Promise { this.password = newPassword; return this.client.requestPasswordEmailToken(emailAddress, this.clientSecret, 1).then((res) => { this.sessionId = res.sid; @@ -69,7 +75,7 @@ export default class PasswordReset { * with a "message" property which contains a human-readable message detailing why * the reset failed, e.g. "There is no mapped matrix user ID for the given email address". */ - async checkEmailLinkClicked() { + public async checkEmailLinkClicked(): Promise { const creds = { sid: this.sessionId, client_secret: this.clientSecret, diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts new file mode 100644 index 0000000000..860a155aff --- /dev/null +++ b/src/PosthogAnalytics.ts @@ -0,0 +1,355 @@ +/* +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 posthog, { PostHog } from 'posthog-js'; +import PlatformPeg from './PlatformPeg'; +import SdkConfig from './SdkConfig'; +import SettingsStore from './settings/SettingsStore'; + +/* Posthog analytics tracking. + * + * Anonymity behaviour is as follows: + * + * - If Posthog isn't configured in `config.json`, events are not sent. + * - If [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is + * enabled, events are not sent (this detection is built into posthog and turned on via the + * `respect_dnt` flag being passed to `posthog.init`). + * - If the `feature_pseudonymous_analytics_opt_in` labs flag is `true`, track pseudonomously, i.e. + * hash all matrix identifiers in tracking events (user IDs, room IDs etc) using SHA-256. + * - Otherwise, if the existing `analyticsOptIn` flag is `true`, track anonymously, i.e. + * redact all matrix identifiers in tracking events. + * - If both flags are false or not set, events are not sent. + */ + +interface IEvent { + // The event name that will be used by PostHog. Event names should use snake_case. + eventName: string; + + // The properties of the event that will be stored in PostHog. This is just a placeholder, + // extending interfaces must override this with a concrete definition to do type validation. + properties: {}; +} + +export enum Anonymity { + Disabled, + Anonymous, + Pseudonymous +} + +// If an event extends IPseudonymousEvent, the event contains pseudonymous data +// that won't be sent unless the user has explicitly consented to pseudonymous tracking. +// For example, it might contain hashed user IDs or room IDs. +// Such events will be automatically dropped if PosthogAnalytics.anonymity isn't set to Pseudonymous. +export interface IPseudonymousEvent extends IEvent {} + +// If an event extends IAnonymousEvent, the event strictly contains *only* anonymous data; +// i.e. no identifiers that can be associated with the user. +export interface IAnonymousEvent extends IEvent {} + +export interface IRoomEvent extends IPseudonymousEvent { + hashedRoomId: string; +} + +interface IPageView extends IAnonymousEvent { + eventName: "$pageview"; + properties: { + durationMs?: number; + screen?: string; + }; +} + +const hashHex = async (input: string): Promise => { + const buf = new TextEncoder().encode(input); + const digestBuf = await window.crypto.subtle.digest("sha-256", buf); + return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join(""); +}; + +const whitelistedScreens = new Set([ + "register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory", + "start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group", +]); + +export async function getRedactedCurrentLocation( + origin: string, + hash: string, + pathname: string, + anonymity: Anonymity, +): Promise { + // Redact PII from the current location. + // If anonymous is true, redact entirely, if false, substitute it with a hash. + // For known screens, assumes a URL structure of //might/be/pii + if (origin.startsWith('file://')) { + pathname = "//"; + } + + let hashStr; + if (hash == "") { + hashStr = ""; + } else { + let [beforeFirstSlash, screen, ...parts] = hash.split("/"); + + if (!whitelistedScreens.has(screen)) { + screen = ""; + } + + for (let i = 0; i < parts.length; i++) { + parts[i] = anonymity === Anonymity.Anonymous ? `` : await hashHex(parts[i]); + } + + hashStr = `${beforeFirstSlash}/${screen}/${parts.join("/")}`; + } + return origin + pathname + hashStr; +} + +interface PlatformProperties { + appVersion: string; + appPlatform: string; +} + +export class PosthogAnalytics { + /* Wrapper for Posthog analytics. + * 3 modes of anonymity are supported, governed by this.anonymity + * - Anonymity.Disabled means *no data* is passed to posthog + * - Anonymity.Anonymous means all identifers will be redacted before being passed to posthog + * - Anonymity.Pseudonymous means all identifiers will be hashed via SHA-256 before being passed + * to Posthog + * + * To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity(). + * + * To pass an event to Posthog: + * + * 1. Declare a type for the event, extending IAnonymousEvent, IPseudonymousEvent or IRoomEvent. + * 2. Call the appropriate track*() method. Pseudonymous events will be dropped when anonymity is + * Anonymous or Disabled; Anonymous events will be dropped when anonymity is Disabled. + */ + + private anonymity = Anonymity.Disabled; + // set true during the constructor if posthog config is present, otherwise false + private enabled = false; + private static _instance = null; + private platformSuperProperties = {}; + + public static get instance(): PosthogAnalytics { + if (!this._instance) { + this._instance = new PosthogAnalytics(posthog); + } + return this._instance; + } + + constructor(private readonly posthog: PostHog) { + const posthogConfig = SdkConfig.get()["posthog"]; + if (posthogConfig) { + this.posthog.init(posthogConfig.projectApiKey, { + api_host: posthogConfig.apiHost, + autocapture: false, + mask_all_text: true, + mask_all_element_attributes: true, + // This only triggers on page load, which for our SPA isn't particularly useful. + // Plus, the .capture call originating from somewhere in posthog makes it hard + // to redact URLs, which requires async code. + // + // To raise this manually, just call .capture("$pageview") or posthog.capture_pageview. + capture_pageview: false, + sanitize_properties: this.sanitizeProperties, + respect_dnt: true, + }); + this.enabled = true; + } else { + this.enabled = false; + } + } + + private sanitizeProperties = (properties: posthog.Properties): posthog.Properties => { + // Callback from posthog to sanitize properties before sending them to the server. + // + // Here we sanitize posthog's built in properties which leak PII e.g. url reporting. + // See utils.js _.info.properties in posthog-js. + + // Replace the $current_url with a redacted version. + // $redacted_current_url is injected by this class earlier in capture(), as its generation + // is async and can't be done in this non-async callback. + if (!properties['$redacted_current_url']) { + console.log("$redacted_current_url not set in sanitizeProperties, will drop $current_url entirely"); + } + properties['$current_url'] = properties['$redacted_current_url']; + delete properties['$redacted_current_url']; + + if (this.anonymity == Anonymity.Anonymous) { + // drop referrer information for anonymous users + properties['$referrer'] = null; + properties['$referring_domain'] = null; + properties['$initial_referrer'] = null; + properties['$initial_referring_domain'] = null; + + // drop device ID, which is a UUID persisted in local storage + properties['$device_id'] = null; + } + + return properties; + }; + + private static getAnonymityFromSettings(): Anonymity { + // determine the current anonymity level based on current user settings + + // "Send anonymous usage data which helps us improve Element. This will use a cookie." + const analyticsOptIn = SettingsStore.getValue("analyticsOptIn", null, true); + + // (proposed wording) "Send pseudonymous usage data which helps us improve Element. This will use a cookie." + // + // TODO: Currently, this is only a labs flag, for testing purposes. + const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymous_analytics_opt_in", null, true); + + let anonymity; + if (pseudonumousOptIn) { + anonymity = Anonymity.Pseudonymous; + } else if (analyticsOptIn) { + anonymity = Anonymity.Anonymous; + } else { + anonymity = Anonymity.Disabled; + } + + return anonymity; + } + + private registerSuperProperties(properties: posthog.Properties) { + if (this.enabled) { + this.posthog.register(properties); + } + } + + private static async getPlatformProperties(): Promise { + const platform = PlatformPeg.get(); + let appVersion; + try { + appVersion = await platform.getAppVersion(); + } catch (e) { + // this happens if no version is set i.e. in dev + appVersion = "unknown"; + } + + return { + appVersion, + appPlatform: platform.getHumanReadableName(), + }; + } + + private async capture(eventName: string, properties: posthog.Properties) { + if (!this.enabled) { + return; + } + const { origin, hash, pathname } = window.location; + properties['$redacted_current_url'] = await getRedactedCurrentLocation( + origin, hash, pathname, this.anonymity); + this.posthog.capture(eventName, properties); + } + + public isEnabled(): boolean { + return this.enabled; + } + + public setAnonymity(anonymity: Anonymity): void { + // Update this.anonymity. + // This is public for testing purposes, typically you want to call updateAnonymityFromSettings + // to ensure this value is in step with the user's settings. + if (this.enabled && (anonymity == Anonymity.Disabled || anonymity == Anonymity.Anonymous)) { + // when transitioning to Disabled or Anonymous ensure we clear out any prior state + // set in posthog e.g. distinct ID + this.posthog.reset(); + // Restore any previously set platform super properties + this.registerSuperProperties(this.platformSuperProperties); + } + this.anonymity = anonymity; + } + + public async identifyUser(userId: string): Promise { + if (this.anonymity == Anonymity.Pseudonymous) { + this.posthog.identify(await hashHex(userId)); + } + } + + public getAnonymity(): Anonymity { + return this.anonymity; + } + + public logout(): void { + if (this.enabled) { + this.posthog.reset(); + } + this.setAnonymity(Anonymity.Anonymous); + } + + public async trackPseudonymousEvent( + eventName: E["eventName"], + properties: E["properties"] = {}, + ) { + if (this.anonymity == Anonymity.Anonymous || this.anonymity == Anonymity.Disabled) return; + await this.capture(eventName, properties); + } + + public async trackAnonymousEvent( + eventName: E["eventName"], + properties: E["properties"] = {}, + ): Promise { + if (this.anonymity == Anonymity.Disabled) return; + await this.capture(eventName, properties); + } + + public async trackRoomEvent( + eventName: E["eventName"], + roomId: string, + properties: Omit, + ): Promise { + const updatedProperties = { + ...properties, + hashedRoomId: roomId ? await hashHex(roomId) : null, + }; + await this.trackPseudonymousEvent(eventName, updatedProperties); + } + + public async trackPageView(durationMs: number): Promise { + const hash = window.location.hash; + + let screen = null; + const split = hash.split("/"); + if (split.length >= 2) { + screen = split[1]; + } + + await this.trackAnonymousEvent("$pageview", { + durationMs, + screen, + }); + } + + public async updatePlatformSuperProperties(): Promise { + // Update super properties in posthog with our platform (app version, platform). + // These properties will be subsequently passed in every event. + // + // This only needs to be done once per page lifetime. Note that getPlatformProperties + // is async and can involve a network request if we are running in a browser. + this.platformSuperProperties = await PosthogAnalytics.getPlatformProperties(); + this.registerSuperProperties(this.platformSuperProperties); + } + + public async updateAnonymityFromSettings(userId?: string): Promise { + // Update this.anonymity based on the user's analytics opt-in settings + // Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous + this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings()); + if (userId && this.getAnonymity() == Anonymity.Pseudonymous) { + await this.identifyUser(userId); + } + } +} diff --git a/src/Registration.js b/src/Registration.js index 70dcd38454..c59d244149 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -51,10 +51,15 @@ export async function startAnyRegistrationFlow(options) { description: _t("Use your account or create a new one to continue."), button: _t("Create Account"), extraButtons: [ - , + , ], onFinished: (proceed) => { if (proceed) { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 9f5ac83a56..902c82fff8 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -34,7 +34,6 @@ import { getAddressType } from './UserAddress'; import { abbreviateUrl } from './utils/UrlUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks"; -import { inviteUsersToRoom } from "./RoomInvite"; import { WidgetType } from "./widgets/WidgetType"; import { Jitsi } from "./widgets/Jitsi"; import { parseFragment as parseHtml, Element as ChildElement } from "parse5"; @@ -49,8 +48,8 @@ import { UIFeature } from "./settings/UIFeature"; import { CHAT_EFFECTS } from "./effects"; import CallHandler from "./CallHandler"; import { guessAndSetDMRoom } from "./Rooms"; +import { upgradeRoom } from './utils/RoomUpgrade'; import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog'; -import ErrorDialog from './components/views/dialogs/ErrorDialog'; import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog'; import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarningDialog"; import InfoDialog from "./components/views/dialogs/InfoDialog"; @@ -245,21 +244,6 @@ export const Commands = [ }, category: CommandCategories.messages, }), - new Command({ - command: 'ddg', - args: '', - description: _td('Searches DuckDuckGo for results'), - runFn: function() { - // TODO Don't explain this away, actually show a search UI here. - Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, { - title: _t('/ddg is not a command'), - description: _t('To use it, just wait for autocomplete results to load and tab through them.'), - }); - return success(); - }, - category: CommandCategories.actions, - hideCompletionAfterSpace: true, - }), new Command({ command: 'upgraderoom', args: '', @@ -277,50 +261,8 @@ export const Commands = [ /*isPriority=*/false, /*isStatic=*/true); return success(finished.then(async ([resp]) => { - if (!resp.continue) return; - - let checkForUpgradeFn; - try { - const upgradePromise = cli.upgradeRoom(roomId, args); - - // We have to wait for the js-sdk to give us the room back so - // we can more effectively abuse the MultiInviter behaviour - // which heavily relies on the Room object being available. - if (resp.invite) { - checkForUpgradeFn = async (newRoom) => { - // The upgradePromise should be done by the time we await it here. - const { replacement_room: newRoomId } = await upgradePromise; - if (newRoom.roomId !== newRoomId) return; - - const toInvite = [ - ...room.getMembersWithMembership("join"), - ...room.getMembersWithMembership("invite"), - ].map(m => m.userId).filter(m => m !== cli.getUserId()); - - if (toInvite.length > 0) { - // Errors are handled internally to this function - await inviteUsersToRoom(newRoomId, toInvite); - } - - cli.removeListener('Room', checkForUpgradeFn); - }; - cli.on('Room', checkForUpgradeFn); - } - - // We have to await after so that the checkForUpgradesFn has a proper reference - // to the new room's ID. - await upgradePromise; - } catch (e) { - console.error(e); - - if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn); - - Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, { - title: _t('Error upgrading room'), - description: _t( - 'Double check that your server supports the room version chosen and try again.'), - }); - } + if (!resp?.continue) return; + await upgradeRoom(room, args, resp.invite); })); } return reject(this.getUsage()); diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 7bad8eb50e..0e9dc1cf15 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -25,11 +25,44 @@ 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"; +import { MatrixClientPeg } from "./MatrixClientPeg"; // These functions are frequently used just to check whether an event has // any text to display at all. For this reason they return deferred values // to avoid the expense of looking up translations when they're not needed. +function textForCallInviteEvent(event: MatrixEvent): () => string | null { + const getSenderName = () => event.sender ? event.sender.name : _t('Someone'); + // FIXME: Find a better way to determine this from the event? + let isVoice = true; + if (event.getContent().offer && event.getContent().offer.sdp && + event.getContent().offer.sdp.indexOf('m=video') !== -1) { + isVoice = false; + } + const isSupported = MatrixClientPeg.get().supportsVoip(); + + // This ladder could be reduced down to a couple string variables, however other languages + // can have a hard time translating those strings. In an effort to make translations easier + // and more accurate, we break out the string-based variables to a couple booleans. + if (isVoice && isSupported) { + return () => _t("%(senderName)s placed a voice call.", { + senderName: getSenderName(), + }); + } else if (isVoice && !isSupported) { + return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", { + senderName: getSenderName(), + }); + } else if (!isVoice && isSupported) { + return () => _t("%(senderName)s placed a video call.", { + senderName: getSenderName(), + }); + } else if (!isVoice && !isSupported) { + return () => _t("%(senderName)s placed a video call. (not supported by this browser)", { + senderName: getSenderName(), + }); + } +} + function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean): () => string | null { // XXX: SYJS-16 "sender is sometimes null for join messages" const senderName = ev.sender ? ev.sender.name : ev.getSender(); @@ -408,6 +441,15 @@ function textForPowerEvent(event: MatrixEvent): () => string | null { }); } +const onPinnedOrUnpinnedMessageClick = (messageId: string, roomId: string): void => { + defaultDispatcher.dispatch({ + action: 'view_room', + event_id: messageId, + highlighted: true, + room_id: roomId, + }); +}; + const onPinnedMessagesClick = (): void => { defaultDispatcher.dispatch({ action: Action.SetRightPanelPhase, @@ -419,17 +461,77 @@ const onPinnedMessagesClick = (): void => { 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(); + const roomId = event.getRoomId(); + + const pinned = event.getContent().pinned ?? []; + const previouslyPinned = event.getPrevContent().pinned ?? []; + const newlyPinned = pinned.filter(item => previouslyPinned.indexOf(item) < 0); + const newlyUnpinned = previouslyPinned.filter(item => pinned.indexOf(item) < 0); + + if (newlyPinned.length === 1 && newlyUnpinned.length === 0) { + // A single message was pinned, include a link to that message. + if (allowJSX) { + const messageId = newlyPinned.pop(); + + return () => ( + + { _t( + "%(senderName)s pinned a message to this room. See all pinned messages.", + { senderName }, + { + "a": (sub) => + onPinnedOrUnpinnedMessageClick(messageId, roomId)}> + { sub } + , + "b": (sub) => + + { sub } + , + }, + ) } + + ); + } + + return () => _t("%(senderName)s pinned a message to this room. See all pinned messages.", { senderName }); + } + + if (newlyUnpinned.length === 1 && newlyPinned.length === 0) { + // A single message was unpinned, include a link to that message. + if (allowJSX) { + const messageId = newlyUnpinned.pop(); + + return () => ( + + { _t( + "%(senderName)s unpinned a message from this room. See all pinned messages.", + { senderName }, + { + "a": (sub) => + onPinnedOrUnpinnedMessageClick(messageId, roomId)}> + { sub } + , + "b": (sub) => + + { sub } + , + }, + ) } + + ); + } + + return () => _t("%(senderName)s unpinned a message from this room. See all pinned messages.", { senderName }); + } if (allowJSX) { return () => ( - { - _t( - "%(senderName)s changed the pinned messages for the room.", - { senderName }, - { "a": (sub) => { sub } }, - ) - } + { _t( + "%(senderName)s changed the pinned messages for the room.", + { senderName }, + { "a": (sub) => { sub } }, + ) } ); } @@ -567,6 +669,7 @@ interface IHandlers { const handlers: IHandlers = { 'm.room.message': textForMessageEvent, + 'm.call.invite': textForCallInviteEvent, }; const stateHandlers: IHandlers = { diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx index 9cc7b60c99..c66984191f 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -163,7 +163,7 @@ const shortcuts: Record = { modifiers: [Modifiers.SHIFT], key: Key.PAGE_UP, }], - description: _td("Jump to oldest unread message"), + description: _td("Jump to oldest unread message"), }, { keybinds: [{ modifiers: [CMD_OR_CTRL, Modifiers.SHIFT], diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 87f525bdfc..68e10049fd 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -150,13 +150,14 @@ const reducer = (state: IState, action: IAction) => { interface IProps { handleHomeEnd?: boolean; + handleUpDown?: boolean; children(renderProps: { onKeyDownHandler(ev: React.KeyboardEvent); }); onKeyDown?(ev: React.KeyboardEvent, state: IState); } -export const RovingTabIndexProvider: React.FC = ({ children, handleHomeEnd, onKeyDown }) => { +export const RovingTabIndexProvider: React.FC = ({ children, handleHomeEnd, handleUpDown, onKeyDown }) => { const [state, dispatch] = useReducer>(reducer, { activeRef: null, refs: [], @@ -167,21 +168,50 @@ export const RovingTabIndexProvider: React.FC = ({ children, handleHomeE const onKeyDownHandler = useCallback((ev) => { let handled = false; // Don't interfere with input default keydown behaviour - if (handleHomeEnd && ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") { + if (ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") { // check if we actually have any items switch (ev.key) { case Key.HOME: - handled = true; - // move focus to first item - if (context.state.refs.length > 0) { - context.state.refs[0].current.focus(); + if (handleHomeEnd) { + handled = true; + // move focus to first item + if (context.state.refs.length > 0) { + context.state.refs[0].current.focus(); + } } break; + case Key.END: - handled = true; - // move focus to last item - if (context.state.refs.length > 0) { - context.state.refs[context.state.refs.length - 1].current.focus(); + if (handleHomeEnd) { + handled = true; + // move focus to last item + if (context.state.refs.length > 0) { + context.state.refs[context.state.refs.length - 1].current.focus(); + } + } + break; + + case Key.ARROW_UP: + if (handleUpDown) { + handled = true; + if (context.state.refs.length > 0) { + const idx = context.state.refs.indexOf(context.state.activeRef); + if (idx > 0) { + context.state.refs[idx - 1].current.focus(); + } + } + } + break; + + case Key.ARROW_DOWN: + if (handleUpDown) { + handled = true; + if (context.state.refs.length > 0) { + const idx = context.state.refs.indexOf(context.state.activeRef); + if (idx < context.state.refs.length - 1) { + context.state.refs[idx + 1].current.focus(); + } + } } break; } @@ -193,7 +223,7 @@ export const RovingTabIndexProvider: React.FC = ({ children, handleHomeE } else if (onKeyDown) { return onKeyDown(ev, context.state); } - }, [context.state, onKeyDown, handleHomeEnd]); + }, [context.state, onKeyDown, handleHomeEnd, handleUpDown]); return { children({ onKeyDownHandler }) } diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js index 412194ab43..2cef1c0e41 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js @@ -269,7 +269,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
{ _t("Advanced") } - + { _t("Set up with a Security Key") }
diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js index aa78d68830..641df4f897 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -474,7 +474,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { outlined >
- + { _t("Generate a Security Key") }
{ _t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }
@@ -493,7 +493,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { outlined >
- + { _t("Enter a Security Phrase") }
{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }
@@ -701,7 +701,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { { this._recoveryKey.encodedPrivateKey }
- diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js index 0435d81968..dbed9f3968 100644 --- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js @@ -148,8 +148,12 @@ export default class ExportE2eKeysDialog extends React.Component {
-
@@ -161,8 +165,10 @@ export default class ExportE2eKeysDialog extends React.Component {
-
diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js index 6017d07047..0936ad696d 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js @@ -174,7 +174,10 @@ export default class ImportE2eKeysDialog extends React.Component {
- + + ; +}; + +const SpaceHierarchy = ({ + space, + initialText = "", + showRoom, + additionalButtons, +}: IProps) => { + const cli = useContext(MatrixClientContext); + const [query, setQuery] = useState(initialText); + + const [selected, setSelected] = useState(new Map>()); // Map> + + const { loading, rooms, hierarchy, loadMore } = useSpaceSummary(space); + + const filteredRoomSet = useMemo>(() => { + if (!rooms.length) return new Set(); + const lcQuery = query.toLowerCase().trim(); + if (!lcQuery) return new Set(rooms); + + const directMatches = rooms.filter(r => { + return r.name?.toLowerCase().includes(lcQuery) || r.topic?.toLowerCase().includes(lcQuery); + }); + + // Walk back up the tree to find all parents of the direct matches to show their place in the hierarchy + const visited = new Set(); + const queue = [...directMatches.map(r => r.room_id)]; + while (queue.length) { + const roomId = queue.pop(); + visited.add(roomId); + hierarchy.backRefs.get(roomId)?.forEach(parentId => { + if (!visited.has(parentId)) { + queue.push(parentId); + } + }); + } + + return new Set(rooms.filter(r => visited.has(r.room_id))); + }, [rooms, hierarchy, query]); + + const [error, setError] = useState(""); + + const loaderRef = useIntersectionObserver(loadMore); + + if (!loading && hierarchy.noSupport) { + return

{ _t("Your server does not support showing space hierarchies.") }

; + } + + const onKeyDown = (ev: KeyboardEvent, state: IState): void => { + if (ev.key === Key.ARROW_DOWN && ev.currentTarget.classList.contains("mx_SpaceHierarchy_search")) { + state.refs[0]?.current?.focus(); + } + }; + + const onToggleClick = (parentId: string, childId: string): void => { + setError(""); + if (!selected.has(parentId)) { + setSelected(new Map(selected.set(parentId, new Set([childId])))); + return; + } + + const parentSet = selected.get(parentId); + if (!parentSet.has(childId)) { + setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId])))); + return; + } + + parentSet.delete(childId); + setSelected(new Map(selected.set(parentId, new Set(parentSet)))); + }; + + return + { ({ onKeyDownHandler }) => { + let content: JSX.Element; + let loader: JSX.Element; + + if (loading && !rooms.length) { + content = ; + } else { + const hasPermissions = space?.getMyMembership() === "join" && + space.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); + + let results: JSX.Element; + if (filteredRoomSet.size) { + results = <> + { + showRoom(cli, hierarchy, roomId, autoJoin); + }} + /> + ; + + if (hierarchy.canLoadMore) { + loader =
+ +
; + } + } else { + results =
+

{ _t("No results found") }

+
{ _t("You may want to try a different search or check for typos.") }
+
; + } + + content = <> +
+

{ query.trim() ? _t("Results") : _t("Rooms and spaces") }

+ + { additionalButtons } + { hasPermissions && ( + + ) } + +
+ { error &&
+ { error } +
} +
    + { results } +
+ { loader } + ; + } + + return <> + + + { content } + ; + } } +
; +}; + +export default SpaceHierarchy; diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx deleted file mode 100644 index 038c1df514..0000000000 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ /dev/null @@ -1,642 +0,0 @@ -/* -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, { ReactNode, useMemo, useState } from "react"; -import { Room } from "matrix-js-sdk/src/models/room"; -import { MatrixClient } from "matrix-js-sdk/src/client"; -import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; -import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces"; -import classNames from "classnames"; -import { sortBy } from "lodash"; - -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import dis from "../../dispatcher/dispatcher"; -import { _t } from "../../languageHandler"; -import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; -import BaseDialog from "../views/dialogs/BaseDialog"; -import Spinner from "../views/elements/Spinner"; -import SearchBox from "./SearchBox"; -import RoomAvatar from "../views/avatars/RoomAvatar"; -import RoomName from "../views/elements/RoomName"; -import { useAsyncMemo } from "../../hooks/useAsyncMemo"; -import { EnhancedMap } from "../../utils/maps"; -import StyledCheckbox from "../views/elements/StyledCheckbox"; -import AutoHideScrollbar from "./AutoHideScrollbar"; -import BaseAvatar from "../views/avatars/BaseAvatar"; -import { mediaFromMxc } from "../../customisations/Media"; -import InfoTooltip from "../views/elements/InfoTooltip"; -import TextWithTooltip from "../views/elements/TextWithTooltip"; -import { useStateToggle } from "../../hooks/useStateToggle"; -import { getChildOrder } from "../../stores/SpaceStore"; -import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; -import { linkifyElement } from "../../HtmlUtils"; -import { getDisplayAliasForAliasSet } from "../../Rooms"; - -interface IHierarchyProps { - space: Room; - initialText?: string; - refreshToken?: any; - additionalButtons?: ReactNode; - showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void; -} - -interface ITileProps { - room: ISpaceSummaryRoom; - suggested?: boolean; - selected?: boolean; - numChildRooms?: number; - hasPermissions?: boolean; - onViewRoomClick(autoJoin: boolean): void; - onToggleClick?(): void; -} - -const Tile: React.FC = ({ - room, - suggested, - selected, - hasPermissions, - onToggleClick, - onViewRoomClick, - numChildRooms, - children, -}) => { - const cli = MatrixClientPeg.get(); - const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null; - const name = joinedRoom?.name || room.name || room.canonical_alias || room.aliases?.[0] - || (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room")); - - const [showChildren, toggleShowChildren] = useStateToggle(true); - - const onPreviewClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - onViewRoomClick(false); - }; - const onJoinClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - onViewRoomClick(true); - }; - - let button; - if (joinedRoom) { - button = - { _t("View") } - ; - } else if (onJoinClick) { - button = - { _t("Join") } - ; - } - - let checkbox; - if (onToggleClick) { - if (hasPermissions) { - checkbox = ; - } else { - checkbox = { ev.stopPropagation(); }} - > - - ; - } - } - - let avatar; - if (joinedRoom) { - avatar = ; - } else { - avatar = ; - } - - let description = _t("%(count)s members", { count: room.num_joined_members }); - if (numChildRooms !== undefined) { - description += " · " + _t("%(count)s rooms", { count: numChildRooms }); - } - - const topic = joinedRoom?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || room.topic; - if (topic) { - description += " · " + topic; - } - - let suggestedSection; - if (suggested) { - suggestedSection = - { _t("Suggested") } - ; - } - - const content = - { avatar } -
- { name } - { suggestedSection } -
- -
e && linkifyElement(e)} - onClick={ev => { - // prevent clicks on links from bubbling up to the room tile - if ((ev.target as HTMLElement).tagName === "A") { - ev.stopPropagation(); - } - }} - > - { description } -
-
- { button } - { checkbox } -
-
; - - let childToggle; - let childSection; - if (children) { - // the chevron is purposefully a div rather than a button as it should be ignored for a11y - childToggle =
{ - ev.stopPropagation(); - toggleShowChildren(); - }} - />; - if (showChildren) { - childSection =
- { children } -
; - } - } - - return <> - - { content } - { childToggle } - - { childSection } - ; -}; - -export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => { - // Don't let the user view a room they won't be able to either peek or join: - // fail earlier so they don't have to click back to the directory. - if (MatrixClientPeg.get().isGuest()) { - if (!room.world_readable && !room.guest_can_join) { - dis.dispatch({ action: "require_registration" }); - return; - } - } - - const roomAlias = getDisplayAliasForRoom(room) || undefined; - dis.dispatch({ - action: "view_room", - auto_join: autoJoin, - should_peek: true, - _type: "room_directory", // instrumentation - room_alias: roomAlias, - room_id: room.room_id, - via_servers: viaServers, - oob_data: { - avatarUrl: room.avatar_url, - // XXX: This logic is duplicated from the JS SDK which would normally decide what the name is. - name: room.name || roomAlias || _t("Unnamed room"), - }, - }); -}; - -interface IHierarchyLevelProps { - spaceId: string; - rooms: Map; - relations: Map>; - parents: Set; - selectedMap?: Map>; - onViewRoomClick(roomId: string, autoJoin: boolean): void; - onToggleClick?(parentId: string, childId: string): void; -} - -export const HierarchyLevel = ({ - spaceId, - rooms, - relations, - parents, - selectedMap, - onViewRoomClick, - onToggleClick, -}: IHierarchyLevelProps) => { - const cli = MatrixClientPeg.get(); - const space = cli.getRoom(spaceId); - const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); - - const children = Array.from(relations.get(spaceId)?.values() || []); - const sortedChildren = sortBy(children, ev => { - // XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting - return getChildOrder(ev.content.order, null, ev.state_key); - }); - const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { - const roomId = ev.state_key; - if (!rooms.has(roomId)) return result; - result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId); - return result; - }, [[], []]) || [[], []]; - - const newParents = new Set(parents).add(spaceId); - return - { - childRooms.map(roomId => ( - { - onViewRoomClick(roomId, autoJoin); - }} - hasPermissions={hasPermissions} - onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined} - /> - )) - } - - { - subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => ( - rooms.has(ev.state_key) && !rooms.get(ev.state_key).room_type).length} - suggested={relations.get(spaceId)?.get(roomId)?.content.suggested} - selected={selectedMap?.get(spaceId)?.has(roomId)} - onViewRoomClick={(autoJoin) => { - onViewRoomClick(roomId, autoJoin); - }} - hasPermissions={hasPermissions} - onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined} - > - - - )) - } - ; -}; - -// mutate argument refreshToken to force a reload -export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [ - null, - ISpaceSummaryRoom[], - Map>?, - Map>?, - Map>?, -] | [Error] => { - // TODO pagination - return useAsyncMemo(async () => { - try { - const data = await cli.getSpaceSummary(space.roomId); - - const parentChildRelations = new EnhancedMap>(); - const childParentRelations = new EnhancedMap>(); - const viaMap = new EnhancedMap>(); - data.events.map((ev: ISpaceSummaryEvent) => { - if (ev.type === EventType.SpaceChild) { - parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev); - childParentRelations.getOrCreate(ev.state_key, new Set()).add(ev.room_id); - } - if (Array.isArray(ev.content.via)) { - const set = viaMap.getOrCreate(ev.state_key, new Set()); - ev.content.via.forEach(via => set.add(via)); - } - }); - - return [null, data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations]; - } catch (e) { - console.error(e); // TODO - return [e]; - } - }, [space, refreshToken], [undefined]); -}; - -export const SpaceHierarchy: React.FC = ({ - space, - initialText = "", - showRoom, - refreshToken, - additionalButtons, - children, -}) => { - const cli = MatrixClientPeg.get(); - const userId = cli.getUserId(); - const [query, setQuery] = useState(initialText); - - const [selected, setSelected] = useState(new Map>()); // Map> - - const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken); - - const roomsMap = useMemo(() => { - if (!rooms) return null; - const lcQuery = query.toLowerCase().trim(); - - const roomsMap = new Map(rooms.map(r => [r.room_id, r])); - if (!lcQuery) return roomsMap; - - const directMatches = rooms.filter(r => { - return r.name?.toLowerCase().includes(lcQuery) || r.topic?.toLowerCase().includes(lcQuery); - }); - - // Walk back up the tree to find all parents of the direct matches to show their place in the hierarchy - const visited = new Set(); - const queue = [...directMatches.map(r => r.room_id)]; - while (queue.length) { - const roomId = queue.pop(); - visited.add(roomId); - childParentMap.get(roomId)?.forEach(parentId => { - if (!visited.has(parentId)) { - queue.push(parentId); - } - }); - } - - // Remove any mappings for rooms which were not visited in the walk - Array.from(roomsMap.keys()).forEach(roomId => { - if (!visited.has(roomId)) { - roomsMap.delete(roomId); - } - }); - return roomsMap; - }, [rooms, childParentMap, query]); - - const [error, setError] = useState(""); - const [removing, setRemoving] = useState(false); - const [saving, setSaving] = useState(false); - - if (summaryError) { - return

{ _t("Your server does not support showing space hierarchies.") }

; - } - - let content; - if (roomsMap) { - const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length; - const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at - - let countsStr; - if (numSpaces > 1) { - countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces }); - } else if (numSpaces > 0) { - countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces }); - } else { - countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces }); - } - - let manageButtons; - if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { - const selectedRelations = Array.from(selected.keys()).flatMap(parentId => { - return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][]; - }); - - const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { - return parentChildMap.get(parentId)?.get(childId)?.content.suggested; - }); - - const disabled = !selectedRelations.length || removing || saving; - - let Button: React.ComponentType> = AccessibleButton; - let props = {}; - if (!selectedRelations.length) { - Button = AccessibleTooltipButton; - props = { - tooltip: _t("Select a room below first"), - yOffset: -40, - }; - } - - manageButtons = <> - - - ; - } - - let results; - if (roomsMap.size) { - const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); - - results = <> - { - setError(""); - if (!selected.has(parentId)) { - setSelected(new Map(selected.set(parentId, new Set([childId])))); - return; - } - - const parentSet = selected.get(parentId); - if (!parentSet.has(childId)) { - setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId])))); - return; - } - - parentSet.delete(childId); - setSelected(new Map(selected.set(parentId, new Set(parentSet)))); - } : undefined} - onViewRoomClick={(roomId, autoJoin) => { - showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin); - }} - /> - { children &&
} - ; - } else { - results =
-

{ _t("No results found") }

-
{ _t("You may want to try a different search or check for typos.") }
-
; - } - - content = <> -
- { countsStr } - - { additionalButtons } - { manageButtons } - -
- { error &&
- { error } -
} - - { results } - { children } - - ; - } else { - content = ; - } - - // TODO loading state/error state - return <> - - - { content } - ; -}; - -interface IProps { - space: Room; - initialText?: string; - onFinished(): void; -} - -const SpaceRoomDirectory: React.FC = ({ space, onFinished, initialText }) => { - const onCreateRoomClick = () => { - dis.dispatch({ - action: 'view_create_room', - public: true, - }); - onFinished(); - }; - - const title = - -
-

{ _t("Explore rooms") }

-
-
-
; - - return ( - -
- { _t("If you can't find the room you're looking for, ask for an invite or create a new room.", - null, - { a: sub => { - return { sub }; - } }, - ) } - - { - showRoom(room, viaServers, autoJoin); - onFinished(); - }} - initialText={initialText} - > - - { _t("Create room") } - - -
-
- ); -}; - -export default SpaceRoomDirectory; - -// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom -// but works with the objects we get from the public room list -function getDisplayAliasForRoom(room: ISpaceSummaryRoom) { - return getDisplayAliasForAliasSet(room.canonical_alias, room.aliases); -} diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 06b2f4a629..6bb0433448 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -16,7 +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 { JoinRule, Preset } from "matrix-js-sdk/src/@types/partials"; import { Room } from "matrix-js-sdk/src/models/room"; import { EventSubscription } from "fbemitter"; @@ -47,13 +47,23 @@ import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload"; import { useStateArray } from "../../hooks/useStateArray"; import SpacePublicShare from "../views/spaces/SpacePublicShare"; -import { shouldShowSpaceSettings, showAddExistingRooms, showCreateNewRoom, showSpaceSettings } from "../../utils/space"; -import { showRoom, SpaceHierarchy } from "./SpaceRoomDirectory"; +import { + shouldShowSpaceSettings, + showAddExistingRooms, + showCreateNewRoom, + showCreateNewSubspace, + showSpaceSettings, +} from "../../utils/space"; +import SpaceHierarchy, { showRoom } from "./SpaceHierarchy"; import MemberAvatar from "../views/avatars/MemberAvatar"; -import { useStateToggle } from "../../hooks/useStateToggle"; import SpaceStore from "../../stores/SpaceStore"; import FacePile from "../views/elements/FacePile"; -import { AddExistingToSpace } from "../views/dialogs/AddExistingToSpaceDialog"; +import { + AddExistingToSpace, + defaultDmsRenderer, + defaultRoomsRenderer, + defaultSpacesRenderer, +} from "../views/dialogs/AddExistingToSpaceDialog"; import { ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu"; import IconizedContextMenu, { IconizedContextMenuOption, @@ -62,11 +72,12 @@ import IconizedContextMenu, { import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { BetaPill } from "../views/beta/BetaCard"; import { UserTab } from "../views/dialogs/UserSettingsDialog"; -import Modal from "../../Modal"; -import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog"; -import SdkConfig from "../../SdkConfig"; import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership"; -import { JoinRule } from "../views/settings/tabs/room/SecurityRoomSettingsTab"; +import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu"; +import { CreateEventField, IGroupSummary } from "../views/dialogs/CreateSpaceFromCommunityDialog"; +import { useAsyncMemo } from "../../hooks/useAsyncMemo"; +import Spinner from "../views/elements/Spinner"; +import GroupAvatar from "../views/avatars/GroupAvatar"; interface IProps { space: Room; @@ -78,7 +89,7 @@ interface IProps { interface IState { phase: Phase; - createdRooms?: boolean; // internal state for the creation wizard + firstRoomId?: string; // internal state for the creation wizard showRightPanel: boolean; myMembership: string; } @@ -93,26 +104,6 @@ enum Phase { PrivateExistingRooms, } -// XXX: Temporary for the Spaces Beta only -export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => { - if (!SdkConfig.get().bug_report_endpoint_url) return null; - - return
-
-
- { _t("Spaces are a beta feature.") } - { - if (onClick) onClick(); - Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, { - featureId: "feature_spaces", - }); - }}> - { _t("Feedback") } - -
-
; -}; - const RoomMemberCount = ({ room, children }) => { const members = useRoomMembers(room); const count = members.length; @@ -171,7 +162,33 @@ const onBetaClick = () => { }); }; -const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { +// XXX: temporary community migration component +const GroupTile = ({ groupId }: { groupId: string }) => { + const cli = useContext(MatrixClientContext); + const groupSummary = useAsyncMemo(() => cli.getGroupSummary(groupId), [cli, groupId]); + + if (!groupSummary) return ; + + return <> + + { groupSummary.profile.name } + ; +}; + +interface ISpacePreviewProps { + space: Room; + onJoinButtonClicked(): void; + onRejectButtonClicked(): void; +} + +const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISpacePreviewProps) => { const cli = useContext(MatrixClientContext); const myMembership = useMyRoomMembership(space); @@ -205,11 +222,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => if (inviteSender) { inviterSection =
- +
{ _t(" invites you", {}, { - inviter: () => { inviter.name || inviteSender }, + inviter: () => { inviter?.name || inviteSender }, }) }
{ inviter ?
@@ -283,8 +300,18 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
; } + let migratedCommunitySection: JSX.Element; + const createContent = space.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent(); + if (createContent[CreateEventField]) { + migratedCommunitySection =
+ { _t("Created from ", {}, { + Community: () => , + }) } +
; + } + return
- + { migratedCommunitySection } { inviterSection }

@@ -306,8 +333,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>

; }; -const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { - const cli = useContext(MatrixClientContext); +const SpaceLandingAddButton = ({ space }) => { const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); let contextMenu; @@ -330,25 +356,33 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { e.stopPropagation(); closeMenu(); - if (await showCreateNewRoom(cli, space)) { - onNewRoomAdded(); + if (await showCreateNewRoom(space)) { + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); } }} /> { + onClick={(e) => { e.preventDefault(); e.stopPropagation(); closeMenu(); - - const [added] = await showAddExistingRooms(cli, space); - if (added) { - onNewRoomAdded(); - } + showAddExistingRooms(space); }} /> + { + e.preventDefault(); + e.stopPropagation(); + closeMenu(); + showCreateNewSubspace(space); + }} + > + + ; } @@ -389,19 +423,17 @@ const SpaceLanding = ({ space }) => { const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId); - const [refreshToken, forceUpdate] = useStateToggle(false); - let addRoomButton; if (canAddRooms) { - addRoomButton = ; + addRoomButton = ; } let settingsButton; - if (shouldShowSpaceSettings(cli, space)) { + if (shouldShowSpaceSettings(space)) { settingsButton = { - showSpaceSettings(cli, space); + showSpaceSettings(space); }} title={_t("Settings")} />; @@ -416,6 +448,7 @@ const SpaceLanding = ({ space }) => { }; return
+
@@ -440,15 +473,8 @@ const SpaceLanding = ({ space }) => {
) } - -
- +
; }; @@ -470,6 +496,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { onChange={ev => setRoomName(i, ev.target.value)} autoFocus={i === 2} disabled={busy} + autoComplete="off" />; }); @@ -479,11 +506,12 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { setError(""); setBusy(true); try { + const isPublic = space.getJoinRule() === JoinRule.Public; const filteredRoomNames = roomNames.map(name => name.trim()).filter(Boolean); - await Promise.all(filteredRoomNames.map(name => { + const roomIds = await Promise.all(filteredRoomNames.map(name => { return createRoom({ createOpts: { - preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat, + preset: isPublic ? Preset.PublicChat : Preset.PrivateChat, name, }, spinner: false, @@ -491,9 +519,11 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { andView: false, inlineErrors: true, parentSpace: space, + joinRule: !isPublic ? JoinRule.Restricted : undefined, + suggested: true, }); })); - onFinished(filteredRoomNames.length > 0); + onFinished(roomIds[0]); } catch (e) { console.error("Failed to create initial space rooms", e); setError(_t("Failed to create initial space rooms")); @@ -503,7 +533,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { let onClick = (ev) => { ev.preventDefault(); - onFinished(false); + onFinished(); }; let buttonLabel = _t("Skip for now"); if (roomNames.some(name => name.trim())) { @@ -531,7 +561,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { value={buttonLabel} />
-
; }; @@ -550,17 +579,20 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => { { _t("Skip for now") } } + filterPlaceholder={_t("Search for rooms or spaces")} onFinished={onFinished} + roomsRenderer={defaultRoomsRenderer} + spacesRenderer={defaultSpacesRenderer} + dmsRenderer={defaultDmsRenderer} /> - -
- -
-
; }; -const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRooms }) => { +interface ISpaceSetupPublicShareProps extends Pick { + onFinished(): void; +} + +const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, firstRoomId }: ISpaceSetupPublicShareProps) => { return

{ _t("Share %(name)s", { name: justCreatedOpts?.createOpts?.name || space.name, @@ -573,10 +605,9 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRoom
- { createdRooms ? _t("Go to my first room") : _t("Go to my space") } + { firstRoomId ? _t("Go to my first room") : _t("Go to my space") }
-

; }; @@ -605,9 +636,8 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {

{ _t("Teammates might not be able to view or join any private rooms you make.") }

-

{ _t("We're working on this as part of the beta, but just want to let you know.") }

+

{ _t("We're working on this, but just want to let you know.") }

-
; }; @@ -730,7 +760,6 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => { value={buttonLabel} /> - ; }; @@ -784,6 +813,11 @@ export default class SpaceRoomView extends React.PureComponent { }; private onAction = (payload: ActionPayload) => { + if (payload.action === "view_room" && payload.room_id === this.props.space.roomId) { + this.setState({ phase: Phase.Landing }); + return; + } + if (payload.action !== Action.ViewUser && payload.action !== "view_3pid_invite") return; if (payload.action === Action.ViewUser && payload.member) { @@ -814,35 +848,10 @@ export default class SpaceRoomView extends React.PureComponent { }; private goToFirstRoom = async () => { - // TODO actually go to the first room - - const childRooms = SpaceStore.instance.getChildRooms(this.props.space.roomId); - if (childRooms.length) { - const room = childRooms[0]; + if (this.state.firstRoomId) { defaultDispatcher.dispatch({ action: "view_room", - room_id: room.roomId, - }); - return; - } - - let suggestedRooms = SpaceStore.instance.suggestedRooms; - if (SpaceStore.instance.activeSpace !== this.props.space) { - // the space store has the suggested rooms loaded for a different space, fetch the right ones - suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1)); - } - - if (suggestedRooms.length) { - const room = suggestedRooms[0]; - defaultDispatcher.dispatch({ - action: "view_room", - room_id: room.room_id, - room_alias: room.canonical_alias || room.aliases?.[0], - via_servers: room.viaServers, - oobData: { - avatarUrl: room.avatar_url, - name: room.name || room.canonical_alias || room.aliases?.[0] || _t("Empty room"), - }, + room_id: this.state.firstRoomId, }); return; } @@ -872,14 +881,14 @@ export default class SpaceRoomView extends React.PureComponent { _t("Let's create a room for each of them.") + "\n" + _t("You can add more later too, including already existing ones.") } - onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.PublicShare, createdRooms })} + onFinished={(firstRoomId: string) => this.setState({ phase: Phase.PublicShare, firstRoomId })} />; case Phase.PublicShare: return ; case Phase.PrivateScope: @@ -901,7 +910,7 @@ export default class SpaceRoomView extends React.PureComponent { title={_t("What projects are you working on?")} description={_t("We'll create rooms for each of them. " + "You can add more later too, including already existing ones.")} - onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.Landing, createdRooms })} + onFinished={(firstRoomId: string) => this.setState({ phase: Phase.Landing, firstRoomId })} />; case Phase.PrivateExistingRooms: return void; + resizeNotifier: ResizeNotifier; +} + +interface IState { + threads?: Thread[]; +} + +@replaceableComponent("structures.ThreadView") +export default class ThreadPanel extends React.Component { + private room: Room; + + constructor(props: IProps) { + super(props); + this.room = MatrixClientPeg.get().getRoom(this.props.roomId); + } + + public componentDidMount(): void { + this.room.on("Thread.update", this.onThreadEventReceived); + this.room.on("Thread.ready", this.onThreadEventReceived); + } + + public componentWillUnmount(): void { + this.room.removeListener("Thread.update", this.onThreadEventReceived); + this.room.removeListener("Thread.ready", this.onThreadEventReceived); + } + + private onThreadEventReceived = () => this.updateThreads(); + + private updateThreads = (callback?: () => void): void => { + this.setState({ + threads: this.room.getThreads(), + }, callback); + }; + + private renderEventTile(event: MatrixEvent): JSX.Element { + return ; + } + + public render(): JSX.Element { + return ( + + { + this.state?.threads.map((thread: Thread) => { + if (thread.ready) { + return this.renderEventTile(thread.rootEvent); + } + }) + } + + ); + } +} diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx new file mode 100644 index 0000000000..614d3c9f4b --- /dev/null +++ b/src/components/structures/ThreadView.tsx @@ -0,0 +1,160 @@ +/* +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 { MatrixEvent, Room } from 'matrix-js-sdk/src'; +import { Thread } from 'matrix-js-sdk/src/models/thread'; + +import BaseCard from "../views/right_panel/BaseCard"; +import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; +import { replaceableComponent } from "../../utils/replaceableComponent"; + +import ResizeNotifier from '../../utils/ResizeNotifier'; +import { TileShape } from '../views/rooms/EventTile'; +import MessageComposer from '../views/rooms/MessageComposer'; +import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; +import { Layout } from '../../settings/Layout'; +import TimelinePanel from './TimelinePanel'; +import dis from "../../dispatcher/dispatcher"; +import { ActionPayload } from '../../dispatcher/payloads'; +import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload'; +import { Action } from '../../dispatcher/actions'; +import { MatrixClientPeg } from '../../MatrixClientPeg'; +import { E2EStatus } from '../../utils/ShieldUtils'; + +interface IProps { + room: Room; + onClose: () => void; + resizeNotifier: ResizeNotifier; + mxEvent: MatrixEvent; + permalinkCreator?: RoomPermalinkCreator; + e2eStatus?: E2EStatus; +} + +interface IState { + replyToEvent?: MatrixEvent; + thread?: Thread; +} + +@replaceableComponent("structures.ThreadView") +export default class ThreadView extends React.Component { + private dispatcherRef: string; + private timelinePanelRef: React.RefObject = React.createRef(); + + constructor(props: IProps) { + super(props); + this.state = {}; + } + + public componentDidMount(): void { + this.setupThread(this.props.mxEvent); + this.dispatcherRef = dis.register(this.onAction); + } + + public componentWillUnmount(): void { + this.teardownThread(); + dis.unregister(this.dispatcherRef); + } + + public componentDidUpdate(prevProps) { + if (prevProps.mxEvent !== this.props.mxEvent) { + this.teardownThread(); + this.setupThread(this.props.mxEvent); + } + + if (prevProps.room !== this.props.room) { + dis.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.RoomSummary, + }); + } + } + + private onAction = (payload: ActionPayload): void => { + if (payload.phase == RightPanelPhases.ThreadView && payload.event) { + if (payload.event !== this.props.mxEvent) { + this.teardownThread(); + this.setupThread(payload.event); + } + } + }; + + private setupThread = (mxEv: MatrixEvent) => { + let thread = mxEv.getThread(); + if (!thread) { + const client = MatrixClientPeg.get(); + thread = new Thread([mxEv], this.props.room, client); + mxEv.setThread(thread); + } + thread.on("Thread.update", this.updateThread); + thread.once("Thread.ready", this.updateThread); + this.updateThread(thread); + }; + + private teardownThread = () => { + if (this.state.thread) { + this.state.thread.removeListener("Thread.update", this.updateThread); + this.state.thread.removeListener("Thread.ready", this.updateThread); + } + }; + + private updateThread = (thread?: Thread) => { + if (thread) { + this.setState({ + thread, + replyToEvent: thread.replyToEvent, + }); + } + + this.timelinePanelRef.current?.refreshTimeline(); + }; + + public render(): JSX.Element { + return ( + + { this.state.thread && ( + empty} + alwaysShowTimestamps={true} + layout={Layout.Group} + hideThreadedMessages={false} + /> + ) } + + + ); + } +} diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 0899b1c72a..dbd479c2ed 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -47,11 +47,14 @@ import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import Spinner from "../views/elements/Spinner"; import EditorStateTransfer from '../../utils/EditorStateTransfer'; import ErrorDialog from '../views/dialogs/ErrorDialog'; +import { debounce } from 'lodash'; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; const READ_RECEIPT_INTERVAL_MS = 500; +const READ_MARKER_DEBOUNCE_MS = 100; + const DEBUG = false; let debuglog = function(...s: any[]) {}; @@ -126,6 +129,8 @@ interface IProps { // callback which is called when we wish to paginate the timeline window. onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise; + + hideThreadedMessages?: boolean; } interface IState { @@ -214,6 +219,7 @@ class TimelinePanel extends React.Component { timelineCap: Number.MAX_VALUE, className: 'mx_RoomView_messagePanel', sendReadReceiptOnLoad: true, + hideThreadedMessages: true, }; private lastRRSentEventId: string = undefined; @@ -472,22 +478,35 @@ class TimelinePanel extends React.Component { } if (this.props.manageReadMarkers) { - const rmPosition = this.getReadMarkerPosition(); - // we hide the read marker when it first comes onto the screen, but if - // it goes back off the top of the screen (presumably because the user - // clicks on the 'jump to bottom' button), we need to re-enable it. - if (rmPosition < 0) { - this.setState({ readMarkerVisible: true }); - } - - // if read marker position goes between 0 and -1/1, - // (and user is active), switch timeout - const timeout = this.readMarkerTimeout(rmPosition); - // NO-OP when timeout already has set to the given value - this.readMarkerActivityTimer.changeTimeout(timeout); + this.doManageReadMarkers(); } }; + /* + * Debounced function to manage read markers because we don't need to + * do this on every tiny scroll update. It also sets state which causes + * a component update, which can in turn reset the scroll position, so + * it's important we allow the browser to scroll a bit before running this + * (hence trailing edge only and debounce rather than throttle because + * we really only need to update this once the user has finished scrolling, + * not periodically while they scroll). + */ + private doManageReadMarkers = debounce(() => { + const rmPosition = this.getReadMarkerPosition(); + // we hide the read marker when it first comes onto the screen, but if + // it goes back off the top of the screen (presumably because the user + // clicks on the 'jump to bottom' button), we need to re-enable it. + if (rmPosition < 0) { + this.setState({ readMarkerVisible: true }); + } + + // if read marker position goes between 0 and -1/1, + // (and user is active), switch timeout + const timeout = this.readMarkerTimeout(rmPosition); + // NO-OP when timeout already has set to the given value + this.readMarkerActivityTimer.changeTimeout(timeout); + }, READ_MARKER_DEBOUNCE_MS, { leading: false, trailing: true }); + private onAction = (payload: ActionPayload): void => { switch (payload.action) { case "ignore_state_changed": @@ -757,16 +776,20 @@ class TimelinePanel extends React.Component { } this.lastRMSentEventId = this.state.readMarkerEventId; + const roomId = this.props.timelineSet.room.roomId; + const hiddenRR = SettingsStore.getValue("feature_hidden_read_receipts", roomId); + debuglog('TimelinePanel: Sending Read Markers for ', this.props.timelineSet.room.roomId, 'rm', this.state.readMarkerEventId, lastReadEvent ? 'rr ' + lastReadEvent.getId() : '', + ' hidden:' + hiddenRR, ); MatrixClientPeg.get().setRoomReadMarkers( - this.props.timelineSet.room.roomId, + roomId, this.state.readMarkerEventId, lastReadEvent, // Could be null, in which case no RR is sent - {}, + { hidden: hiddenRR }, ).catch((e) => { // /read_markers API is not implemented on this HS, fallback to just RR if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) { @@ -1172,6 +1195,12 @@ class TimelinePanel extends React.Component { this.setState(this.getEvents()); } + // Force refresh the timeline before threads support pending events + public refreshTimeline(): void { + this.loadTimeline(); + this.reloadEvents(); + } + // get the list of events from the timeline window and the pending event list private getEvents(): Pick { const events: MatrixEvent[] = this.timelineWindow.getEvents(); @@ -1507,6 +1536,7 @@ class TimelinePanel extends React.Component { showReactions={this.props.showReactions} layout={this.props.layout} enableFlair={SettingsStore.getValue(UIFeature.Flair)} + hideThreadedMessages={this.props.hideThreadedMessages} /> ); } diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index b7b0b7c652..0b0e871975 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -58,28 +58,39 @@ export default class ToastContainer extends React.Component<{}, IState> { let containerClasses; if (totalCount !== 0) { const topToast = this.state.toasts[0]; - const { title, icon, key, component, className, props } = topToast; - const toastClasses = classNames("mx_Toast_toast", { + const { title, icon, key, component, className, bodyClassName, props } = topToast; + const bodyClasses = classNames("mx_Toast_body", bodyClassName); + const toastClasses = classNames("mx_Toast_toast", className, { "mx_Toast_hasIcon": icon, [`mx_Toast_icon_${icon}`]: icon, - }, className); - - let countIndicator; - if (isStacked || this.state.countSeen > 0) { - countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`; - } - + }); const toastProps = Object.assign({}, props, { key, toastKey: key, }); - toast = (
-
-

{ title }

- { countIndicator } + const content = React.createElement(component, toastProps); + + let countIndicator; + if (title && isStacked || this.state.countSeen > 0) { + countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`; + } + + let titleElement; + if (title) { + titleElement = ( +
+

{ title }

+ { countIndicator } +
+ ); + } + + toast = ( +
+ { titleElement } +
{ content }
-
{ React.createElement(component, toastProps) }
-
); + ); containerClasses = classNames("mx_ToastContainer", { "mx_ToastContainer_stacked": isStacked, diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 3755505f3d..24b47bfa03 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -31,6 +31,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm'; import { IValidationResult } from "../../views/elements/Validation"; +import InlineSpinner from '../../views/elements/InlineSpinner'; enum Phase { // Show the forgot password inputs @@ -66,13 +67,14 @@ interface IState { serverDeadError: string; passwordFieldValid: boolean; + currentHttpRequest?: Promise; } @replaceableComponent("structures.auth.ForgotPassword") export default class ForgotPassword extends React.Component { private reset: PasswordReset; - state = { + state: IState = { phase: Phase.Forgot, email: "", password: "", @@ -148,8 +150,10 @@ export default class ForgotPassword extends React.Component { console.error("onVerify called before submitPasswordReset!"); return; } + if (this.state.currentHttpRequest) return; + try { - await this.reset.checkEmailLinkClicked(); + await this.handleHttpRequest(this.reset.checkEmailLinkClicked()); this.setState({ phase: Phase.Done }); } catch (err) { this.showErrorDialog(err.message); @@ -158,9 +162,10 @@ export default class ForgotPassword extends React.Component { private onSubmitForm = async (ev: React.FormEvent): Promise => { ev.preventDefault(); + if (this.state.currentHttpRequest) return; // refresh the server errors, just in case the server came back online - await this.checkServerLiveliness(this.props.serverConfig); + await this.handleHttpRequest(this.checkServerLiveliness(this.props.serverConfig)); await this['password_field'].validate({ allowEmpty: false }); @@ -221,6 +226,17 @@ export default class ForgotPassword extends React.Component { }); } + private handleHttpRequest(request: Promise): Promise { + this.setState({ + currentHttpRequest: request, + }); + return request.finally(() => { + this.setState({ + currentHttpRequest: undefined, + }); + }); + } + renderForgot() { const Field = sdk.getComponent('elements.Field'); @@ -315,8 +331,14 @@ export default class ForgotPassword extends React.Component { { _t("An email has been sent to %(emailAddress)s. Once you've followed the " + "link it contains, click below.", { emailAddress: this.state.email }) }
- + { this.state.currentHttpRequest && ( +
) + }
; } @@ -328,7 +350,10 @@ export default class ForgotPassword extends React.Component { "push notifications. To re-enable notifications, sign in again on each " + "device.", ) }

- ; } @@ -351,6 +376,8 @@ export default class ForgotPassword extends React.Component { case Phase.Done: resetPasswordJsx = this.renderDone(); break; + default: + resetPasswordJsx =
; } return ( diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 6a3d339681..7a05d8c6c6 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -463,7 +463,9 @@ export default class LoginComponent extends React.PureComponent "Either use HTTPS or enable unsafe scripts.", {}, { 'a': (sub) => { - return { sub } diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 549e47260f..2b97650d4b 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -557,12 +557,16 @@ export default class Registration extends React.Component { loggedInUserId: this.state.differentLoggedInUserId, }, ) }

-

{ - const sessionLoaded = await this.onLoginClickWithCheck(event); - if (sessionLoaded) { - dis.dispatch({ action: "view_welcome_page" }); - } - }}> +

{ + const sessionLoaded = await this.onLoginClickWithCheck(event); + if (sessionLoaded) { + dis.dispatch({ action: "view_welcome_page" }); + } + }} + > { _t("Continue with previous account") }

; diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx index fb9270765e..b83f89fe5b 100644 --- a/src/components/views/audio_messages/AudioPlayer.tsx +++ b/src/components/views/audio_messages/AudioPlayer.tsx @@ -14,9 +14,7 @@ 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"; @@ -25,47 +23,13 @@ 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; - error?: boolean; -} +import AudioPlayerBase from "./AudioPlayerBase"; @replaceableComponent("views.audio_messages.AudioPlayer") -export default class AudioPlayer extends React.PureComponent { +export default class AudioPlayer extends AudioPlayerBase { private playPauseRef: RefObject = createRef(); private seekRef: RefObject = 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. - this.props.playback.prepare().catch(e => { - console.error("Error processing audio file:", e); - this.setState({ error: true }); - }); - } - - 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 @@ -91,10 +55,10 @@ export default class AudioPlayer extends React.PureComponent { return `(${formatBytes(bytes)})`; } - public render(): ReactNode { + protected renderComponent(): ReactNode { // tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard // events for accessibility - return <> + return (
{
- { this.state.error &&
{ _t("Error downloading audio") }
} - ; + ); } } diff --git a/src/components/views/audio_messages/AudioPlayerBase.tsx b/src/components/views/audio_messages/AudioPlayerBase.tsx new file mode 100644 index 0000000000..d8fc9d507f --- /dev/null +++ b/src/components/views/audio_messages/AudioPlayerBase.tsx @@ -0,0 +1,70 @@ +/* +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 "../../../audio/Playback"; +import { TileShape } from "../rooms/EventTile"; +import React, { ReactNode } from "react"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { _t } from "../../../languageHandler"; + +interface IProps { + // Playback instance to render. Cannot change during component lifecycle: create + // an all-new component instead. + playback: Playback; + + mediaName?: string; + tileShape?: TileShape; +} + +interface IState { + playbackPhase: PlaybackState; + error?: boolean; +} + +@replaceableComponent("views.audio_messages.AudioPlayerBase") +export default abstract class AudioPlayerBase extends React.PureComponent { + 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. + this.props.playback.prepare().catch(e => { + console.error("Error processing audio file:", e); + this.setState({ error: true }); + }); + } + + private onPlaybackUpdate = (ev: PlaybackState) => { + this.setState({ playbackPhase: ev }); + }; + + protected abstract renderComponent(): ReactNode; + + public render(): ReactNode { + return <> + { this.renderComponent() } + { this.state.error &&
{ _t("Error downloading audio") }
} + ; + } +} diff --git a/src/components/views/audio_messages/Clock.tsx b/src/components/views/audio_messages/Clock.tsx index cb1a179f2e..69244cc5ad 100644 --- a/src/components/views/audio_messages/Clock.tsx +++ b/src/components/views/audio_messages/Clock.tsx @@ -15,34 +15,30 @@ limitations under the License. */ import React from "react"; +import { formatSeconds } from "../../../DateUtils"; import { replaceableComponent } from "../../../utils/replaceableComponent"; export interface IProps { seconds: number; } -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.audio_messages.Clock") -export default class Clock extends React.Component { +export default class Clock extends React.Component { public constructor(props) { super(props); } - shouldComponentUpdate(nextProps: Readonly, nextState: Readonly, nextContext: any): boolean { + shouldComponentUpdate(nextProps: Readonly): boolean { const currentFloor = Math.floor(this.props.seconds); const nextFloor = Math.floor(nextProps.seconds); return currentFloor !== nextFloor; } public render() { - const minutes = Math.floor(this.props.seconds / 60).toFixed(0).padStart(2, '0'); - const seconds = Math.floor(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis - return { minutes }:{ seconds }; + return { formatSeconds(this.props.seconds) }; } } diff --git a/src/components/views/audio_messages/DurationClock.tsx b/src/components/views/audio_messages/DurationClock.tsx index 81852b5944..15bc6c98a4 100644 --- a/src/components/views/audio_messages/DurationClock.tsx +++ b/src/components/views/audio_messages/DurationClock.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import Clock from "./Clock"; -import { Playback } from "../../../voice/Playback"; +import { Playback } from "../../../audio/Playback"; interface IProps { playback: Playback; diff --git a/src/components/views/audio_messages/LiveRecordingClock.tsx b/src/components/views/audio_messages/LiveRecordingClock.tsx index a9dbd3c52f..e7330efc1d 100644 --- a/src/components/views/audio_messages/LiveRecordingClock.tsx +++ b/src/components/views/audio_messages/LiveRecordingClock.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording"; +import { IRecordingUpdate, VoiceRecording } from "../../../audio/VoiceRecording"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import Clock from "./Clock"; import { MarkedExecution } from "../../../utils/MarkedExecution"; diff --git a/src/components/views/audio_messages/LiveRecordingWaveform.tsx b/src/components/views/audio_messages/LiveRecordingWaveform.tsx index b9c5f80f05..73e18626fe 100644 --- a/src/components/views/audio_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/audio_messages/LiveRecordingWaveform.tsx @@ -15,10 +15,9 @@ limitations under the License. */ import React from "react"; -import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../voice/VoiceRecording"; +import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { arrayFastResample } from "../../../utils/arrays"; -import { percentageOf } from "../../../utils/numbers"; +import { arrayFastResample, arraySeed } from "../../../utils/arrays"; import Waveform from "./Waveform"; import { MarkedExecution } from "../../../utils/MarkedExecution"; @@ -48,18 +47,14 @@ export default class LiveRecordingWaveform extends React.PureComponent { - const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES); - // The incoming data is between zero and one, but typically even screaming into a - // microphone won't send you over 0.6, so we artificially adjust the gain for the - // waveform. This results in a slightly more cinematic/animated waveform for the - // user. - this.waveform = bars.map(b => percentageOf(b, 0, 0.50)); + // The incoming data is between zero and one, so we don't need to clamp/rescale it. + this.waveform = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES); this.scheduledUpdate.mark(); }); } diff --git a/src/components/views/audio_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx index a4f1e770f2..de2822cc39 100644 --- a/src/components/views/audio_messages/PlayPauseButton.tsx +++ b/src/components/views/audio_messages/PlayPauseButton.tsx @@ -18,7 +18,7 @@ import React, { ReactNode } from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { _t } from "../../../languageHandler"; -import { Playback, PlaybackState } from "../../../voice/Playback"; +import { Playback, PlaybackState } from "../../../audio/Playback"; import classNames from "classnames"; // omitted props are handled by render function diff --git a/src/components/views/audio_messages/PlaybackClock.tsx b/src/components/views/audio_messages/PlaybackClock.tsx index 374d47c31d..affb025d86 100644 --- a/src/components/views/audio_messages/PlaybackClock.tsx +++ b/src/components/views/audio_messages/PlaybackClock.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import Clock from "./Clock"; -import { Playback, PlaybackState } from "../../../voice/Playback"; +import { Playback, PlaybackState } from "../../../audio/Playback"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; interface IProps { diff --git a/src/components/views/audio_messages/PlaybackWaveform.tsx b/src/components/views/audio_messages/PlaybackWaveform.tsx index ea1b846c01..96fd3f5ae2 100644 --- a/src/components/views/audio_messages/PlaybackWaveform.tsx +++ b/src/components/views/audio_messages/PlaybackWaveform.tsx @@ -18,7 +18,7 @@ import React from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { arraySeed, arrayTrimFill } from "../../../utils/arrays"; import Waveform from "./Waveform"; -import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../voice/Playback"; +import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../audio/Playback"; import { percentageOf } from "../../../utils/numbers"; interface IProps { diff --git a/src/components/views/audio_messages/RecordingPlayback.tsx b/src/components/views/audio_messages/RecordingPlayback.tsx index ca0ed83d84..e3f612c9b6 100644 --- a/src/components/views/audio_messages/RecordingPlayback.tsx +++ b/src/components/views/audio_messages/RecordingPlayback.tsx @@ -14,68 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Playback, PlaybackState } from "../../../voice/Playback"; import React, { ReactNode } from "react"; -import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import PlayPauseButton from "./PlayPauseButton"; import PlaybackClock from "./PlaybackClock"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { TileShape } from "../rooms/EventTile"; import PlaybackWaveform from "./PlaybackWaveform"; -import { _t } from "../../../languageHandler"; - -interface IProps { - // Playback instance to render. Cannot change during component lifecycle: create - // an all-new component instead. - playback: Playback; - - tileShape?: TileShape; -} - -interface IState { - playbackPhase: PlaybackState; - error?: boolean; -} +import AudioPlayerBase from "./AudioPlayerBase"; @replaceableComponent("views.audio_messages.RecordingPlayback") -export default class RecordingPlayback extends React.PureComponent { - 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. - this.props.playback.prepare().catch(e => { - console.error("Error processing audio file:", e); - this.setState({ error: true }); - }); - } - +export default class RecordingPlayback extends AudioPlayerBase { private get isWaveformable(): boolean { return this.props.tileShape !== TileShape.Notif && this.props.tileShape !== TileShape.FileGrid && this.props.tileShape !== TileShape.Pinned; } - private onPlaybackUpdate = (ev: PlaybackState) => { - this.setState({ playbackPhase: ev }); - }; - - public render(): ReactNode { + protected renderComponent(): ReactNode { const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : ''; - return <> + return (
{ this.isWaveformable && }
- { this.state.error &&
{ _t("Error downloading audio") }
} - ; + ); } } diff --git a/src/components/views/audio_messages/SeekBar.tsx b/src/components/views/audio_messages/SeekBar.tsx index 5231a2fb79..f0c03bb032 100644 --- a/src/components/views/audio_messages/SeekBar.tsx +++ b/src/components/views/audio_messages/SeekBar.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Playback, PlaybackState } from "../../../voice/Playback"; +import { Playback, PlaybackState } from "../../../audio/Playback"; import React, { ChangeEvent, CSSProperties, ReactNode } from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { MarkedExecution } from "../../../utils/MarkedExecution"; diff --git a/src/components/views/audio_messages/Waveform.tsx b/src/components/views/audio_messages/Waveform.tsx index 8a4427fd01..4e44abdf46 100644 --- a/src/components/views/audio_messages/Waveform.tsx +++ b/src/components/views/audio_messages/Waveform.tsx @@ -54,9 +54,13 @@ export default class Waveform extends React.PureComponent { 'mx_Waveform_bar': true, 'mx_Waveform_bar_100pct': isCompleteBar, }); - return ; + return ; }) } ; } diff --git a/src/components/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx index b1c09f2b22..97f45167a8 100644 --- a/src/components/views/auth/CaptchaForm.tsx +++ b/src/components/views/auth/CaptchaForm.tsx @@ -103,8 +103,8 @@ export default class CaptchaForm extends React.Component{ _t("Accept") }; + submitButton = ; } return ( @@ -616,7 +592,9 @@ export class MsisdnAuthEntry extends React.Component
- @@ -831,7 +809,26 @@ export class FallbackAuthEntry extends React.Component { } } -export default function getEntryComponentForLoginType(loginType: AuthType): typeof React.Component { +export interface IStageComponentProps extends IAuthEntryProps { + clientSecret?: string; + stageParams?: Record; + inputs?: IInputs; + stageState?: IStageStatus; + showContinue?: boolean; + continueText?: string; + continueKind?: string; + fail?(e: Error): void; + setEmailSid?(sid: string): void; + onCancel?(): void; +} + +export interface IStageComponent extends React.ComponentClass> { + tryContinue?(): void; + attemptFailed?(): void; + focus?(): void; +} + +export default function getEntryComponentForLoginType(loginType: AuthType): IStageComponent { switch (loginType) { case AuthType.Password: return PasswordAuthEntry; diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 87cdbe7512..6aaef29854 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -187,7 +187,8 @@ const BaseAvatar = (props: IProps) => { width: toPx(width), height: toPx(height), }} - title={title} alt={_t("Avatar")} + title={title} + alt={_t("Avatar")} inputRef={inputRef} {...otherProps} /> ); @@ -201,7 +202,8 @@ const BaseAvatar = (props: IProps) => { width: toPx(width), height: toPx(height), }} - title={title} alt="" + title={title} + alt="" ref={inputRef} {...otherProps} /> ); diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 61155e3880..3c734705b7 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -36,6 +36,7 @@ interface IProps extends Omit, "name" | // Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser` viewUserOnClick?: boolean; title?: string; + style?: any; } interface IState { @@ -102,8 +103,12 @@ export default class MemberAvatar extends React.Component { } return ( - + ); } } diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index a07990c3bb..f285222f7b 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -13,9 +13,11 @@ WITHOUT 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, { ComponentProps } from 'react'; import { Room } from 'matrix-js-sdk/src/models/room'; import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; +import classNames from "classnames"; import BaseAvatar from './BaseAvatar'; import ImageView from '../elements/ImageView'; @@ -32,11 +34,14 @@ interface IProps extends Omit, "name" | "idNam // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) room?: Room; - oobData?: IOOBData; + oobData?: IOOBData & { + roomId?: string; + }; width?: number; height?: number; resizeMethod?: ResizeMethod; viewAvatarOnClick?: boolean; + className?: string; onClick?(): void; } @@ -129,15 +134,19 @@ export default class RoomAvatar extends React.Component { }; public render() { - const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props; + const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props; const roomName = room ? room.name : oobData.name; // If the room is a DM, we use the other user's ID for the color hash // in order to match the room avatar with their avatar - const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : null; + const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : oobData.roomId; return ( - { + private numberEntryFieldRef: React.RefObject = createRef(); + constructor(props) { super(props); @@ -40,15 +43,29 @@ export default class DialpadContextMenu extends React.Component }; } - onDigitPress = (digit) => { + onDigitPress = (digit: string, ev: ButtonEvent) => { this.props.call.sendDtmfDigit(digit); this.setState({ value: this.state.value + digit }); + + // Keep the number field focused so that keyboard entry is still available + // However, don't focus if this wasn't the result of directly clicking on the button, + // i.e someone using keyboard navigation. + if (ev.type === "click") { + this.numberEntryFieldRef.current?.focus(); + } }; onCancelClick = () => { this.props.onFinished(); }; + onKeyDown = (ev) => { + // Prevent Backspace and Delete keys from functioning in the entry field + if (ev.code === "Backspace" || ev.code === "Delete") { + ev.preventDefault(); + } + }; + onChange = (ev) => { this.setState({ value: ev.target.value }); }; @@ -60,8 +77,12 @@ export default class DialpadContextMenu extends React.Component
-
diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx index 1d822fd246..571b0b39bf 100644 --- a/src/components/views/context_menus/IconizedContextMenu.tsx +++ b/src/components/views/context_menus/IconizedContextMenu.tsx @@ -86,14 +86,18 @@ export const IconizedContextMenuCheckbox: React.FC = ({ > { label } - { active && } + ; }; -export const IconizedContextMenuOption: React.FC = ({ label, iconClassName, ...props }) => { +export const IconizedContextMenuOption: React.FC = ({ label, iconClassName, children, ...props }) => { return { iconClassName && } { label } + { children } ; }; diff --git a/src/components/views/context_menus/SpaceContextMenu.tsx b/src/components/views/context_menus/SpaceContextMenu.tsx new file mode 100644 index 0000000000..28c35eef8f --- /dev/null +++ b/src/components/views/context_menus/SpaceContextMenu.tsx @@ -0,0 +1,216 @@ +/* +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, { useContext } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { EventType } from "matrix-js-sdk/src/@types/event"; + +import { + IProps as IContextMenuProps, +} from "../../structures/ContextMenu"; +import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; +import { _t } from "../../../languageHandler"; +import { + leaveSpace, + shouldShowSpaceSettings, + showAddExistingRooms, + showCreateNewRoom, + showCreateNewSubspace, + showSpaceInvite, + showSpaceSettings, +} from "../../../utils/space"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { ButtonEvent } from "../elements/AccessibleButton"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import RoomViewStore from "../../../stores/RoomViewStore"; +import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; +import { Action } from "../../../dispatcher/actions"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { BetaPill } from "../beta/BetaCard"; + +interface IProps extends IContextMenuProps { + space: Room; +} + +const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => { + const cli = useContext(MatrixClientContext); + const userId = cli.getUserId(); + + let inviteOption; + if (space.getJoinRule() === "public" || space.canInvite(userId)) { + const onInviteClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showSpaceInvite(space); + onFinished(); + }; + + inviteOption = ( + + ); + } + + let settingsOption; + let leaveSection; + if (shouldShowSpaceSettings(space)) { + const onSettingsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showSpaceSettings(space); + onFinished(); + }; + + settingsOption = ( + + ); + } else { + const onLeaveClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + leaveSpace(space); + onFinished(); + }; + + leaveSection = + + ; + } + + const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId); + + let newRoomSection; + if (space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { + const onNewRoomClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showCreateNewRoom(space); + onFinished(); + }; + + const onAddExistingRoomClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showAddExistingRooms(space); + onFinished(); + }; + + const onNewSubspaceClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showCreateNewSubspace(space); + onFinished(); + }; + + newRoomSection = + + + + + + ; + } + + const onMembersClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (!RoomViewStore.getRoomId()) { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: space.roomId, + }, true); + } + + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.SpaceMemberList, + refireParams: { space }, + }); + onFinished(); + }; + + const onExploreRoomsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + defaultDispatcher.dispatch({ + action: "view_room", + room_id: space.roomId, + }); + onFinished(); + }; + + return +
+ { space.name } +
+ + { inviteOption } + + { settingsOption } + + + { newRoomSection } + { leaveSection } +
; +}; + +export default SpaceContextMenu; + diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index f90d9cc005..e05b05116c 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -109,8 +109,10 @@ export default class StatusMessageContextMenu extends React.Component {
; } } else { - actionButton = { _t("Set status") } ; @@ -121,12 +123,19 @@ export default class StatusMessageContextMenu extends React.Component { spinner = ; } - const form =
-
diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index c40ff4207b..0c3c48a07f 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -24,6 +24,8 @@ import { MenuItem } from "../../structures/ContextMenu"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore"; +import { createSpaceFromCommunity } from "../../../utils/space"; +import GroupStore from "../../../stores/GroupStore"; @replaceableComponent("views.context_menus.TagTileContextMenu") export default class TagTileContextMenu extends React.Component { @@ -49,6 +51,11 @@ export default class TagTileContextMenu extends React.Component { this.props.onFinished(); }; + _onCreateSpaceClick = () => { + createSpaceFromCommunity(this.context, this.props.tag); + this.props.onFinished(); + }; + _onMoveUp = () => { dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1)); this.props.onFinished(); @@ -77,6 +84,16 @@ export default class TagTileContextMenu extends React.Component { ); } + let createSpaceOption; + if (GroupStore.isUserPrivileged(this.props.tag)) { + createSpaceOption = <> +
+ + { _t("Create Space") } + + ; + } + return
{ _t('View Community') } @@ -88,6 +105,7 @@ export default class TagTileContextMenu extends React.Component { { _t("Unpin") } + { createSpaceOption }
; } } diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index b21efdceb9..26d7b640a4 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -76,7 +76,8 @@ const WidgetContextMenu: React.FC = ({ onFinished(); }; streamAudioStreamButton = ; } diff --git a/src/components/views/dialogs/AddExistingSubspaceDialog.tsx b/src/components/views/dialogs/AddExistingSubspaceDialog.tsx new file mode 100644 index 0000000000..7fef2c2d9d --- /dev/null +++ b/src/components/views/dialogs/AddExistingSubspaceDialog.tsx @@ -0,0 +1,67 @@ +/* +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, { useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { _t } from '../../../languageHandler'; +import BaseDialog from "./BaseDialog"; +import AccessibleButton from "../elements/AccessibleButton"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { AddExistingToSpace, defaultSpacesRenderer, SubspaceSelector } from "./AddExistingToSpaceDialog"; + +interface IProps { + space: Room; + onCreateSubspaceClick(): void; + onFinished(added?: boolean): void; +} + +const AddExistingSubspaceDialog: React.FC = ({ space, onCreateSubspaceClick, onFinished }) => { + const [selectedSpace, setSelectedSpace] = useState(space); + + return + )} + className="mx_AddExistingToSpaceDialog" + contentId="mx_AddExistingToSpace" + onFinished={onFinished} + fixedWidth={false} + > + + +
{ _t("Want to add a new space instead?") }
+ + { _t("Create a new space") } + + } + filterPlaceholder={_t("Search for spaces")} + spacesRenderer={defaultSpacesRenderer} + /> +
+
; +}; + +export default AddExistingSubspaceDialog; + diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 0f78b971eb..cf4f369d09 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -17,11 +17,10 @@ limitations under the License. import React, { ReactNode, useContext, useMemo, useState } from "react"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; -import { MatrixClient } from "matrix-js-sdk/src/client"; import { sleep } from "matrix-js-sdk/src/utils"; +import { EventType } from "matrix-js-sdk/src/@types/event"; import { _t } from '../../../languageHandler'; -import { IDialogProps } from "./IDialogProps"; import BaseDialog from "./BaseDialog"; import Dropdown from "../elements/Dropdown"; import SearchBox from "../../structures/SearchBox"; @@ -36,20 +35,20 @@ import StyledCheckbox from "../elements/StyledCheckbox"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import ProgressBar from "../elements/ProgressBar"; -import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import QueryMatcher from "../../../autocomplete/QueryMatcher"; import TruncatedList from "../elements/TruncatedList"; import EntityTile from "../rooms/EntityTile"; import BaseAvatar from "../avatars/BaseAvatar"; -interface IProps extends IDialogProps { - matrixClient: MatrixClient; +interface IProps { space: Room; - onCreateRoomClick(cli: MatrixClient, space: Room): void; + onCreateRoomClick(): void; + onAddSubspaceClick(): void; + onFinished(added?: boolean): void; } -const Entry = ({ room, checked, onChange }) => { +export const Entry = ({ room, checked, onChange }) => { return
{ query }
diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 8ccc485d7c..42b21ec743 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -118,9 +118,7 @@ export default class BaseDialog extends React.Component { let headerImage; if (this.props.headerImage) { - headerImage = ; + headerImage = ; } return ( diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 917004dbc7..c5fba52b51 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -14,22 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useState } from "react"; +import React from "react"; -import QuestionDialog from './QuestionDialog'; import { _t } from '../../../languageHandler'; -import Field from "../elements/Field"; -import SdkConfig from "../../../SdkConfig"; import { IDialogProps } from "./IDialogProps"; import SettingsStore from "../../../settings/SettingsStore"; -import { submitFeedback } from "../../../rageshake/submit-rageshake"; -import StyledCheckbox from "../elements/StyledCheckbox"; -import Modal from "../../../Modal"; -import InfoDialog from "./InfoDialog"; import AccessibleButton from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { UserTab } from "./UserSettingsDialog"; +import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog"; + +// XXX: Keep this around for re-use in future Betas interface IProps extends IDialogProps { featureId: string; @@ -38,74 +34,28 @@ interface IProps extends IDialogProps { const BetaFeedbackDialog: React.FC = ({ featureId, onFinished }) => { const info = SettingsStore.getBetaInfo(featureId); - const [comment, setComment] = useState(""); - const [canContact, setCanContact] = useState(false); - - const sendFeedback = async (ok: boolean) => { - if (!ok) return onFinished(false); - - const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => { - o[k] = SettingsStore.getValue(k); - return o; - }, {}); - - submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact, extraData); - onFinished(true); - - Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, { - title: _t("Beta feedback"), - description: _t("Thank you for your feedback, we really appreciate it."), - button: _t("Done"), - hasCloseButton: false, - fixedWidth: false, - }); - }; - - return ( -
- { _t(info.feedbackSubheading) } -   - { _t("Your platform and username will be noted to help us use your feedback as much as we can.") } - - { - onFinished(false); - defaultDispatcher.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Labs, - }); - }}> - { _t("To leave the beta, visit your settings.") } - -
- - { - setComment(ev.target.value); - }} - autoFocus={true} - /> - - setCanContact((e.target as HTMLInputElement).checked)} - > - { _t("You may contact me if you have any follow up questions") } - - } - button={_t("Send feedback")} - buttonDisabled={!comment} - onFinished={sendFeedback} - />); + subheading={_t(info.feedbackSubheading)} + onFinished={onFinished} + rageshakeLabel={info.feedbackLabel} + rageshakeData={Object.fromEntries((SettingsStore.getBetaInfo(featureId)?.extraSettings || []).map(k => { + return SettingsStore.getValue(k); + }))} + > + { + onFinished(false); + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Labs, + }); + }} + > + { _t("To leave the beta, visit your settings.") } + + ; }; export default BetaFeedbackDialog; diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index 64e984fe20..38566cdf04 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -29,11 +29,13 @@ import BaseDialog from "./BaseDialog"; import Field from '../elements/Field'; import Spinner from "../elements/Spinner"; import DialogButtons from "../elements/DialogButtons"; +import { sendSentryReport } from "../../../sentry"; interface IProps { onFinished: (success: boolean) => void; initialText?: string; label?: string; + error?: Error; } interface IState { @@ -113,6 +115,8 @@ export default class BugReportDialog extends React.Component { }); } }); + + sendSentryReport(this.state.text, this.state.issueUrl, this.props.error); }; private onDownload = async (): Promise => { @@ -188,7 +192,9 @@ export default class BugReportDialog extends React.Component { } return ( - @@ -198,8 +204,8 @@ export default class BugReportDialog extends React.Component { { _t( "Debug logs contain application usage data including your " + "username, the IDs or aliases of the rooms or groups you " + - "have visited and the usernames of other users. They do " + - "not contain messages.", + "have visited, which UI elements you last interacted with, " + + "and the usernames of other users. They do not contain messages.", ) }

@@ -209,7 +215,7 @@ export default class BugReportDialog extends React.Component { { a: (sub) => { sub } , diff --git a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx index 73fd4def25..6a8773ce45 100644 --- a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx +++ b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx @@ -205,9 +205,12 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< people.push(( { _t("Show more") } + > + { _t("Show more") } + )); } } @@ -240,10 +243,13 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< { peopleIntro } { people } { buttonText } + > + { buttonText } +

diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx index a2f2b10144..b346d2d44c 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx @@ -37,8 +37,8 @@ export default class ConfirmRedactDialog extends React.Component { "Note that if you delete a room name or topic change, it could undo the change.")} placeholder={_t("Reason (optional)")} focus - button={_t("Remove")}> - + button={_t("Remove")} + /> ); } } diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.tsx b/src/components/views/dialogs/ConfirmUserActionDialog.tsx index cbef474c69..7099556ac6 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.tsx +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -104,7 +104,9 @@ export default class ConfirmUserActionDialog extends React.Component { } return ( - diff --git a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx index 392ab9edad..ccac45fbcc 100644 --- a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx +++ b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx @@ -204,8 +204,10 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
{ } return ( -
@@ -133,8 +135,11 @@ export default class CreateGroupDialog extends React.Component {
- { + private readonly supportsRestricted: boolean; private nameField = createRef(); private aliasField = createRef(); constructor(props) { super(props); + this.supportsRestricted = this.props.parentSpace && !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred; + + let joinRule = JoinRule.Invite; + if (this.props.defaultPublic) { + joinRule = JoinRule.Public; + } else if (this.supportsRestricted) { + joinRule = JoinRule.Restricted; + } + const config = SdkConfig.get(); this.state = { isPublic: this.props.defaultPublic || false, - isEncrypted: privateShouldBeEncrypted(), + isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(), + joinRule, name: this.props.defaultName || "", topic: "", alias: "", @@ -81,13 +96,18 @@ export default class CreateRoomDialog extends React.Component { const opts: IOpts = {}; const createOpts: IOpts["createOpts"] = opts.createOpts = {}; createOpts.name = this.state.name; - if (this.state.isPublic) { + + if (this.state.joinRule === JoinRule.Public) { createOpts.visibility = Visibility.Public; createOpts.preset = Preset.PublicChat; opts.guestAccess = false; const { alias } = this.state; createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1); + } else { + // If we cannot change encryption we pass `true` for safety, the server should automatically do this for us. + opts.encryption = this.state.canChangeEncryption ? this.state.isEncrypted : true; } + if (this.state.topic) { createOpts.topic = this.state.topic; } @@ -95,22 +115,13 @@ export default class CreateRoomDialog extends React.Component { createOpts.creation_content = { 'm.federate': false }; } - if (!this.state.isPublic) { - if (this.state.canChangeEncryption) { - opts.encryption = this.state.isEncrypted; - } else { - // the server should automatically do this for us, but for safety - // we'll demand it too. - opts.encryption = true; - } - } - if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId(); } - if (this.props.parentSpace) { - opts.parentSpace = this.props.parentSpace; + opts.parentSpace = this.props.parentSpace; + if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) { + opts.joinRule = JoinRule.Restricted; } return opts; @@ -172,8 +183,8 @@ export default class CreateRoomDialog extends React.Component { this.setState({ topic: ev.target.value }); }; - private onPublicChange = (isPublic: boolean) => { - this.setState({ isPublic }); + private onJoinRuleChange = (joinRule: JoinRule) => { + this.setState({ joinRule }); }; private onEncryptedChange = (isEncrypted: boolean) => { @@ -210,7 +221,7 @@ export default class CreateRoomDialog extends React.Component { render() { let aliasField; - if (this.state.isPublic) { + if (this.state.joinRule === JoinRule.Public) { const domain = MatrixClientPeg.get().getDomain(); aliasField = (
@@ -224,19 +235,52 @@ export default class CreateRoomDialog extends React.Component { ); } - let publicPrivateLabel =

{ _t( - "Private rooms can be found and joined by invitation only. Public rooms can be " + - "found and joined by anyone.", - ) }

; + let publicPrivateLabel: JSX.Element; if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { - publicPrivateLabel =

{ _t( - "Private rooms can be found and joined by invitation only. Public rooms can be " + - "found and joined by anyone in this community.", - ) }

; + publicPrivateLabel =

+ { _t( + "Private rooms can be found and joined by invitation only. Public rooms can be " + + "found and joined by anyone in this community.", + ) } +

; + } else if (this.state.joinRule === JoinRule.Restricted) { + publicPrivateLabel =

+ { _t( + "Everyone in will be able to find and join this room.", {}, { + SpaceName: () => { this.props.parentSpace.name }, + }, + ) } +   + { _t("You can change this at any time from room settings.") } +

; + } else if (this.state.joinRule === JoinRule.Public && this.props.parentSpace) { + publicPrivateLabel =

+ { _t( + "Anyone will be able to find and join this room, not just members of .", {}, { + SpaceName: () => { this.props.parentSpace.name }, + }, + ) } +   + { _t("You can change this at any time from room settings.") } +

; + } else if (this.state.joinRule === JoinRule.Public) { + publicPrivateLabel =

+ { _t("Anyone will be able to find and join this room.") } +   + { _t("You can change this at any time from room settings.") } +

; + } else if (this.state.joinRule === JoinRule.Invite) { + publicPrivateLabel =

+ { _t( + "Only people invited will be able to find and join this room.", + ) } +   + { _t("You can change this at any time from room settings.") } +

; } let e2eeSection; - if (!this.state.isPublic) { + if (this.state.joinRule !== JoinRule.Public) { let microcopy; if (privateShouldBeEncrypted()) { if (this.state.canChangeEncryption) { @@ -273,15 +317,16 @@ export default class CreateRoomDialog extends React.Component { ); } - let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room'); + let title = _t("Create a room"); if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { const name = CommunityPrototypeStore.instance.getSelectedCommunityName(); title = _t("Create a room in %(communityName)s", { communityName: name }); + } else if (!this.props.parentSpace) { + title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room'); } + return ( - +
{ value={this.state.topic} className="mx_CreateRoomDialog_topic" /> - + { publicPrivateLabel } { e2eeSection } { aliasField } diff --git a/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx new file mode 100644 index 0000000000..4fb0994e23 --- /dev/null +++ b/src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx @@ -0,0 +1,340 @@ +/* +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, { useEffect, useRef, useState } from "react"; +import { JoinRule } from "matrix-js-sdk/src/@types/partials"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { _t } from '../../../languageHandler'; +import BaseDialog from "./BaseDialog"; +import AccessibleButton from "../elements/AccessibleButton"; +import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu"; +import JoinRuleDropdown from "../elements/JoinRuleDropdown"; +import Field from "../elements/Field"; +import RoomAliasField from "../elements/RoomAliasField"; +import { GroupMember } from "../right_panel/UserInfo"; +import { parseMembersResponse, parseRoomsResponse } from "../../../stores/GroupStore"; +import { calculateRoomVia, makeRoomPermalink } from "../../../utils/permalinks/Permalinks"; +import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; +import Spinner from "../elements/Spinner"; +import { mediaFromMxc } from "../../../customisations/Media"; +import SpaceStore from "../../../stores/SpaceStore"; +import Modal from "../../../Modal"; +import InfoDialog from "./InfoDialog"; +import dis from "../../../dispatcher/dispatcher"; +import { Action } from "../../../dispatcher/actions"; +import { UserTab } from "./UserSettingsDialog"; +import TagOrderActions from "../../../actions/TagOrderActions"; + +interface IProps { + matrixClient: MatrixClient; + groupId: string; + onFinished(spaceId?: string): void; +} + +export const CreateEventField = "io.element.migrated_from_community"; + +interface IGroupRoom { + displayname: string; + name?: string; + roomId: string; + canonicalAlias?: string; + avatarUrl?: string; + topic?: string; + numJoinedMembers?: number; + worldReadable?: boolean; + guestCanJoin?: boolean; + isPublic?: boolean; +} + +/* eslint-disable camelcase */ +export interface IGroupSummary { + profile: { + avatar_url?: string; + is_openly_joinable?: boolean; + is_public?: boolean; + long_description: string; + name: string; + short_description: string; + }; + rooms_section: { + rooms: unknown[]; + categories: Record; + total_room_count_estimate: number; + }; + user: { + is_privileged: boolean; + is_public: boolean; + is_publicised: boolean; + membership: string; + }; + users_section: { + users: unknown[]; + roles: Record; + total_user_count_estimate: number; + }; +} +/* eslint-enable camelcase */ + +const CreateSpaceFromCommunityDialog: React.FC = ({ matrixClient: cli, groupId, onFinished }) => { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + + const [avatar, setAvatar] = useState(null); // undefined means to remove avatar + const [name, setName] = useState(""); + const spaceNameField = useRef(); + const [alias, setAlias] = useState("#" + groupId.substring(1, groupId.indexOf(":")) + ":" + cli.getDomain()); + const spaceAliasField = useRef(); + const [topic, setTopic] = useState(""); + const [joinRule, setJoinRule] = useState(JoinRule.Public); + + const groupSummary = useAsyncMemo(() => cli.getGroupSummary(groupId), [groupId]); + useEffect(() => { + if (groupSummary) { + setName(groupSummary.profile.name || ""); + setTopic(groupSummary.profile.short_description || ""); + setJoinRule(groupSummary.profile.is_openly_joinable ? JoinRule.Public : JoinRule.Invite); + setLoading(false); + } + }, [groupSummary]); + + if (loading) { + return ; + } + + const onCreateSpaceClick = async (e) => { + e.preventDefault(); + if (busy) return; + + setError(null); + setBusy(true); + + // require & validate the space name field + if (!await spaceNameField.current.validate({ allowEmpty: false })) { + setBusy(false); + spaceNameField.current.focus(); + spaceNameField.current.validate({ allowEmpty: false, focused: true }); + return; + } + // validate the space name alias field but do not require it + if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) { + setBusy(false); + spaceAliasField.current.focus(); + spaceAliasField.current.validate({ allowEmpty: true, focused: true }); + return; + } + + try { + const [rooms, members, invitedMembers] = await Promise.all([ + cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise, + cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise, + cli.getGroupInvitedUsers(groupId).then(parseMembersResponse) as Promise, + ]); + + const viaMap = new Map(); + for (const { roomId, canonicalAlias } of rooms) { + const room = cli.getRoom(roomId); + if (room) { + viaMap.set(roomId, calculateRoomVia(room)); + } else if (canonicalAlias) { + try { + const { servers } = await cli.getRoomIdForAlias(canonicalAlias); + viaMap.set(roomId, servers); + } catch (e) { + console.warn("Failed to resolve alias during community migration", e); + } + } + + if (!viaMap.get(roomId)?.length) { + // XXX: lets guess the via, this might end up being incorrect. + const str = canonicalAlias || roomId; + viaMap.set(roomId, [str.substring(1, str.indexOf(":"))]); + } + } + + const spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url; + const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, { + creation_content: { + [CreateEventField]: groupId, + }, + initial_state: rooms.map(({ roomId }) => ({ + type: EventType.SpaceChild, + state_key: roomId, + content: { + via: viaMap.get(roomId) || [], + }, + })), + invite: [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId()), + }, { + andView: false, + }); + + // eagerly remove it from the community panel + dis.dispatch(TagOrderActions.removeTag(cli, groupId)); + + // don't bother awaiting this, as we don't hugely care if it fails + cli.setGroupProfile(groupId, { + ...groupSummary.profile, + long_description: `

` + + _t("This community has been upgraded into a Space") + `


` + + groupSummary.profile.long_description, + } as IGroupSummary["profile"]).catch(e => { + console.warn("Failed to update community profile during migration", e); + }); + + onFinished(roomId); + + const onSpaceClick = () => { + dis.dispatch({ + action: "view_room", + room_id: roomId, + }); + }; + + const onPreferencesClick = () => { + dis.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Preferences, + }); + }; + + let spacesDisabledCopy; + if (!SpaceStore.spacesEnabled) { + spacesDisabledCopy = _t("To view Spaces, hide communities in Preferences", {}, { + a: sub => { sub }, + }); + } + + Modal.createDialog(InfoDialog, { + title: _t("Space created"), + description: <> +
+

+ { _t(" has been made and everyone who was a part of the community has " + + "been invited to it.", {}, { + SpaceName: () => + { name } + , + }) } +   + { spacesDisabledCopy } +

+

+ { _t("To create a Space from another community, just pick the community in Preferences.") } +

+ , + button: _t("Preferences"), + onFinished: (openPreferences: boolean) => { + if (openPreferences) { + onPreferencesClick(); + } + }, + }, "mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog"); + } catch (e) { + console.error(e); + setError(e); + } + + setBusy(false); + }; + + let footer; + if (error) { + footer = <> + + + +
{ _t("Failed to migrate community") }
+
{ _t("Try again") }
+
+ + + { _t("Retry") } + + ; + } else { + footer = <> + onFinished()}> + { _t("Cancel") } + + + { busy ? _t("Creating...") : _t("Create Space") } + + ; + } + + return +
+

+ { _t("A link to the Space will be put in your community description.") } +   + { _t("All rooms will be added and all community members will be invited.") } +

+

+ { _t("Flair won't be available in Spaces for the foreseeable future.") } +

+ + +

{ _t("This description will be shown to people when they view your space") }

+ +

{ joinRule === JoinRule.Public + ? _t("Open space for anyone, best for communities") + : _t("Invite only, best for yourself or teams") + }

+ { joinRule !== JoinRule.Public && +
+ } + +
+ +
+ { footer } +
+ ; +}; + +export default CreateSpaceFromCommunityDialog; + diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx new file mode 100644 index 0000000000..d80245918f --- /dev/null +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -0,0 +1,187 @@ +/* +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, { useRef, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { JoinRule } from "matrix-js-sdk/src/@types/partials"; + +import { _t } from '../../../languageHandler'; +import BaseDialog from "./BaseDialog"; +import AccessibleButton from "../elements/AccessibleButton"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { BetaPill } from "../beta/BetaCard"; +import Field from "../elements/Field"; +import RoomAliasField from "../elements/RoomAliasField"; +import SpaceStore from "../../../stores/SpaceStore"; +import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu"; +import { SubspaceSelector } from "./AddExistingToSpaceDialog"; +import JoinRuleDropdown from "../elements/JoinRuleDropdown"; + +interface IProps { + space: Room; + onAddExistingSpaceClick(): void; + onFinished(added?: boolean): void; +} + +const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick, onFinished }) => { + const [parentSpace, setParentSpace] = useState(space); + + const [busy, setBusy] = useState(false); + const [name, setName] = useState(""); + const spaceNameField = useRef(); + const [alias, setAlias] = useState(""); + const spaceAliasField = useRef(); + const [avatar, setAvatar] = useState(null); + const [topic, setTopic] = useState(""); + + const supportsRestricted = !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred; + + const spaceJoinRule = space.getJoinRule(); + let defaultJoinRule = JoinRule.Invite; + if (spaceJoinRule === JoinRule.Public) { + defaultJoinRule = JoinRule.Public; + } else if (supportsRestricted) { + defaultJoinRule = JoinRule.Restricted; + } + const [joinRule, setJoinRule] = useState(defaultJoinRule); + + const onCreateSubspaceClick = async (e) => { + e.preventDefault(); + if (busy) return; + + setBusy(true); + // require & validate the space name field + if (!await spaceNameField.current.validate({ allowEmpty: false })) { + spaceNameField.current.focus(); + spaceNameField.current.validate({ allowEmpty: false, focused: true }); + setBusy(false); + return; + } + // validate the space name alias field but do not require it + if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) { + spaceAliasField.current.focus(); + spaceAliasField.current.validate({ allowEmpty: true, focused: true }); + setBusy(false); + return; + } + + try { + await createSpace(name, joinRule === JoinRule.Public, alias, topic, avatar, {}, { parentSpace, joinRule }); + + onFinished(true); + } catch (e) { + console.error(e); + } + }; + + let joinRuleMicrocopy: JSX.Element; + if (joinRule === JoinRule.Restricted) { + joinRuleMicrocopy =

+ { _t( + "Anyone in will be able to find and join.", {}, { + SpaceName: () => { parentSpace.name }, + }, + ) } +

; + } else if (joinRule === JoinRule.Public) { + joinRuleMicrocopy =

+ { _t( + "Anyone will be able to find and join this space, not just members of .", {}, { + SpaceName: () => { parentSpace.name }, + }, + ) } +

; + } else if (joinRule === JoinRule.Invite) { + joinRuleMicrocopy =

+ { _t("Only people invited will be able to find and join this space.") } +

; + } + + return + )} + className="mx_CreateSubspaceDialog" + contentId="mx_CreateSubspaceDialog" + onFinished={onFinished} + fixedWidth={false} + > + +
+
+ + { _t("Add a space to a space you manage.") } +
+ + + + { joinRuleMicrocopy } + +
+ +
+
+
{ _t("Want to add an existing space instead?") }
+ { + onAddExistingSpaceClick(); + onFinished(); + }} + > + { _t("Add existing space") } + +
+ + onFinished(false)}> + { _t("Cancel") } + + + { busy ? _t("Adding...") : _t("Add") } + +
+
+
; +}; + +export default CreateSubspaceDialog; + diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx index 134c4ab79e..d03b668cd9 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx @@ -72,7 +72,7 @@ const CryptoStoreTooNewDialog: React.FC = (props: IProps) => { hasCancel={false} onPrimaryButtonClick={props.onFinished} > - diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx index 7221df222f..6548bd78fc 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.tsx +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import { AuthType, IAuthData } from 'matrix-js-sdk/src/interactive-auth'; import Analytics from '../../../Analytics'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; @@ -65,7 +66,7 @@ export default class DeactivateAccountDialog extends React.Component { + private onStagePhaseChange = (stage: AuthType, phase: string): void => { const dialogAesthetics = { [SSOAuthEntry.PHASE_PREAUTH]: { body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."), @@ -115,7 +116,10 @@ export default class DeactivateAccountDialog extends React.Component { + private onUIAuthComplete = (auth: IAuthData): void => { + // XXX: this should be returning a promise to maintain the state inside the state machine correct + // but given that a deactivation is followed by a local logout and all object instances being thrown away + // this isn't done. MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => { // Deactivation worked - logout & close this dialog Analytics.trackEvent('Account', 'Deactivate Account'); @@ -180,7 +184,9 @@ export default class DeactivateAccountDialog extends React.Component - +
{ !this.state.message && } { showTglFlip &&
- - +
{ !this.state.message && } { !this.state.message &&
- - + key={this.props.children[0] ? this.props.children[0].key : ''} + /> ; + return ; } return
@@ -594,7 +625,9 @@ class AccountDataExplorer extends React.PureComponent; + }} + forceMode={true} + />; } return
@@ -631,7 +664,9 @@ class AccountDataExplorer extends React.PureComponent
-
@@ -1040,7 +1080,9 @@ class SettingsExplorer extends React.PureComponent this.onViewClick(e, i)}> { i } - this.onEditClick(e, i)} + this.onEditClick(e, i)} className='mx_DevTools_SettingsExplorer_edit' > ✏ @@ -1104,18 +1146,26 @@ class SettingsExplorer extends React.PureComponent
diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx index 1eabb68081..a0e6046d71 100644 --- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx +++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx @@ -144,8 +144,10 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent
{ const [rating, setRating] = useState(""); diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 839ca6da2f..77e2b6ae0c 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -106,12 +106,12 @@ const Entry: React.FC = ({ room, event, matrixClient: cli, onFinish className = "mx_ForwardList_sending"; disabled = true; title = _t("Sending"); - icon =
; + icon =
; } else if (sendState === SendState.Sent) { className = "mx_ForwardList_sent"; disabled = true; title = _t("Sent"); - icon =
; + icon =
; } else { className = "mx_ForwardList_sendFailed"; disabled = true; @@ -204,10 +204,16 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr function overflowTile(overflowCount, totalCount) { const text = _t("and %(count)s others...", { count: overflowCount }); return ( - - } name={text} presenceState="online" suppressOnHover={true} - onClick={() => setTruncateAt(totalCount)} /> + + } + name={text} + presenceState="online" + suppressOnHover={true} + onClick={() => setTruncateAt(totalCount)} + /> ); } diff --git a/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx new file mode 100644 index 0000000000..d68569b126 --- /dev/null +++ b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx @@ -0,0 +1,101 @@ +/* +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, { useState } from "react"; + +import QuestionDialog from './QuestionDialog'; +import { _t } from '../../../languageHandler'; +import Field from "../elements/Field"; +import SdkConfig from "../../../SdkConfig"; +import { IDialogProps } from "./IDialogProps"; +import { submitFeedback } from "../../../rageshake/submit-rageshake"; +import StyledCheckbox from "../elements/StyledCheckbox"; +import Modal from "../../../Modal"; +import InfoDialog from "./InfoDialog"; + +interface IProps extends IDialogProps { + title: string; + subheading: string; + rageshakeLabel: string; + rageshakeData?: Record; +} + +const GenericFeatureFeedbackDialog: React.FC = ({ + title, + subheading, + children, + rageshakeLabel, + rageshakeData = {}, + onFinished, +}) => { + const [comment, setComment] = useState(""); + const [canContact, setCanContact] = useState(false); + + const sendFeedback = async (ok: boolean) => { + if (!ok) return onFinished(false); + + submitFeedback(SdkConfig.get().bug_report_endpoint_url, rageshakeLabel, comment, canContact, rageshakeData); + onFinished(true); + + Modal.createTrackedDialog("Feedback Sent", rageshakeLabel, InfoDialog, { + title, + description: _t("Thank you for your feedback, we really appreciate it."), + button: _t("Done"), + hasCloseButton: false, + fixedWidth: false, + }); + }; + + return ( +
+ { subheading } +   + { _t("Your platform and username will be noted to help us use your feedback as much as we can.") } + + { children } +
+ + { + setComment(ev.target.value); + }} + autoFocus={true} + /> + + setCanContact((e.target as HTMLInputElement).checked)} + > + { _t("You may contact me if you have any follow up questions") } + + } + button={_t("Send feedback")} + buttonDisabled={!comment} + onFinished={sendFeedback} + />); +}; + +export default GenericFeatureFeedbackDialog; diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index 963d98ef3f..a5c9f2107f 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -133,18 +133,23 @@ export default class IncomingSasDialog extends React.Component { ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(48) : null; profile =
-

{ oppProfile.displayname }

; } else if (this.state.opponentProfileError) { profile =
-

{ this.props.verifier.userId }

; diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.tsx similarity index 74% rename from src/components/views/dialogs/InfoDialog.js rename to src/components/views/dialogs/InfoDialog.tsx index 8207d334d3..9c74e08e98 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.tsx @@ -1,7 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd. Copyright 2019 Bastian Masanek, Noxware IT +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. @@ -16,31 +15,31 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; -import { _t } from '../../../languageHandler'; +import React, { ReactNode, KeyboardEvent } from 'react'; import classNames from "classnames"; -export default class InfoDialog extends React.Component { - static propTypes = { - className: PropTypes.string, - title: PropTypes.string, - description: PropTypes.node, - button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - onFinished: PropTypes.func, - hasCloseButton: PropTypes.bool, - onKeyDown: PropTypes.func, - fixedWidth: PropTypes.bool, - }; +import { _t } from '../../../languageHandler'; +import * as sdk from '../../../index'; +import { IDialogProps } from "./IDialogProps"; +interface IProps extends IDialogProps { + title?: string; + description?: ReactNode; + className?: string; + button?: boolean | string; + hasCloseButton?: boolean; + fixedWidth?: boolean; + onKeyDown?(event: KeyboardEvent): void; +} + +export default class InfoDialog extends React.Component { static defaultProps = { title: '', description: '', hasCloseButton: false, }; - onFinished = () => { + private onFinished = () => { this.props.onFinished(); }; @@ -63,8 +62,7 @@ export default class InfoDialog extends React.Component { { this.props.button !== false && - } + /> } ); } diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 46f140fd39..1568e06720 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -55,7 +55,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; import { getAddressType } from "../../../UserAddress"; import BaseAvatar from '../avatars/BaseAvatar'; -import AccessibleButton from '../elements/AccessibleButton'; +import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton'; import { compare } from '../../../utils/strings'; import { IInvite3PID } from "matrix-js-sdk/src/@types/requests"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; @@ -196,7 +196,9 @@ class DMUserTile extends React.PureComponent { ? + width={avatarSize} + height={avatarSize} + /> : { className='mx_InviteDialog_userTile_remove' onClick={this.onRemove} > - {_t('Remove')} ); @@ -297,7 +302,9 @@ class DMRoomTile extends React.PureComponent { const avatar = (this.props.member as ThreepidMember).isEmail ? + width={avatarSize} + height={avatarSize} + /> : void; private debounceTimer: number = null; // actually number because we're in the browser private editorRef = createRef(); + private numberEntryFieldRef: React.RefObject = createRef(); private unmounted = false; constructor(props) { @@ -1276,13 +1284,27 @@ export default class InviteDialog extends React.PureComponent { + private onDigitPress = (digit: string, ev: ButtonEvent) => { this.setState({ dialPadValue: this.state.dialPadValue + digit }); + + // Keep the number field focused so that keyboard entry is still available + // However, don't focus if this wasn't the result of directly clicking on the button, + // i.e someone using keyboard navigation. + if (ev.type === "click") { + this.numberEntryFieldRef.current?.focus(); + } }; - private onDeletePress = () => { + private onDeletePress = (ev: ButtonEvent) => { if (this.state.dialPadValue.length === 0) return; this.setState({ dialPadValue: this.state.dialPadValue.slice(0, -1) }); + + // Keep the number field focused so that keyboard entry is still available + // However, don't focus if this wasn't the result of directly clicking on the button, + // i.e someone using keyboard navigation. + if (ev.type === "click") { + this.numberEntryFieldRef.current?.focus(); + } }; private onTabChange = (tabId: TabId) => { @@ -1458,7 +1480,8 @@ export default class InviteDialog extends React.PureComponent + width={14} + height={14} /> { " " + _t("Invited people will be able to read old messages.") }

; } @@ -1534,14 +1557,20 @@ export default class InviteDialog extends React.PureComponent; } else { - dialPadField = { dialPadField } -
; tabs.push(new Tab(TabId.DialPad, _td("Dial pad"), 'mx_InviteDialog_dialPadIcon', dialPadSection)); dialogContent = - { consultConnectSection } ; diff --git a/src/components/views/dialogs/KeySignatureUploadFailedDialog.js b/src/components/views/dialogs/KeySignatureUploadFailedDialog.js index 9fa9fc1373..6b36c19977 100644 --- a/src/components/views/dialogs/KeySignatureUploadFailedDialog.js +++ b/src/components/views/dialogs/KeySignatureUploadFailedDialog.js @@ -20,10 +20,10 @@ import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; export default function KeySignatureUploadFailedDialog({ - failures, - source, - continuation, - onFinished, + failures, + source, + continuation, + onFinished, }) { const RETRIES = 2; const BaseDialog = sdk.getComponent('dialogs.BaseDialog'); diff --git a/src/components/views/dialogs/LeaveSpaceDialog.tsx b/src/components/views/dialogs/LeaveSpaceDialog.tsx new file mode 100644 index 0000000000..3a8cd53945 --- /dev/null +++ b/src/components/views/dialogs/LeaveSpaceDialog.tsx @@ -0,0 +1,197 @@ +/* +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, { useEffect, useMemo, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { JoinRule } from "matrix-js-sdk/src/@types/partials"; + +import { _t } from '../../../languageHandler'; +import DialogButtons from "../elements/DialogButtons"; +import BaseDialog from "../dialogs/BaseDialog"; +import SpaceStore from "../../../stores/SpaceStore"; +import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; +import { Entry } from "./AddExistingToSpaceDialog"; +import SearchBox from "../../structures/SearchBox"; +import QueryMatcher from "../../../autocomplete/QueryMatcher"; +import StyledRadioGroup from "../elements/StyledRadioGroup"; + +enum RoomsToLeave { + All = "All", + Specific = "Specific", + None = "None", +} + +const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => { + const [query, setQuery] = useState(""); + const lcQuery = query.toLowerCase().trim(); + + const filteredRooms = useMemo(() => { + if (!lcQuery) { + return rooms; + } + + const matcher = new QueryMatcher(rooms, { + keys: ["name"], + funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)], + shouldMatchWordsOnly: false, + }); + + return matcher.match(lcQuery); + }, [rooms, lcQuery]); + + return
+ + + { filteredRooms.map(room => { + return { + onChange(checked, room); + }} + />; + }) } + { filteredRooms.length < 1 ? + { _t("No results") } + : undefined } + +
; +}; + +const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave }) => { + const selected = useMemo(() => new Set(roomsToLeave), [roomsToLeave]); + const [state, setState] = useState(RoomsToLeave.None); + + useEffect(() => { + if (state === RoomsToLeave.All) { + setRoomsToLeave(spaceChildren); + } else { + setRoomsToLeave([]); + } + }, [setRoomsToLeave, state, spaceChildren]); + + return
+ + + { state === RoomsToLeave.Specific && ( + { + if (selected) { + setRoomsToLeave([room, ...roomsToLeave]); + } else { + setRoomsToLeave(roomsToLeave.filter(r => r !== room)); + } + }} + /> + ) } +
; +}; + +interface IProps { + space: Room; + onFinished(leave: boolean, rooms?: Room[]): void; +} + +const isOnlyAdmin = (room: Room): boolean => { + return !room.getJoinedMembers().some(member => { + return member.userId !== room.client.credentials.userId && member.powerLevelNorm === 100; + }); +}; + +const LeaveSpaceDialog: React.FC = ({ space, onFinished }) => { + const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]); + const [roomsToLeave, setRoomsToLeave] = useState([]); + + let rejoinWarning; + if (space.getJoinRule() !== JoinRule.Public) { + rejoinWarning = _t("You won't be able to rejoin unless you are re-invited."); + } + + let onlyAdminWarning; + if (isOnlyAdmin(space)) { + onlyAdminWarning = _t("You're the only admin of this space. " + + "Leaving it will mean no one has control over it."); + } else { + const numChildrenOnlyAdminIn = roomsToLeave.filter(isOnlyAdmin).length; + if (numChildrenOnlyAdminIn > 0) { + onlyAdminWarning = _t("You're the only admin of some of the rooms or spaces you wish to leave. " + + "Leaving them will leave them without any admins."); + } + } + + return onFinished(false)} + fixedWidth={false} + > +
+

+ { _t("Are you sure you want to leave ?", {}, { + spaceName: () => { space.name }, + }) } +   + { rejoinWarning } +

+ + { spaceChildren.length > 0 && } + + { onlyAdminWarning &&
+ { onlyAdminWarning } +
} +
+ onFinished(true, roomsToLeave)} + hasCancel={true} + onCancel={onFinished} + /> +
; +}; + +export default LeaveSpaceDialog; diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.tsx similarity index 82% rename from src/components/views/dialogs/LogoutDialog.js rename to src/components/views/dialogs/LogoutDialog.tsx index 469cd48093..8c035dcbba 100644 --- a/src/components/views/dialogs/LogoutDialog.js +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import Modal from '../../../Modal'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; @@ -24,19 +25,25 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + onFinished: (success: boolean) => void; +} + +interface IState { + shouldLoadBackupStatus: boolean; + loading: boolean; + backupInfo: IKeyBackupInfo; + error?: string; +} + @replaceableComponent("views.dialogs.LogoutDialog") -export default class LogoutDialog extends React.Component { - defaultProps = { +export default class LogoutDialog extends React.Component { + static defaultProps = { onFinished: function() {}, }; - constructor() { - super(); - this._onSettingsLinkClick = this._onSettingsLinkClick.bind(this); - this._onExportE2eKeysClicked = this._onExportE2eKeysClicked.bind(this); - this._onFinished = this._onFinished.bind(this); - this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this); - this._onLogoutConfirm = this._onLogoutConfirm.bind(this); + constructor(props) { + super(props); const cli = MatrixClientPeg.get(); const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled(); @@ -49,11 +56,11 @@ export default class LogoutDialog extends React.Component { }; if (shouldLoadBackupStatus) { - this._loadBackupStatus(); + this.loadBackupStatus(); } } - async _loadBackupStatus() { + private async loadBackupStatus() { try { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); this.setState({ @@ -69,29 +76,29 @@ export default class LogoutDialog extends React.Component { } } - _onSettingsLinkClick() { + private onSettingsLinkClick = (): void => { // close dialog - this.props.onFinished(); - } + this.props.onFinished(true); + }; - _onExportE2eKeysClicked() { + private onExportE2eKeysClicked = (): void => { Modal.createTrackedDialogAsync('Export E2E Keys', '', import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), { matrixClient: MatrixClientPeg.get(), }, ); - } + }; - _onFinished(confirmed) { + private onFinished = (confirmed: boolean): void => { if (confirmed) { dis.dispatch({ action: 'logout' }); } // close dialog - this.props.onFinished(); - } + this.props.onFinished(confirmed); + }; - _onSetRecoveryMethodClick() { + private onSetRecoveryMethodClick = (): void => { if (this.state.backupInfo) { // A key backup exists for this account, but the creating device is not // verified, so restore the backup which will give us the keys from it and @@ -108,15 +115,15 @@ export default class LogoutDialog extends React.Component { } // close dialog - this.props.onFinished(); - } + this.props.onFinished(true); + }; - _onLogoutConfirm() { + private onLogoutConfirm = (): void => { dis.dispatch({ action: 'logout' }); // close dialog - this.props.onFinished(); - } + this.props.onFinished(true); + }; render() { if (this.state.shouldLoadBackupStatus) { @@ -152,16 +159,16 @@ export default class LogoutDialog extends React.Component {
-
{ _t("Advanced") } -

@@ -174,7 +181,7 @@ export default class LogoutDialog extends React.Component { title={_t("You'll lose access to your encrypted messages")} contentId='mx_Dialog_content' hasCancel={true} - onFinished={this._onFinished} + onFinished={this.onFinished} > { dialogContent } ); @@ -187,7 +194,7 @@ export default class LogoutDialog extends React.Component { "Are you sure you want to sign out?", )} button={_t("Sign out")} - onFinished={this._onFinished} + onFinished={this.onFinished} />); } } diff --git a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx new file mode 100644 index 0000000000..c63fdc4c84 --- /dev/null +++ b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx @@ -0,0 +1,192 @@ +/* +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, { useMemo, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { _t } from '../../../languageHandler'; +import { IDialogProps } from "./IDialogProps"; +import BaseDialog from "./BaseDialog"; +import SearchBox from "../../structures/SearchBox"; +import SpaceStore from "../../../stores/SpaceStore"; +import RoomAvatar from "../avatars/RoomAvatar"; +import AccessibleButton from "../elements/AccessibleButton"; +import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; +import StyledCheckbox from "../elements/StyledCheckbox"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; + +interface IProps extends IDialogProps { + room: Room; + selected?: string[]; +} + +const Entry = ({ room, checked, onChange }) => { + const localRoom = room instanceof Room; + + let description; + if (localRoom) { + description = _t("%(count)s members", { count: room.getJoinedMemberCount() }); + const numChildRooms = SpaceStore.instance.getChildRooms(room.roomId).length; + if (numChildRooms > 0) { + description += " · " + _t("%(count)s rooms", { count: numChildRooms }); + } + } + + return ; +}; + +const ManageRestrictedJoinRuleDialog: React.FC = ({ room, selected = [], onFinished }) => { + const cli = room.client; + const [newSelected, setNewSelected] = useState(new Set(selected)); + const [query, setQuery] = useState(""); + const lcQuery = query.toLowerCase().trim(); + + const [spacesContainingRoom, otherEntries] = useMemo(() => { + const spaces = cli.getVisibleRooms().filter(r => r.getMyMembership() === "join" && r.isSpaceRoom()); + return [ + spaces.filter(r => SpaceStore.instance.getSpaceFilteredRoomIds(r).has(room.roomId)), + selected.map(roomId => { + const room = cli.getRoom(roomId); + if (!room) { + return { roomId, name: roomId } as Room; + } + if (room.getMyMembership() !== "join" || !room.isSpaceRoom()) { + return room; + } + }).filter(Boolean), + ]; + }, [cli, selected, room.roomId]); + + const [filteredSpacesContainingRooms, filteredOtherEntries] = useMemo(() => [ + spacesContainingRoom.filter(r => r.name.toLowerCase().includes(lcQuery)), + otherEntries.filter(r => r.name.toLowerCase().includes(lcQuery)), + ], [spacesContainingRoom, otherEntries, lcQuery]); + + const onChange = (checked: boolean, room: Room): void => { + if (checked) { + newSelected.add(room.roomId); + } else { + newSelected.delete(room.roomId); + } + setNewSelected(new Set(newSelected)); + }; + + let inviteOnlyWarning; + if (newSelected.size < 1) { + inviteOnlyWarning =
+ { _t("You're removing all spaces. Access will default to invite only") } +
; + } + + return +

+ { _t("Decide which spaces can access this room. " + + "If a space is selected, its members can find and join .", {}, { + RoomName: () => { room.name }, + }) } +

+ + + + { filteredSpacesContainingRooms.length > 0 ? ( +
+

{ _t("Spaces you know that contain this room") }

+ { filteredSpacesContainingRooms.map(space => { + return { + onChange(checked, space); + }} + />; + }) } +
+ ) : undefined } + + { filteredOtherEntries.length > 0 ? ( +
+

{ _t("Other spaces or rooms you might not know") }

+
+
{ _t("These are likely ones other room admins are a part of.") }
+
+ { filteredOtherEntries.map(space => { + return { + onChange(checked, space); + }} + />; + }) } +
+ ) : null } + + { filteredSpacesContainingRooms.length + filteredOtherEntries.length < 1 + ? + { _t("No results") } + + : undefined + } +
+ +
+ { inviteOnlyWarning } +
+ onFinished()}> + { _t("Cancel") } + + onFinished(Array.from(newSelected))}> + { _t("Confirm") } + +
+
+
+
; +}; + +export default ManageRestrictedJoinRuleDialog; + diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index a426dce5c7..a73f0a595b 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -79,7 +79,10 @@ export default class RoomSettingsDialog extends React.Component { ROOM_SECURITY_TAB, _td("Security & Privacy"), "mx_RoomSettingsDialog_securityIcon", - , + this.props.onFinished(true)} + />, )); tabs.push(new Tab( ROOM_ROLES_TAB, diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.tsx similarity index 66% rename from src/components/views/dialogs/RoomUpgradeDialog.js rename to src/components/views/dialogs/RoomUpgradeDialog.tsx index e48f728383..bcca0e3829 100644 --- a/src/components/views/dialogs/RoomUpgradeDialog.js +++ b/src/components/views/dialogs/RoomUpgradeDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +Copyright 2018 - 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,19 +15,29 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { Room } from "matrix-js-sdk/src/models/room"; + import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { upgradeRoom } from "../../../utils/RoomUpgrade"; +import { IDialogProps } from "./IDialogProps"; +import BaseDialog from "./BaseDialog"; +import ErrorDialog from './ErrorDialog'; +import DialogButtons from '../elements/DialogButtons'; +import Spinner from "../elements/Spinner"; + +interface IProps extends IDialogProps { + room: Room; +} + +interface IState { + busy: boolean; +} @replaceableComponent("views.dialogs.RoomUpgradeDialog") -export default class RoomUpgradeDialog extends React.Component { - static propTypes = { - room: PropTypes.object.isRequired, - onFinished: PropTypes.func.isRequired, - }; +export default class RoomUpgradeDialog extends React.Component { + private targetVersion: string; state = { busy: true, @@ -35,20 +45,19 @@ export default class RoomUpgradeDialog extends React.Component { async componentDidMount() { const recommended = await this.props.room.getRecommendedVersion(); - this._targetVersion = recommended.version; + this.targetVersion = recommended.version; this.setState({ busy: false }); } - _onCancelClick = () => { + private onCancelClick = (): void => { this.props.onFinished(false); }; - _onUpgradeClick = () => { + private onUpgradeClick = (): void => { this.setState({ busy: true }); - MatrixClientPeg.get().upgradeRoom(this.props.room.roomId, this._targetVersion).then(() => { + upgradeRoom(this.props.room, this.targetVersion, false, false).then(() => { this.props.onFinished(true); }).catch((err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to upgrade room', '', ErrorDialog, { title: _t("Failed to upgrade room"), description: ((err && err.message) ? err.message : _t("The room upgrade could not be completed")), @@ -59,29 +68,22 @@ export default class RoomUpgradeDialog extends React.Component { }; render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Spinner = sdk.getComponent('views.elements.Spinner'); - let buttons; if (this.state.busy) { buttons = ; } else { buttons = ; } return ( -
  • { _t("Create a new room with the same name, description and avatar") }
  • { _t("Update any local room aliases to point to the new room") }
  • -
  • { _t("Stop users from speaking in the old version of the room, and post a message advising users to move to the new room") }
  • -
  • { _t("Put a link back to the old room at the start of the new room so people can see old messages") }
  • +
  • { _t("Stop users from speaking in the old version of the room, " + + "and post a message advising users to move to the new room") }
  • +
  • { _t("Put a link back to the old room at the start of the new room " + + "so people can see old messages") }
  • { buttons }
    diff --git a/src/components/views/dialogs/RoomUpgradeWarningDialog.js b/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx similarity index 64% rename from src/components/views/dialogs/RoomUpgradeWarningDialog.js rename to src/components/views/dialogs/RoomUpgradeWarningDialog.tsx index 29adb261be..a90f417959 100644 --- a/src/components/views/dialogs/RoomUpgradeWarningDialog.js +++ b/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,73 +14,82 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { ReactNode } from 'react'; +import { EventType } from 'matrix-js-sdk/src/@types/event'; +import { JoinRule } from 'matrix-js-sdk/src/@types/partials'; + import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; -import * as sdk from "../../../index"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Modal from "../../../Modal"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IDialogProps } from "./IDialogProps"; +import BugReportDialog from './BugReportDialog'; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; + +interface IProps extends IDialogProps { + roomId: string; + targetVersion: string; + description?: ReactNode; +} + +interface IState { + inviteUsersToNewRoom: boolean; +} @replaceableComponent("views.dialogs.RoomUpgradeWarningDialog") -export default class RoomUpgradeWarningDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - roomId: PropTypes.string.isRequired, - targetVersion: PropTypes.string.isRequired, - }; +export default class RoomUpgradeWarningDialog extends React.Component { + private readonly isPrivate: boolean; + private readonly currentVersion: string; constructor(props) { super(props); const room = MatrixClientPeg.get().getRoom(this.props.roomId); - const joinRules = room ? room.currentState.getStateEvents("m.room.join_rules", "") : null; - const isPrivate = joinRules ? joinRules.getContent()['join_rule'] !== 'public' : true; + const joinRules = room?.currentState.getStateEvents(EventType.RoomJoinRules, ""); + this.isPrivate = joinRules?.getContent()['join_rule'] !== JoinRule.Public ?? true; + this.currentVersion = room?.getVersion() || "1"; + this.state = { - currentVersion: room ? room.getVersion() : "1", - isPrivate, inviteUsersToNewRoom: true, }; } - _onContinue = () => { - this.props.onFinished({ continue: true, invite: this.state.isPrivate && this.state.inviteUsersToNewRoom }); + private onContinue = () => { + this.props.onFinished({ continue: true, invite: this.isPrivate && this.state.inviteUsersToNewRoom }); }; - _onCancel = () => { + private onCancel = () => { this.props.onFinished({ continue: false, invite: false }); }; - _onInviteUsersToggle = (newVal) => { - this.setState({ inviteUsersToNewRoom: newVal }); + private onInviteUsersToggle = (inviteUsersToNewRoom: boolean) => { + this.setState({ inviteUsersToNewRoom }); }; - _openBugReportDialog = (e) => { + private openBugReportDialog = (e) => { e.preventDefault(); e.stopPropagation(); - const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); }; render() { const brand = SdkConfig.get().brand; - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); let inviteToggle = null; - if (this.state.isPrivate) { + if (this.isPrivate) { inviteToggle = ( + onChange={this.onInviteUsersToggle} + label={_t("Automatically invite members from this room to the new one")} /> ); } - const title = this.state.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room"); + const title = this.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room"); let bugReports = (

    @@ -101,7 +110,7 @@ export default class RoomUpgradeWarningDialog extends React.Component { }, { "a": (sub) => { - return { sub }; + return { sub }; }, }, ) } @@ -119,18 +128,26 @@ export default class RoomUpgradeWarningDialog extends React.Component { >

    - { _t( + { this.props.description || _t( "Upgrading a room is an advanced action and is usually recommended when a room " + "is unstable due to bugs, missing features or security vulnerabilities.", ) }

    +

    + { _t( + "Please note upgrading will make a new version of the room. " + + "All current messages will stay in this archived room.", {}, { + b: sub => { sub }, + }, + ) } +

    { bugReports }

    { _t( "You'll upgrade this room from to .", {}, { - oldVersion: () => { this.state.currentVersion }, + oldVersion: () => { this.currentVersion }, newVersion: () => { this.props.targetVersion }, }, ) } @@ -139,9 +156,9 @@ export default class RoomUpgradeWarningDialog extends React.Component {

    ); diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index b7037d1f4f..eeeadbbfe5 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -85,7 +85,9 @@ export default class SessionRestoreErrorDialog extends React.Component { } return ( - { if (this.state.linkSpecificEvent) { matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId()); } else { - matrixToUrl = this.props.permalinkCreator.forRoom(); + matrixToUrl = this.props.permalinkCreator.forShareableRoom(); } } return matrixToUrl; diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx index fe836ebc5c..7abcb0eceb 100644 --- a/src/components/views/dialogs/SpaceSettingsDialog.tsx +++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx @@ -29,10 +29,12 @@ import SpaceSettingsVisibilityTab from "../spaces/SpaceSettingsVisibilityTab"; import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab"; +import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab"; export enum SpaceSettingsTab { General = "SPACE_GENERAL_TAB", Visibility = "SPACE_VISIBILITY_TAB", + Roles = "SPACE_ROLES_TAB", Advanced = "SPACE_ADVANCED_TAB", } @@ -60,7 +62,13 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin SpaceSettingsTab.Visibility, _td("Visibility"), "mx_SpaceSettingsDialog_visibilityIcon", - , + , + ), + new Tab( + SpaceSettingsTab.Roles, + _td("Roles & Permissions"), + "mx_RoomSettingsDialog_rolesIcon", + , ), SettingsStore.getValue(UIFeature.AdvancedSettings) ? new Tab( diff --git a/src/components/views/dialogs/StorageEvictedDialog.js b/src/components/views/dialogs/StorageEvictedDialog.js index 124e1763c9..507ee09e75 100644 --- a/src/components/views/dialogs/StorageEvictedDialog.js +++ b/src/components/views/dialogs/StorageEvictedDialog.js @@ -54,7 +54,9 @@ export default class StorageEvictedDialog extends React.Component { } return ( - UserTab.Preferences, _td("Preferences"), "mx_UserSettingsDialog_preferencesIcon", - , + , )); if (SettingsStore.getValue(UIFeature.Voip)) { diff --git a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx index ebeab191b1..366adb887c 100644 --- a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx +++ b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx @@ -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. @@ -20,6 +20,7 @@ import { _t } from "../../../languageHandler"; import { IDialogProps } from "./IDialogProps"; import { Capability, + isTimelineCapability, Widget, WidgetEventCapability, WidgetKind, @@ -30,14 +31,7 @@ import DialogButtons from "../elements/DialogButtons"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import { CapabilityText } from "../../../widgets/CapabilityText"; import { replaceableComponent } from "../../../utils/replaceableComponent"; - -export function getRememberedCapabilitiesForWidget(widget: Widget): Capability[] { - return JSON.parse(localStorage.getItem(`widget_${widget.id}_approved_caps`) || "[]"); -} - -function setRememberedCapabilitiesForWidget(widget: Widget, caps: Capability[]) { - localStorage.setItem(`widget_${widget.id}_approved_caps`, JSON.stringify(caps)); -} +import { lexicographicCompare } from "matrix-js-sdk/src/utils"; interface IProps extends IDialogProps { requestedCapabilities: Set; @@ -95,14 +89,24 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent< }; private closeAndTryRemember(approved: Capability[]) { - if (this.state.rememberSelection) { - setRememberedCapabilitiesForWidget(this.props.widget, approved); - } - this.props.onFinished({ approved }); + this.props.onFinished({ approved, remember: this.state.rememberSelection }); } public render() { - const checkboxRows = Object.entries(this.state.booleanStates).map(([cap, isChecked], i) => { + // We specifically order the timeline capabilities down to the bottom. The capability text + // generation cares strongly about this. + const orderedCapabilities = Object.entries(this.state.booleanStates).sort(([capA], [capB]) => { + const isTimelineA = isTimelineCapability(capA); + const isTimelineB = isTimelineCapability(capB); + + if (!isTimelineA && !isTimelineB) return lexicographicCompare(capA, capB); + if (isTimelineA && !isTimelineB) return 1; + if (!isTimelineA && isTimelineB) return -1; + if (isTimelineA && isTimelineB) return lexicographicCompare(capA, capB); + + return 0; + }); + const checkboxRows = orderedCapabilities.map(([cap, isChecked], i) => { const text = CapabilityText.for(cap, this.props.widgetKind); const byline = text.byline ? { text.byline } diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx similarity index 71% rename from src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js rename to src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx index 1bc6444ac1..7993f9c418 100644 --- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx @@ -1,5 +1,6 @@ /* Copyright 2019 Travis Ralston +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. @@ -15,42 +16,46 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { _t } from "../../../languageHandler"; -import * as sdk from "../../../index"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; -import { Widget } from "matrix-widget-api"; +import { Widget, WidgetKind } from "matrix-widget-api"; import { OIDCState, WidgetPermissionStore } from "../../../stores/widgets/WidgetPermissionStore"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IDialogProps } from "./IDialogProps"; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; + +interface IProps extends IDialogProps { + widget: Widget; + widgetKind: WidgetKind; + inRoomId?: string; +} + +interface IState { + rememberSelection: boolean; +} @replaceableComponent("views.dialogs.WidgetOpenIDPermissionsDialog") -export default class WidgetOpenIDPermissionsDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - widget: PropTypes.objectOf(Widget).isRequired, - widgetKind: PropTypes.string.isRequired, // WidgetKind from widget-api - inRoomId: PropTypes.string, - }; - - constructor() { - super(); +export default class WidgetOpenIDPermissionsDialog extends React.PureComponent { + constructor(props: IProps) { + super(props); this.state = { rememberSelection: false, }; } - _onAllow = () => { - this._onPermissionSelection(true); + private onAllow = () => { + this.onPermissionSelection(true); }; - _onDeny = () => { - this._onPermissionSelection(false); + private onDeny = () => { + this.onPermissionSelection(false); }; - _onPermissionSelection(allowed) { + private onPermissionSelection(allowed: boolean) { if (this.state.rememberSelection) { - console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`); + console.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`); WidgetPermissionStore.instance.setOIDCState( this.props.widget, this.props.widgetKind, this.props.inRoomId, @@ -61,14 +66,11 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component { this.props.onFinished(allowed); } - _onRememberSelectionChange = (newVal) => { + private onRememberSelectionChange = (newVal: boolean) => { this.setState({ rememberSelection: newVal }); }; - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - + public render() { return ( } /> diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index b425f37dce..de0a0e1f18 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -287,7 +287,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent { _t("Forgotten or lost all recovery methods? Reset all", null, { a: (sub) => { sub }, }) }
    diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js index e8bd8af01c..2b272a3b88 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js @@ -399,7 +399,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { let keyStatus; if (this.state.recoveryKey.length === 0) { - keyStatus =
    ; + keyStatus =
    ; } else if (this.state.recoveryKeyValid) { keyStatus =
    { "\uD83D\uDC4D " }{ _t("This looks like a valid Security Key!") } diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 8bb6341c3d..75b6890112 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -19,7 +19,7 @@ import React, { ReactHTML } from 'react'; import { Key } from '../../../Keyboard'; import classnames from 'classnames'; -export type ButtonEvent = React.MouseEvent | React.KeyboardEvent; +export type ButtonEvent = React.MouseEvent | React.KeyboardEvent | React.FormEvent; /** * children: React's magic prop. Represents all children given to the element. @@ -39,7 +39,7 @@ interface IProps extends React.InputHTMLAttributes { tabIndex?: number; disabled?: boolean; className?: string; - onClick(e?: ButtonEvent): void; + onClick(e?: ButtonEvent): void | Promise; } interface IAccessibleButtonProps extends React.InputHTMLAttributes { @@ -67,7 +67,9 @@ export default function AccessibleButton({ ...restProps }: IProps) { const newProps: IAccessibleButtonProps = restProps; - if (!disabled) { + if (disabled) { + newProps["aria-disabled"] = true; + } else { newProps.onClick = onClick; // We need to consume enter onKeyDown and space onKeyUp // otherwise we are risking also activating other keyboard focusable elements @@ -118,7 +120,7 @@ export default function AccessibleButton({ ); // React.createElement expects InputHTMLAttributes - return React.createElement(element, restProps, children); + return React.createElement(element, newProps, children); } AccessibleButton.defaultProps = { diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 8ac41ad1a2..d2a4801a2d 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -25,6 +25,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; interface ITooltipProps extends React.ComponentProps { title: string; tooltip?: React.ReactNode; + label?: React.ReactNode; tooltipClassName?: string; forceHide?: boolean; yOffset?: number; @@ -84,7 +85,8 @@ export default class AccessibleTooltipButton extends React.PureComponent { children } - { tip } + { this.props.label } + { (tooltip || title) && tip } ); } diff --git a/src/components/views/elements/AddressTile.tsx b/src/components/views/elements/AddressTile.tsx index cdeb7cacd6..52c0d84ac2 100644 --- a/src/components/views/elements/AddressTile.tsx +++ b/src/components/views/elements/AddressTile.tsx @@ -122,7 +122,7 @@ export default class AddressTile extends React.Component { let dismiss; if (this.props.canDismiss) { dismiss = ( -
    +
    ); diff --git a/src/components/views/elements/AppPermission.tsx b/src/components/views/elements/AppPermission.tsx index 8dc874381a..c0543eb363 100644 --- a/src/components/views/elements/AppPermission.tsx +++ b/src/components/views/elements/AppPermission.tsx @@ -62,10 +62,10 @@ export default class AppPermission extends React.Component { // Set all this into the initial state this.state = { - ...urlInfo, - roomMember, - isWrapped: null, widgetDomain: null, + isWrapped: null, + roomMember, + ...urlInfo, }; } diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 74ef178066..a02465d01e 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -218,6 +218,7 @@ export default class AppTile extends React.Component { // Delete the widget from the persisted store for good measure. PersistedElement.destroyElement(this._persistKey); + ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); if (this._sgWidget) this._sgWidget.stop({ forceDestroy: true }); } @@ -307,7 +308,6 @@ export default class AppTile extends React.Component { if (this.iframe) { // Reload iframe this.iframe.src = this._sgWidget.embedUrl; - this.setState({}); } }); } @@ -333,7 +333,7 @@ export default class AppTile extends React.Component { // this would only be for content hosted on the same origin as the element client: anything // hosted on the same origin as the client will get the same access as if you clicked // a link to it. - const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+ + const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox " + "allow-same-origin allow-scripts allow-presentation"; // Additional iframe feature pemissions @@ -443,25 +443,25 @@ export default class AppTile extends React.Component { return
    { this.props.showMenubar && -
    - - { this.props.showTitle && this._getTileTitle() } - - - { this.props.showPopout && } - - -
    } +
    + + { this.props.showTitle && this._getTileTitle() } + + + { this.props.showPopout && } + + +
    } { appTileBody }
    diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index 82d0aa4976..034fc3d49c 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -17,72 +17,91 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; import BaseDialog from "..//dialogs/BaseDialog"; +import DialogButtons from "./DialogButtons"; +import classNames from 'classnames'; import AccessibleButton from './AccessibleButton'; -import { getDesktopCapturerSources } from "matrix-js-sdk/src/webrtc/call"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView'; -export interface DesktopCapturerSource { - id: string; - name: string; - thumbnailURL; +export function getDesktopCapturerSources(): Promise> { + const options: GetSourcesOptions = { + thumbnailSize: { + height: 176, + width: 312, + }, + types: [ + "screen", + "window", + ], + }; + return window.electron.getDesktopCapturerSources(options); } export enum Tabs { - Screens = "screens", - Windows = "windows", + Screens = "screen", + Windows = "window", } -export interface DesktopCapturerSourceIProps { +export interface ExistingSourceIProps { source: DesktopCapturerSource; onSelect(source: DesktopCapturerSource): void; + selected: boolean; } -export class ExistingSource extends React.Component { - constructor(props) { +export class ExistingSource extends React.Component { + constructor(props: ExistingSourceIProps) { super(props); } - onClick = (ev) => { + private onClick = (): void => { this.props.onSelect(this.props.source); }; render() { + const thumbnailClasses = classNames({ + mx_desktopCapturerSourcePicker_source_thumbnail: true, + mx_desktopCapturerSourcePicker_source_thumbnail_selected: this.props.selected, + }); + return ( + onClick={this.onClick} + > - { this.props.source.name } + { this.props.source.name } ); } } -export interface DesktopCapturerSourcePickerIState { +export interface PickerIState { selectedTab: Tabs; sources: Array; + selectedSource: DesktopCapturerSource | null; } -export interface DesktopCapturerSourcePickerIProps { - onFinished(source: DesktopCapturerSource): void; +export interface PickerIProps { + onFinished(sourceId: string): void; } @replaceableComponent("views.elements.DesktopCapturerSourcePicker") export default class DesktopCapturerSourcePicker extends React.Component< - DesktopCapturerSourcePickerIProps, - DesktopCapturerSourcePickerIState - > { - interval; + PickerIProps, + PickerIState +> { + interval: number; - constructor(props) { + constructor(props: PickerIProps) { super(props); this.state = { selectedTab: Tabs.Screens, sources: [], + selectedSource: null, }; } @@ -106,69 +125,61 @@ export default class DesktopCapturerSourcePicker extends React.Component< clearInterval(this.interval); } - onSelect = (source) => { - this.props.onFinished(source); + private onSelect = (source: DesktopCapturerSource): void => { + this.setState({ selectedSource: source }); }; - onScreensClick = (ev) => { - this.setState({ selectedTab: Tabs.Screens }); + private onShare = (): void => { + this.props.onFinished(this.state.selectedSource.id); }; - onWindowsClick = (ev) => { - this.setState({ selectedTab: Tabs.Windows }); + private onTabChange = (): void => { + this.setState({ selectedSource: null }); }; - onCloseClick = (ev) => { + private onCloseClick = (): void => { this.props.onFinished(null); }; - render() { - let sources; - if (this.state.selectedTab === Tabs.Screens) { - sources = this.state.sources - .filter((source) => { - return source.id.startsWith("screen"); - }) - .map((source) => { - return ; - }); - } else { - sources = this.state.sources - .filter((source) => { - return source.id.startsWith("window"); - }) - .map((source) => { - return ; - }); - } + private getTab(type: "screen" | "window", label: string): Tab { + const sources = this.state.sources.filter((source) => source.id.startsWith(type)).map((source) => { + return ( + + ); + }); - const buttonStyle = "mx_desktopCapturerSourcePicker_tabLabel"; - const screensButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Screens) ? "_selected" : ""); - const windowsButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Windows) ? "_selected" : ""); + return new Tab(type, label, null, ( +
    + { sources } +
    + )); + } + + render() { + const tabs = [ + this.getTab("screen", _t("Share entire screen")), + this.getTab("window", _t("Application window")), + ]; return ( -
    - - { _t("Screens") } - - - { _t("Windows") } - -
    -
    - { sources } -
    + +
    ); } diff --git a/src/components/views/elements/DialPadBackspaceButton.tsx b/src/components/views/elements/DialPadBackspaceButton.tsx index 69f0fcb39a..d64ced8239 100644 --- a/src/components/views/elements/DialPadBackspaceButton.tsx +++ b/src/components/views/elements/DialPadBackspaceButton.tsx @@ -15,11 +15,11 @@ limitations under the License. */ import * as React from "react"; -import AccessibleButton from "./AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "./AccessibleButton"; interface IProps { // Callback for when the button is pressed - onBackspacePress: () => void; + onBackspacePress: (ev: ButtonEvent) => void; } export default class DialPadBackspaceButton extends React.PureComponent { diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.tsx similarity index 67% rename from src/components/views/elements/Dropdown.js rename to src/components/views/elements/Dropdown.tsx index f95247e9ae..b4f382c9c3 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.tsx @@ -1,7 +1,6 @@ /* -Copyright 2017 Vector Creations Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2017 - 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. @@ -16,34 +15,38 @@ 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, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react'; import classnames from 'classnames'; -import AccessibleButton from './AccessibleButton'; + +import AccessibleButton, { ButtonEvent } from './AccessibleButton'; import { _t } from '../../../languageHandler'; import { Key } from "../../../Keyboard"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -class MenuOption extends React.Component { - constructor(props) { - super(props); - this._onMouseEnter = this._onMouseEnter.bind(this); - this._onClick = this._onClick.bind(this); - } +interface IMenuOptionProps { + children: ReactElement; + highlighted?: boolean; + dropdownKey: string; + id?: string; + inputRef?: Ref; + onClick(dropdownKey: string): void; + onMouseEnter(dropdownKey: string): void; +} +class MenuOption extends React.Component { static defaultProps = { disabled: false, }; - _onMouseEnter() { + private onMouseEnter = () => { this.props.onMouseEnter(this.props.dropdownKey); - } + }; - _onClick(e) { + private onClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); this.props.onClick(this.props.dropdownKey); - } + }; render() { const optClasses = classnames({ @@ -54,8 +57,8 @@ class MenuOption extends React.Component { return
    { + private readonly buttonRef = createRef(); + private dropdownRootElement: HTMLDivElement = null; + private ignoreEvent: MouseEvent = null; + private childrenByKey: Record = {}; + + constructor(props: IProps) { super(props); - this.dropdownRootElement = null; - this.ignoreEvent = null; + this.reindexChildren(this.props.children); - this._onInputClick = this._onInputClick.bind(this); - this._onRootClick = this._onRootClick.bind(this); - this._onDocumentClick = this._onDocumentClick.bind(this); - this._onMenuOptionClick = this._onMenuOptionClick.bind(this); - this._onInputChange = this._onInputChange.bind(this); - this._collectRoot = this._collectRoot.bind(this); - this._collectInputTextBox = this._collectInputTextBox.bind(this); - this._setHighlightedOption = this._setHighlightedOption.bind(this); - - this.inputTextBox = null; - - this._reindexChildren(this.props.children); - - const firstChild = React.Children.toArray(props.children)[0]; + const firstChild = React.Children.toArray(props.children)[0] as ReactElement; this.state = { // True if the menu is dropped-down expanded: false, // The key of the highlighted option // (the option that would become selected if you pressed enter) - highlightedOption: firstChild ? firstChild.key : null, + highlightedOption: firstChild ? firstChild.key as string : null, // the current search query searchQuery: '', }; - } - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount() { // eslint-disable-line camelcase - this._button = createRef(); // Listen for all clicks on the document so we can close the // menu when the user clicks somewhere else - document.addEventListener('click', this._onDocumentClick, false); + document.addEventListener('click', this.onDocumentClick, false); } componentWillUnmount() { - document.removeEventListener('click', this._onDocumentClick, false); + document.removeEventListener('click', this.onDocumentClick, false); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase + UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line if (!nextProps.children || nextProps.children.length === 0) { return; } - this._reindexChildren(nextProps.children); + this.reindexChildren(nextProps.children); const firstChild = nextProps.children[0]; this.setState({ highlightedOption: firstChild ? firstChild.key : null, }); } - _reindexChildren(children) { + private reindexChildren(children: ReactElement[]): void { this.childrenByKey = {}; React.Children.forEach(children, (child) => { this.childrenByKey[child.key] = child; }); } - _onDocumentClick(ev) { + private onDocumentClick = (ev: MouseEvent) => { // Close the dropdown if the user clicks anywhere that isn't // within our root element if (ev !== this.ignoreEvent) { @@ -157,9 +166,9 @@ export default class Dropdown extends React.Component { expanded: false, }); } - } + }; - _onRootClick(ev) { + private onRootClick = (ev: MouseEvent) => { // This captures any clicks that happen within our elements, // such that we can then ignore them when they're seen by the // click listener on the document handler, ie. not close the @@ -167,9 +176,9 @@ export default class Dropdown extends React.Component { // NB. We can't just stopPropagation() because then the event // doesn't reach the React onClick(). this.ignoreEvent = ev; - } + }; - _onInputClick(ev) { + private onAccessibleButtonClick = (ev: ButtonEvent) => { if (this.props.disabled) return; if (!this.state.expanded) { @@ -177,25 +186,29 @@ export default class Dropdown extends React.Component { expanded: true, }); ev.preventDefault(); + } else if ((ev as React.KeyboardEvent).key === Key.ENTER) { + // the accessible button consumes enter onKeyDown for firing onClick, so handle it here + this.props.onOptionChange(this.state.highlightedOption); + this.close(); } - } + }; - _close() { + private close() { this.setState({ expanded: false, }); // their focus was on the input, its getting unmounted, move it to the button - if (this._button.current) { - this._button.current.focus(); + if (this.buttonRef.current) { + this.buttonRef.current.focus(); } } - _onMenuOptionClick(dropdownKey) { - this._close(); + private onMenuOptionClick = (dropdownKey: string) => { + this.close(); this.props.onOptionChange(dropdownKey); - } + }; - _onInputKeyDown = (e) => { + private onKeyDown = (e: React.KeyboardEvent) => { let handled = true; // These keys don't generate keypress events and so needs to be on keyup @@ -204,16 +217,16 @@ export default class Dropdown extends React.Component { this.props.onOptionChange(this.state.highlightedOption); // fallthrough case Key.ESCAPE: - this._close(); + this.close(); break; case Key.ARROW_DOWN: this.setState({ - highlightedOption: this._nextOption(this.state.highlightedOption), + highlightedOption: this.nextOption(this.state.highlightedOption), }); break; case Key.ARROW_UP: this.setState({ - highlightedOption: this._prevOption(this.state.highlightedOption), + highlightedOption: this.prevOption(this.state.highlightedOption), }); break; default: @@ -224,53 +237,46 @@ export default class Dropdown extends React.Component { e.preventDefault(); e.stopPropagation(); } - } + }; - _onInputChange(e) { + private onInputChange = (e: ChangeEvent) => { this.setState({ - searchQuery: e.target.value, + searchQuery: e.currentTarget.value, }); if (this.props.onSearchChange) { - this.props.onSearchChange(e.target.value); + this.props.onSearchChange(e.currentTarget.value); } - } + }; - _collectRoot(e) { + private collectRoot = (e: HTMLDivElement) => { if (this.dropdownRootElement) { - this.dropdownRootElement.removeEventListener( - 'click', this._onRootClick, false, - ); + this.dropdownRootElement.removeEventListener('click', this.onRootClick, false); } if (e) { - e.addEventListener('click', this._onRootClick, false); + e.addEventListener('click', this.onRootClick, false); } this.dropdownRootElement = e; - } + }; - _collectInputTextBox(e) { - this.inputTextBox = e; - if (e) e.focus(); - } - - _setHighlightedOption(optionKey) { + private setHighlightedOption = (optionKey: string) => { this.setState({ highlightedOption: optionKey, }); - } + }; - _nextOption(optionKey) { + private nextOption(optionKey: string): string { const keys = Object.keys(this.childrenByKey); const index = keys.indexOf(optionKey); return keys[(index + 1) % keys.length]; } - _prevOption(optionKey) { + private prevOption(optionKey: string): string { const keys = Object.keys(this.childrenByKey); const index = keys.indexOf(optionKey); - return keys[(index - 1) % keys.length]; + return keys[index <= 0 ? keys.length - 1 : (index - 1) % keys.length]; } - _scrollIntoView(node) { + private scrollIntoView(node: Element) { if (node) { node.scrollIntoView({ block: "nearest", @@ -279,18 +285,18 @@ export default class Dropdown extends React.Component { } } - _getMenuOptions() { + private getMenuOptions() { const options = React.Children.map(this.props.children, (child) => { const highlighted = this.state.highlightedOption === child.key; return ( { child } @@ -307,7 +313,7 @@ export default class Dropdown extends React.Component { render() { let currentValue; - const menuStyle = {}; + const menuStyle: CSSProperties = {}; if (this.props.menuWidth) menuStyle.width = this.props.menuWidth; let menu; @@ -316,10 +322,9 @@ export default class Dropdown extends React.Component { currentValue = ( ); } menu = (
    - { this._getMenuOptions() } + { this.getMenuOptions() }
    ); } @@ -356,16 +362,17 @@ export default class Dropdown extends React.Component { // Note the menu sits inside the AccessibleButton div so it's anchored // to the input, but overflows below it. The root contains both. - return
    + return
    { currentValue } @@ -374,28 +381,3 @@ export default class Dropdown extends React.Component {
    ; } } - -Dropdown.propTypes = { - id: PropTypes.string.isRequired, - // The width that the dropdown should be. If specified, - // the dropped-down part of the menu will be set to this - // width. - menuWidth: PropTypes.number, - // Called when the selected option changes - onOptionChange: PropTypes.func.isRequired, - // Called when the value of the search field changes - onSearchChange: PropTypes.func, - searchEnabled: PropTypes.bool, - // Function that, given the key of an option, returns - // a node representing that option to be displayed in the - // box itself as the currently-selected option (ie. as - // opposed to in the actual dropped-down part). If - // unspecified, the appropriate child element is used as - // in the dropped-down menu. - getShortOption: PropTypes.func, - value: PropTypes.string, - // negative for consistency with HTML - disabled: PropTypes.bool, - // ARIA label - label: PropTypes.string.isRequired, -}; diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx index 334e569163..50ea7d9a56 100644 --- a/src/components/views/elements/ErrorBoundary.tsx +++ b/src/components/views/elements/ErrorBoundary.tsx @@ -71,12 +71,13 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> { private onBugReport = (): void => { Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, { label: 'react-soft-crash', + error: this.state.error, }); }; render() { if (this.state.error) { - const newIssueUrl = "https://github.com/vector-im/element-web/issues/new"; + const newIssueUrl = "https://github.com/vector-im/element-web/issues/new/choose"; let bugReportSection; if (SdkConfig.get().bug_report_endpoint_url) { @@ -93,8 +94,9 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> { "If you've submitted a bug via GitHub, debug logs can help " + "us track down the problem. Debug logs contain application " + "usage data including your username, the IDs or aliases of " + - "the rooms or groups you have visited and the usernames of " + - "other users. They do not contain messages.", + "the rooms or groups you have visited, which UI elements you " + + "last interacted with, and the usernames of other users. " + + "They do not contain messages.", ) }

    { _t("Submit debug logs") } diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index b1cc9c773d..cbb0e17b42 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -22,6 +22,7 @@ import MemberAvatar from '../avatars/MemberAvatar'; import { _t } from '../../../languageHandler'; import { useStateToggle } from "../../../hooks/useStateToggle"; import AccessibleButton from "./AccessibleButton"; +import { Layout } from '../../../settings/Layout'; interface IProps { // An array of member events to summarise @@ -33,11 +34,13 @@ interface IProps { // The list of room members for which to show avatars next to the summary summaryMembers?: RoomMember[]; // The text to show as the summary of this event list - summaryText?: string; + summaryText?: string | JSX.Element; // An array of EventTiles to render when expanded children: ReactNode[]; // Called when the event list expansion is toggled onToggle?(): void; + // The layout currently used + layout?: Layout; } const EventListSummary: React.FC = ({ @@ -48,6 +51,7 @@ const EventListSummary: React.FC = ({ startExpanded, summaryMembers = [], summaryText, + layout, }) => { const [expanded, toggleExpanded] = useStateToggle(startExpanded); @@ -63,7 +67,7 @@ const EventListSummary: React.FC = ({ // If we are only given few events then just pass them through if (events.length < threshold) { return ( -
  • +
  • { children }
  • ); @@ -92,7 +96,7 @@ const EventListSummary: React.FC = ({ } return ( -
  • +
  • { expanded ? _t('collapse') : _t('expand') } @@ -103,6 +107,7 @@ const EventListSummary: React.FC = ({ EventListSummary.defaultProps = { startExpanded: false, + layout: Layout.Group, }; export default EventListSummary; diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 68a70133e6..a7ebf40c3a 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -25,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import { Layout } from "../../../settings/Layout"; import { UIFeature } from "../../../settings/UIFeature"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Spinner from './Spinner'; interface IProps { /** @@ -45,7 +46,7 @@ interface IProps { /** * The ID of the displayed user */ - userId: string; + userId?: string; /** * The display name of the displayed user @@ -118,13 +119,16 @@ export default class EventTilePreview extends React.Component { } public render() { - const event = this.fakeEvent(this.state); - const className = classnames(this.props.className, { "mx_IRCLayout": this.props.layout == Layout.IRC, "mx_GroupLayout": this.props.layout == Layout.Group, + "mx_EventTilePreview_loader": !this.props.userId, }); + if (!this.props.userId) return
    ; + + const event = this.fakeEvent(this.state); + return
    { const avatar = ( ); @@ -486,7 +488,7 @@ export default class ImageView extends React.Component { // an empty div here, since the panel uses space-between // and we want the same placement of elements info = ( -
    +
    ); } @@ -510,15 +512,15 @@ export default class ImageView extends React.Component { - + onClick={this.onZoomOutClick} + /> ); zoomInButton = ( - + onClick={this.onZoomInClick} + /> ); } @@ -540,24 +542,24 @@ export default class ImageView extends React.Component { - + onClick={this.onRotateCounterClockwiseClick} + /> - + onClick={this.onRotateClockwiseClick} + /> - + onClick={this.onDownloadClick} + /> { contextMenuButton } - + onClick={this.props.onFinished} + /> { this.renderContextMenu() }
    diff --git a/src/components/views/elements/InviteReason.tsx b/src/components/views/elements/InviteReason.tsx index dff5c7d6bd..865a5be747 100644 --- a/src/components/views/elements/InviteReason.tsx +++ b/src/components/views/elements/InviteReason.tsx @@ -16,11 +16,13 @@ limitations under the License. import classNames from "classnames"; import React from "react"; +import { sanitizedHtmlNode } from "../../../HtmlUtils"; import { _t } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { reason: string; + htmlReason?: string; } interface IState { @@ -51,7 +53,7 @@ export default class InviteReason extends React.PureComponent { }); return
    -
    { this.props.reason }
    +
    { this.props.htmlReason ? sanitizedHtmlNode(this.props.htmlReason) : this.props.reason }
    diff --git a/src/components/views/elements/JoinRuleDropdown.tsx b/src/components/views/elements/JoinRuleDropdown.tsx new file mode 100644 index 0000000000..e2d9b6d872 --- /dev/null +++ b/src/components/views/elements/JoinRuleDropdown.tsx @@ -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. +*/ + +import React from 'react'; +import { JoinRule } from 'matrix-js-sdk/src/@types/partials'; + +import Dropdown from "./Dropdown"; + +interface IProps { + value: JoinRule; + label: string; + width?: number; + labelInvite: string; + labelPublic: string; + labelRestricted?: string; // if omitted then this option will be hidden, e.g if unsupported + onChange(value: JoinRule): void; +} + +const JoinRuleDropdown = ({ + label, + labelInvite, + labelPublic, + labelRestricted, + value, + width = 448, + onChange, +}: IProps) => { + const options = [ +
    + { labelInvite } +
    , +
    + { labelPublic } +
    , + ]; + + if (labelRestricted) { + options.unshift(
    + { labelRestricted } +
    ); + } + + return + { options } + ; +}; + +export default JoinRuleDropdown; diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index d52462f629..0722cb872a 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -25,12 +25,31 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { isValid3pidInvite } from "../../../RoomInvite"; import EventListSummary from "./EventListSummary"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import defaultDispatcher from '../../../dispatcher/dispatcher'; +import { RightPanelPhases } from '../../../stores/RightPanelStorePhases'; +import { Action } from '../../../dispatcher/actions'; +import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; +import { jsxJoin } from '../../../utils/ReactUtils'; +import { EventType } from 'matrix-js-sdk/src/@types/event'; +import { Layout } from '../../../settings/Layout'; + +const onPinnedMessagesClick = (): void => { + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.PinnedMessages, + allowClose: false, + }); +}; + +const SENDER_AS_DISPLAY_NAME_EVENTS = [EventType.RoomServerAcl, EventType.RoomPinnedEvents]; interface IProps extends Omit, "summaryText" | "summaryMembers"> { // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left" summaryLength?: number; // The maximum number of avatars to display in the summary avatarsMaxLength?: number; + // The currently selected layout + layout: Layout; } interface IUserEvents { @@ -57,6 +76,7 @@ enum TransitionType { ChangedAvatar = "changed_avatar", NoChange = "no_change", ServerAcl = "server_acl", + ChangedPins = "pinned_messages" } const SEP = ","; @@ -67,6 +87,7 @@ export default class MemberEventListSummary extends React.Component { summaryLength: 1, threshold: 3, avatarsMaxLength: 5, + layout: Layout.Group, }; shouldComponentUpdate(nextProps) { @@ -89,7 +110,10 @@ export default class MemberEventListSummary extends React.Component { * `Object.keys(eventAggregates)`. * @returns {string} the textual summary of the aggregated events that occurred. */ - private generateSummary(eventAggregates: Record, orderedTransitionSequences: string[]) { + private generateSummary( + eventAggregates: Record, + orderedTransitionSequences: string[], + ): string | JSX.Element { const summaries = orderedTransitionSequences.map((transitions) => { const userNames = eventAggregates[transitions]; const nameList = this.renderNameList(userNames); @@ -118,7 +142,7 @@ export default class MemberEventListSummary extends React.Component { return null; } - return summaries.join(", "); + return jsxJoin(summaries, ", "); } /** @@ -212,7 +236,11 @@ export default class MemberEventListSummary extends React.Component { * @param {number} repeats the number of times the transition was repeated in a row. * @returns {string} the written Human Readable equivalent of the transition. */ - private static getDescriptionForTransition(t: TransitionType, userCount: number, repeats: number) { + private static getDescriptionForTransition( + t: TransitionType, + userCount: number, + repeats: number, + ): string | JSX.Element { // The empty interpolations 'severalUsers' and 'oneUser' // are there only to show translators to non-English languages // that the verb is conjugated to plural or singular Subject. @@ -295,6 +323,15 @@ export default class MemberEventListSummary extends React.Component { { severalUsers: "", count: repeats }) : _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats }); break; + case "pinned_messages": + res = (userCount > 1) + ? _t("%(severalUsers)schanged the pinned messages for the room %(count)s times.", + { severalUsers: "", count: repeats }, + { "a": (sub) => { sub } }) + : _t("%(oneUser)schanged the pinned messages for the room %(count)s times.", + { oneUser: "", count: repeats }, + { "a": (sub) => { sub } }); + break; } return res; @@ -313,16 +350,18 @@ export default class MemberEventListSummary extends React.Component { * if a transition is not recognised. */ private static getTransition(e: IUserEvents): TransitionType { - if (e.mxEvent.getType() === 'm.room.third_party_invite') { + const type = e.mxEvent.getType(); + + if (type === EventType.RoomThirdPartyInvite) { // Handle 3pid invites the same as invites so they get bundled together if (!isValid3pidInvite(e.mxEvent)) { return TransitionType.InviteWithdrawal; } return TransitionType.Invited; - } - - if (e.mxEvent.getType() === 'm.room.server_acl') { + } else if (type === EventType.RoomServerAcl) { return TransitionType.ServerAcl; + } else if (type === EventType.RoomPinnedEvents) { + return TransitionType.ChangedPins; } switch (e.mxEvent.getContent().membership) { @@ -411,22 +450,23 @@ export default class MemberEventListSummary extends React.Component { // Object mapping user IDs to an array of IUserEvents const userEvents: Record = {}; eventsToRender.forEach((e, index) => { - const userId = e.getType() === 'm.room.server_acl' ? e.getSender() : e.getStateKey(); + const type = e.getType(); + const userId = type === EventType.RoomServerAcl ? e.getSender() : e.getStateKey(); // Initialise a user's events if (!userEvents[userId]) { userEvents[userId] = []; } - if (e.getType() === 'm.room.server_acl') { + if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) { latestUserAvatarMember.set(userId, e.sender); } else if (e.target) { latestUserAvatarMember.set(userId, e.target); } let displayName = userId; - if (e.getType() === 'm.room.third_party_invite') { + if (type === EventType.RoomThirdPartyInvite) { displayName = e.getContent().display_name; - } else if (e.getType() === 'm.room.server_acl') { + } else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) { displayName = e.sender.name; } else if (e.target) { displayName = e.target.name; @@ -453,6 +493,7 @@ export default class MemberEventListSummary extends React.Component { startExpanded={this.props.startExpanded} children={this.props.children} summaryMembers={[...latestUserAvatarMember.values()]} + layout={this.props.layout} summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />; } } diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index 47bcd845ba..22ff4bf4b3 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -92,7 +92,7 @@ const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAva
    { busy ? : -
    } +
    }
    { + onUserPillClicked = (e) => { + e.preventDefault(); dis.dispatch({ action: Action.ViewUser, member: this.state.member, @@ -258,7 +259,10 @@ class Pill extends React.Component { linkText = groupId; if (this.props.shouldShowPillAvatar) { avatar =