Merge remote-tracking branch 'upstream/develop' into feature/image-view-load-anim/18186
This commit is contained in:
commit
7022ab4f8a
144 changed files with 4658 additions and 2660 deletions
2
.github/workflows/layered-build.yaml
vendored
2
.github/workflows/layered-build.yaml
vendored
|
@ -5,6 +5,8 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
PR_NUMBER: ${{github.event.number}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|
2
.github/workflows/typecheck.yaml
vendored
2
.github/workflows/typecheck.yaml
vendored
|
@ -5,6 +5,8 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
PR_NUMBER: ${{github.event.number}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: c-hive/gha-yarn-cache@v2
|
- uses: c-hive/gha-yarn-cache@v2
|
||||||
|
|
67
CHANGELOG.md
67
CHANGELOG.md
|
@ -1,3 +1,70 @@
|
||||||
|
Changes in [3.30.0](https://github.com/vector-im/element-desktop/releases/tag/v3.30.0) (2021-09-14)
|
||||||
|
===================================================================================================
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
* Add bubble highlight styling ([\#6582](https://github.com/matrix-org/matrix-react-sdk/pull/6582)). Fixes vector-im/element-web#18295 and vector-im/element-web#18295. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* [Release] Add config option to turn on in-room event sending timing metrics ([\#6773](https://github.com/matrix-org/matrix-react-sdk/pull/6773)).
|
||||||
|
* Create narrow mode for Composer ([\#6682](https://github.com/matrix-org/matrix-react-sdk/pull/6682)). Fixes vector-im/element-web#18533 and vector-im/element-web#18533.
|
||||||
|
* Prefer matrix.to alias links over room id in spaces & share ([\#6745](https://github.com/matrix-org/matrix-react-sdk/pull/6745)). Fixes vector-im/element-web#18796 and vector-im/element-web#18796.
|
||||||
|
* Stop automatic playback of voice messages if a non-voice message is encountered ([\#6728](https://github.com/matrix-org/matrix-react-sdk/pull/6728)). Fixes vector-im/element-web#18850 and vector-im/element-web#18850.
|
||||||
|
* Show call length during a call ([\#6700](https://github.com/matrix-org/matrix-react-sdk/pull/6700)). Fixes vector-im/element-web#18566 and vector-im/element-web#18566. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* Serialize and retry mass-leave when leaving space ([\#6737](https://github.com/matrix-org/matrix-react-sdk/pull/6737)). Fixes vector-im/element-web#18789 and vector-im/element-web#18789.
|
||||||
|
* Improve form handling in and around space creation ([\#6739](https://github.com/matrix-org/matrix-react-sdk/pull/6739)). Fixes vector-im/element-web#18775 and vector-im/element-web#18775.
|
||||||
|
* Split autoplay GIFs and videos into different settings ([\#6726](https://github.com/matrix-org/matrix-react-sdk/pull/6726)). Fixes vector-im/element-web#5771 and vector-im/element-web#5771. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* Add autoplay for voice messages ([\#6710](https://github.com/matrix-org/matrix-react-sdk/pull/6710)). Fixes vector-im/element-web#18804, vector-im/element-web#18715, vector-im/element-web#18714 vector-im/element-web#17961 and vector-im/element-web#18804.
|
||||||
|
* Allow to use basic html to format invite messages ([\#6703](https://github.com/matrix-org/matrix-react-sdk/pull/6703)). Fixes vector-im/element-web#15738 and vector-im/element-web#15738. Contributed by [skolmer](https://github.com/skolmer).
|
||||||
|
* Allow widgets, when eligible, to interact with more rooms as per MSC2762 ([\#6684](https://github.com/matrix-org/matrix-react-sdk/pull/6684)).
|
||||||
|
* Remove arbitrary limits from send/receive events for widgets ([\#6719](https://github.com/matrix-org/matrix-react-sdk/pull/6719)). Fixes vector-im/element-web#17994 and vector-im/element-web#17994.
|
||||||
|
* Reload suggested rooms if we see the state change down /sync ([\#6715](https://github.com/matrix-org/matrix-react-sdk/pull/6715)). Fixes vector-im/element-web#18761 and vector-im/element-web#18761.
|
||||||
|
* When creating private spaces, make the initial rooms restricted if supported ([\#6721](https://github.com/matrix-org/matrix-react-sdk/pull/6721)). Fixes vector-im/element-web#18722 and vector-im/element-web#18722.
|
||||||
|
* Threading exploration work ([\#6658](https://github.com/matrix-org/matrix-react-sdk/pull/6658)). Fixes vector-im/element-web#18532 and vector-im/element-web#18532.
|
||||||
|
* Default to `Don't leave any` when leaving a space ([\#6697](https://github.com/matrix-org/matrix-react-sdk/pull/6697)). Fixes vector-im/element-web#18592 and vector-im/element-web#18592. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* Special case redaction event sending from widgets per MSC2762 ([\#6686](https://github.com/matrix-org/matrix-react-sdk/pull/6686)). Fixes vector-im/element-web#18573 and vector-im/element-web#18573.
|
||||||
|
* Add active speaker indicators ([\#6639](https://github.com/matrix-org/matrix-react-sdk/pull/6639)). Fixes vector-im/element-web#17627 and vector-im/element-web#17627. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* Increase general app performance by optimizing layers ([\#6644](https://github.com/matrix-org/matrix-react-sdk/pull/6644)). Fixes vector-im/element-web#18730 and vector-im/element-web#18730. Contributed by [Palid](https://github.com/Palid).
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Fix autocomplete not having y-scroll ([\#6802](https://github.com/matrix-org/matrix-react-sdk/pull/6802)).
|
||||||
|
* Fix emoji picker and stickerpicker not appearing correctly when opened ([\#6801](https://github.com/matrix-org/matrix-react-sdk/pull/6801)).
|
||||||
|
* Debounce read marker update on scroll ([\#6774](https://github.com/matrix-org/matrix-react-sdk/pull/6774)).
|
||||||
|
* Fix Space creation wizard go to my first room button behaviour ([\#6748](https://github.com/matrix-org/matrix-react-sdk/pull/6748)). Fixes vector-im/element-web#18764 and vector-im/element-web#18764.
|
||||||
|
* Fix scroll being stuck at bottom ([\#6751](https://github.com/matrix-org/matrix-react-sdk/pull/6751)). Fixes vector-im/element-web#18903 and vector-im/element-web#18903.
|
||||||
|
* Fix widgets not remembering identity verification when asked to. ([\#6742](https://github.com/matrix-org/matrix-react-sdk/pull/6742)). Fixes vector-im/element-web#15631 and vector-im/element-web#15631.
|
||||||
|
* Add missing pluralisation i18n strings for Spaces ([\#6738](https://github.com/matrix-org/matrix-react-sdk/pull/6738)). Fixes vector-im/element-web#18780 and vector-im/element-web#18780.
|
||||||
|
* Make ForgotPassword UX slightly more user friendly ([\#6636](https://github.com/matrix-org/matrix-react-sdk/pull/6636)). Fixes vector-im/element-web#11531 and vector-im/element-web#11531. Contributed by [Palid](https://github.com/Palid).
|
||||||
|
* Don't context switch room on SpaceStore ready as it can break permalinks ([\#6730](https://github.com/matrix-org/matrix-react-sdk/pull/6730)). Fixes vector-im/element-web#17974 and vector-im/element-web#17974.
|
||||||
|
* Fix explore rooms button not working during space creation wizard ([\#6729](https://github.com/matrix-org/matrix-react-sdk/pull/6729)). Fixes vector-im/element-web#18762 and vector-im/element-web#18762.
|
||||||
|
* Fix bug where one party's media would sometimes not be shown ([\#6731](https://github.com/matrix-org/matrix-react-sdk/pull/6731)).
|
||||||
|
* Only make the initial space rooms suggested by default ([\#6714](https://github.com/matrix-org/matrix-react-sdk/pull/6714)). Fixes vector-im/element-web#18760 and vector-im/element-web#18760.
|
||||||
|
* Replace fake username in EventTilePreview with a proper loading state ([\#6702](https://github.com/matrix-org/matrix-react-sdk/pull/6702)). Fixes vector-im/element-web#15897 and vector-im/element-web#15897. Contributed by [skolmer](https://github.com/skolmer).
|
||||||
|
* Don't send prehistorical events to widgets during decryption at startup ([\#6695](https://github.com/matrix-org/matrix-react-sdk/pull/6695)). Fixes vector-im/element-web#18060 and vector-im/element-web#18060.
|
||||||
|
* When creating subspaces properly set restricted join rule ([\#6725](https://github.com/matrix-org/matrix-react-sdk/pull/6725)). Fixes vector-im/element-web#18797 and vector-im/element-web#18797.
|
||||||
|
* Fix the Image View not openning for some pinned messages ([\#6723](https://github.com/matrix-org/matrix-react-sdk/pull/6723)). Fixes vector-im/element-web#18422 and vector-im/element-web#18422. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* Show autocomplete sections vertically ([\#6722](https://github.com/matrix-org/matrix-react-sdk/pull/6722)). Fixes vector-im/element-web#18860 and vector-im/element-web#18860. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* Fix EmojiPicker filtering to lower case emojibase data strings ([\#6717](https://github.com/matrix-org/matrix-react-sdk/pull/6717)). Fixes vector-im/element-web#18686 and vector-im/element-web#18686.
|
||||||
|
* Clear currentRoomId when viewing home page, fixing document title ([\#6716](https://github.com/matrix-org/matrix-react-sdk/pull/6716)). Fixes vector-im/element-web#18668 and vector-im/element-web#18668.
|
||||||
|
* Fix membership updates to Spaces not applying in real-time ([\#6713](https://github.com/matrix-org/matrix-react-sdk/pull/6713)). Fixes vector-im/element-web#18737 and vector-im/element-web#18737.
|
||||||
|
* Don't show a double stacked invite modals when inviting to Spaces ([\#6698](https://github.com/matrix-org/matrix-react-sdk/pull/6698)). Fixes vector-im/element-web#18745 and vector-im/element-web#18745. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* Remove non-functional DuckDuckGo Autocomplete Provider ([\#6712](https://github.com/matrix-org/matrix-react-sdk/pull/6712)). Fixes vector-im/element-web#18778 and vector-im/element-web#18778.
|
||||||
|
* Filter members on `MemberList` load ([\#6708](https://github.com/matrix-org/matrix-react-sdk/pull/6708)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* Fix improper voice messages being produced in Firefox and sometimes other browsers. ([\#6696](https://github.com/matrix-org/matrix-react-sdk/pull/6696)). Fixes vector-im/element-web#18587 and vector-im/element-web#18587.
|
||||||
|
* Fix client forgetting which capabilities a widget was approved for ([\#6685](https://github.com/matrix-org/matrix-react-sdk/pull/6685)). Fixes vector-im/element-web#18786 and vector-im/element-web#18786.
|
||||||
|
* Fix left panel widgets not remembering collapsed state ([\#6687](https://github.com/matrix-org/matrix-react-sdk/pull/6687)). Fixes vector-im/element-web#17803 and vector-im/element-web#17803.
|
||||||
|
* Fix changelog link colour back to blue ([\#6692](https://github.com/matrix-org/matrix-react-sdk/pull/6692)). Fixes vector-im/element-web#18726 and vector-im/element-web#18726.
|
||||||
|
* Soften codeblock border color ([\#6564](https://github.com/matrix-org/matrix-react-sdk/pull/6564)). Fixes vector-im/element-web#18367 and vector-im/element-web#18367. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* Pause ringing more aggressively ([\#6691](https://github.com/matrix-org/matrix-react-sdk/pull/6691)). Fixes vector-im/element-web#18588 and vector-im/element-web#18588. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* Fix command autocomplete ([\#6680](https://github.com/matrix-org/matrix-react-sdk/pull/6680)). Fixes vector-im/element-web#18670 and vector-im/element-web#18670. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* Don't re-sort the room-list based on profile/status changes ([\#6595](https://github.com/matrix-org/matrix-react-sdk/pull/6595)). Fixes vector-im/element-web#110 and vector-im/element-web#110. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* Fix codeblock formatting with syntax highlighting on ([\#6681](https://github.com/matrix-org/matrix-react-sdk/pull/6681)). Fixes vector-im/element-web#18739 vector-im/element-web#18365 and vector-im/element-web#18739. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
* Add padding to the Add button in the notification settings ([\#6665](https://github.com/matrix-org/matrix-react-sdk/pull/6665)). Fixes vector-im/element-web#18706 and vector-im/element-web#18706. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||||
|
|
||||||
|
Changes in [3.29.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.29.1) (2021-09-13)
|
||||||
|
===================================================================================================
|
||||||
|
|
||||||
|
## 🔒 SECURITY FIXES
|
||||||
|
* Fix a security issue with message key sharing. See https://matrix.org/blog/2021/09/13/vulnerability-disclosure-key-sharing
|
||||||
|
for details.
|
||||||
|
|
||||||
Changes in [3.29.0](https://github.com/vector-im/element-desktop/releases/tag/v3.29.0) (2021-08-31)
|
Changes in [3.29.0](https://github.com/vector-im/element-desktop/releases/tag/v3.29.0) (2021-08-31)
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
|
|
||||||
|
|
15
package.json
15
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.29.0",
|
"version": "3.30.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -93,10 +93,10 @@
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "^17.0.2",
|
"react": "17.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-blurhash": "^0.1.3",
|
"react-blurhash": "^0.1.3",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-focus-lock": "^2.5.0",
|
"react-focus-lock": "^2.5.0",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
|
@ -142,9 +142,9 @@
|
||||||
"@types/pako": "^1.0.1",
|
"@types/pako": "^1.0.1",
|
||||||
"@types/parse5": "^6.0.0",
|
"@types/parse5": "^6.0.0",
|
||||||
"@types/qrcode": "^1.3.5",
|
"@types/qrcode": "^1.3.5",
|
||||||
"@types/react": "^17.0.2",
|
"@types/react": "17.0.14",
|
||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
"@types/react-dom": "^17.0.2",
|
"@types/react-dom": "17.0.9",
|
||||||
"@types/react-transition-group": "^4.4.0",
|
"@types/react-transition-group": "^4.4.0",
|
||||||
"@types/sanitize-html": "^2.3.1",
|
"@types/sanitize-html": "^2.3.1",
|
||||||
"@types/zxcvbn": "^4.4.0",
|
"@types/zxcvbn": "^4.4.0",
|
||||||
|
@ -175,9 +175,12 @@
|
||||||
"stylelint": "^13.9.0",
|
"stylelint": "^13.9.0",
|
||||||
"stylelint-config-standard": "^20.0.0",
|
"stylelint-config-standard": "^20.0.0",
|
||||||
"stylelint-scss": "^3.18.0",
|
"stylelint-scss": "^3.18.0",
|
||||||
"typescript": "^4.1.3",
|
"typescript": "4.3.5",
|
||||||
"walk": "^2.3.14"
|
"walk": "^2.3.14"
|
||||||
},
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@types/react": "17.0.14"
|
||||||
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "./__test-utils__/environment.js",
|
"testEnvironment": "./__test-utils__/environment.js",
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
|
|
|
@ -73,12 +73,6 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
|
||||||
.mx_GroupFilterPanel .mx_TagTile {
|
.mx_GroupFilterPanel .mx_TagTile {
|
||||||
// opacity: 0.5;
|
// opacity: 0.5;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.mx_BetaDot {
|
|
||||||
position: absolute;
|
|
||||||
right: -13px;
|
|
||||||
top: -11px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype {
|
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype {
|
||||||
|
|
|
@ -183,3 +183,40 @@ limitations under the License.
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
.mx_RoomDirectory_roomMemberCount {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_secondary {
|
||||||
|
padding: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomDirectory_join {
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomDirectory_alias {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomDirectory_roomDescription {
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomDirectory_name {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomDirectory_roomAvatar {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomDirectory_table {
|
||||||
|
grid-template-columns: auto;
|
||||||
|
row-gap: 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -103,6 +103,16 @@ $activeBorderColor: $secondary-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SpaceItem_new {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.mx_BetaDot {
|
||||||
|
position: absolute;
|
||||||
|
left: 33px;
|
||||||
|
top: -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton {
|
.mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton {
|
||||||
margin-left: $gutterSize;
|
margin-left: $gutterSize;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
|
@ -139,7 +149,6 @@ $activeBorderColor: $secondary-content;
|
||||||
&:not(.mx_SpaceButton_narrow) {
|
&:not(.mx_SpaceButton_narrow) {
|
||||||
.mx_SpaceButton_selectionWrapper {
|
.mx_SpaceButton_selectionWrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-right: 16px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,7 +160,6 @@ $activeBorderColor: $secondary-content;
|
||||||
display: block;
|
display: block;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-right: 8px;
|
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
line-height: $font-18px;
|
line-height: $font-18px;
|
||||||
}
|
}
|
||||||
|
@ -196,23 +204,18 @@ $activeBorderColor: $secondary-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_SpaceButton_new .mx_SpaceButton_icon {
|
&.mx_SpaceButton_new .mx_SpaceButton_icon {
|
||||||
background-color: $accent-color;
|
background-color: $roomlist-button-bg-color;
|
||||||
transition: all .1s ease-in-out; // TODO transition
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
background-color: #ffffff;
|
background-color: $primary-content;
|
||||||
mask-image: url('$(res)/img/element-icons/plus.svg');
|
mask-image: url('$(res)/img/element-icons/plus.svg');
|
||||||
transition: all .2s ease-in-out; // TODO transition
|
transition: all .2s ease-in-out; // TODO transition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_SpaceButton_newCancel .mx_SpaceButton_icon {
|
&.mx_SpaceButton_newCancel .mx_SpaceButton_icon::before {
|
||||||
background-color: $icon-button-color;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_BaseAvatar_image {
|
.mx_BaseAvatar_image {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
@ -225,8 +228,7 @@ $activeBorderColor: $secondary-content;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: relative;
|
||||||
right: 4px;
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
top: 2px;
|
top: 2px;
|
||||||
|
@ -245,8 +247,6 @@ $activeBorderColor: $secondary-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpacePanel_badgeContainer {
|
.mx_SpacePanel_badgeContainer {
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
// Create a flexbox to make aligning dot badges easier
|
// Create a flexbox to make aligning dot badges easier
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -264,6 +264,7 @@ $activeBorderColor: $secondary-content;
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
.mx_SpaceButton {
|
.mx_SpaceButton {
|
||||||
.mx_SpacePanel_badgeContainer {
|
.mx_SpacePanel_badgeContainer {
|
||||||
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
||||||
|
@ -293,19 +294,12 @@ $activeBorderColor: $secondary-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.collapsed) {
|
&:not(.collapsed) {
|
||||||
.mx_SpacePanel_badgeContainer {
|
|
||||||
position: absolute;
|
|
||||||
right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SpaceButton:hover,
|
.mx_SpaceButton:hover,
|
||||||
.mx_SpaceButton:focus-within,
|
.mx_SpaceButton:focus-within,
|
||||||
.mx_SpaceButton_hasMenuOpen {
|
.mx_SpaceButton_hasMenuOpen {
|
||||||
&:not(.mx_SpaceButton_invite) {
|
&:not(.mx_SpaceButton_invite) {
|
||||||
// Hide the badge container on hover because it'll be a menu button
|
// Hide the badge container on hover because it'll be a menu button
|
||||||
.mx_SpacePanel_badgeContainer {
|
.mx_SpacePanel_badgeContainer {
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -215,10 +215,11 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .mx_BaseAvatar_image,
|
> .mx_RoomAvatar_isSpaceRoom {
|
||||||
> .mx_BaseAvatar > .mx_BaseAvatar_image {
|
&.mx_BaseAvatar_image, .mx_BaseAvatar_image {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h1.mx_SpaceRoomView_preview_name {
|
h1.mx_SpaceRoomView_preview_name {
|
||||||
margin: 20px 0 !important; // override default margin from above
|
margin: 20px 0 !important; // override default margin from above
|
||||||
|
|
|
@ -113,6 +113,7 @@ $dot-size: 12px;
|
||||||
animation: mx_Beta_bluePulse 2s infinite;
|
animation: mx_Beta_bluePulse 2s infinite;
|
||||||
animation-iteration-count: 20;
|
animation-iteration-count: 20;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
|
@ -98,14 +98,14 @@ limitations under the License.
|
||||||
transition:
|
transition:
|
||||||
font-size 0.25s ease-out 0.1s,
|
font-size 0.25s ease-out 0.1s,
|
||||||
color 0.25s ease-out 0.1s,
|
color 0.25s ease-out 0.1s,
|
||||||
top 0.25s ease-out 0.1s,
|
transform 0.25s ease-out 0.1s,
|
||||||
background-color 0.25s ease-out 0.1s;
|
background-color 0.25s ease-out 0.1s;
|
||||||
color: $primary-content;
|
color: $primary-content;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
|
transform: translateY(0);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
top: 0px;
|
|
||||||
margin: 7px 8px;
|
margin: 7px 8px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
pointer-events: none; // Allow clicks to fall through to the input
|
pointer-events: none; // Allow clicks to fall through to the input
|
||||||
|
@ -124,10 +124,10 @@ limitations under the License.
|
||||||
transition:
|
transition:
|
||||||
font-size 0.25s ease-out 0s,
|
font-size 0.25s ease-out 0s,
|
||||||
color 0.25s ease-out 0s,
|
color 0.25s ease-out 0s,
|
||||||
top 0.25s ease-out 0s,
|
transform 0.25s ease-out 0s,
|
||||||
background-color 0.25s ease-out 0s;
|
background-color 0.25s ease-out 0s;
|
||||||
font-size: $font-10px;
|
font-size: $font-10px;
|
||||||
top: -13px;
|
transform: translateY(-13px);
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
background-color: $field-focused-label-bg-color;
|
background-color: $field-focused-label-bg-color;
|
||||||
pointer-events: initial;
|
pointer-events: initial;
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
background: $background;
|
background: $background;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-radius: 8px 8px 0 0;
|
border-radius: 8px 8px 0 0;
|
||||||
max-height: 35vh;
|
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -64,6 +63,7 @@
|
||||||
margin: 12px;
|
margin: 12px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
max-height: 35vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Autocomplete_Completion_container_truncate {
|
.mx_Autocomplete_Completion_container_truncate {
|
||||||
|
|
|
@ -23,11 +23,11 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile[data-layout=bubble] {
|
.mx_EventTile[data-layout=bubble] {
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: var(--gutterSize);
|
margin-top: var(--gutterSize);
|
||||||
margin-left: 50px;
|
margin-left: 49px;
|
||||||
margin-right: 100px;
|
margin-right: 100px;
|
||||||
|
font-size: $font-14px;
|
||||||
|
|
||||||
&.mx_EventTile_continuation {
|
&.mx_EventTile_continuation {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
@ -77,10 +77,11 @@ limitations under the License.
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SenderProfile {
|
> .mx_SenderProfile {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -2px;
|
top: -2px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
|
font-size: $font-15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-self=false] {
|
&[data-self=false] {
|
||||||
|
@ -113,8 +114,6 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_ReplyTile .mx_SenderProfile {
|
.mx_ReplyTile .mx_SenderProfile {
|
||||||
display: block;
|
display: block;
|
||||||
top: unset;
|
|
||||||
left: unset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ReactionsRow {
|
.mx_ReactionsRow {
|
||||||
|
@ -287,6 +286,8 @@ limitations under the License.
|
||||||
.mx_EventTile_line,
|
.mx_EventTile_line,
|
||||||
.mx_EventTile_info {
|
.mx_EventTile_info {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
// Preserve alignment with left edge of text in bubbles
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_e2eIcon {
|
.mx_EventTile_e2eIcon {
|
||||||
|
@ -294,9 +295,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_line > a {
|
.mx_EventTile_line > a {
|
||||||
|
// Align timestamps with those of normal bubble tiles
|
||||||
right: auto;
|
right: auto;
|
||||||
top: -15px;
|
top: -11px;
|
||||||
left: -68px;
|
left: -95px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,11 +328,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
margin: 0 5px;
|
margin: 0;
|
||||||
> a {
|
> a {
|
||||||
left: auto;
|
// Align timestamps with those of normal bubble tiles
|
||||||
right: 0;
|
left: -76px;
|
||||||
transform: translateX(calc(100% + 5px));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,7 +341,8 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventListSummary[data-expanded=false][data-layout=bubble] {
|
.mx_EventListSummary[data-expanded=false][data-layout=bubble] {
|
||||||
padding: 0 34px;
|
// Align with left edge of bubble tiles
|
||||||
|
padding: 0 49px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* events that do not require bubble layout */
|
/* events that do not require bubble layout */
|
||||||
|
|
|
@ -172,14 +172,12 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In the general case, we leave height of headers alone even if sticky, so
|
// In the general case, we reserve space for each sublist header to prevent
|
||||||
// that the sublists below them do not jump. However, that leaves a gap
|
// scroll jumps when they become sticky. However, that leaves a gap when
|
||||||
// when scrolled to the top above the first sublist (whose header can only
|
// scrolled to the top above the first sublist (whose header can only ever
|
||||||
// ever stick to top), so we force height to 0 for only that first header.
|
// stick to top), so we make sure to exclude the first visible sublist.
|
||||||
// See also https://github.com/vector-im/element-web/issues/14429.
|
&:not(.mx_RoomSublist_hidden) ~ .mx_RoomSublist .mx_RoomSublist_headerContainer {
|
||||||
&:first-child .mx_RoomSublist_headerContainer {
|
height: 24px;
|
||||||
height: 0;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSublist_resizeBox {
|
.mx_RoomSublist_resizeBox {
|
||||||
|
|
|
@ -21,6 +21,17 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_SettingsTab_section {
|
.mx_SettingsTab_section {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
> details {
|
||||||
|
> summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: $primary-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .mx_SettingsFlag {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_PreferencesUserSettingsTab_CommunityMigrator {
|
.mx_PreferencesUserSettingsTab_CommunityMigrator {
|
||||||
|
|
0
res/img/betas/.gitkeep
Normal file
0
res/img/betas/.gitkeep
Normal file
Binary file not shown.
Before Width: | Height: | Size: 380 KiB |
|
@ -184,6 +184,9 @@ $visual-bell-bg-color: #800;
|
||||||
|
|
||||||
$room-warning-bg-color: $header-panel-bg-color;
|
$room-warning-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
|
$authpage-body-bg-color: $background;
|
||||||
|
$authpage-primary-color: $primary-content;
|
||||||
|
|
||||||
$dark-panel-bg-color: $header-panel-bg-color;
|
$dark-panel-bg-color: $header-panel-bg-color;
|
||||||
$panel-gradient: rgba(34, 38, 46, 0), rgba(34, 38, 46, 1);
|
$panel-gradient: rgba(34, 38, 46, 0), rgba(34, 38, 46, 1);
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,8 @@ $tab-label-fg-color: var(--timeline-text-color);
|
||||||
// was #4e5054
|
// was #4e5054
|
||||||
$authpage-lang-color: var(--timeline-text-color);
|
$authpage-lang-color: var(--timeline-text-color);
|
||||||
$roomheader-color: var(--timeline-text-color);
|
$roomheader-color: var(--timeline-text-color);
|
||||||
|
// was #232f32
|
||||||
|
$authpage-primary-color: var(--timeline-text-color);
|
||||||
// --roomlist-text-secondary-color
|
// --roomlist-text-secondary-color
|
||||||
$roomtile-preview-color: var(--roomlist-text-secondary-color);
|
$roomtile-preview-color: var(--roomlist-text-secondary-color);
|
||||||
$roomlist-header-color: var(--roomlist-text-secondary-color);
|
$roomlist-header-color: var(--roomlist-text-secondary-color);
|
||||||
|
|
|
@ -574,11 +574,12 @@ async function doSetLoggedIn(
|
||||||
await abortLogin();
|
await abortLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
PosthogAnalytics.instance.updateAnonymityFromSettings(credentials.userId);
|
|
||||||
|
|
||||||
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
|
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
|
||||||
|
|
||||||
MatrixClientPeg.replaceUsingCreds(credentials);
|
MatrixClientPeg.replaceUsingCreds(credentials);
|
||||||
|
|
||||||
|
PosthogAnalytics.instance.updateAnonymityFromSettings(credentials.userId);
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) {
|
if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) {
|
||||||
|
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import { SettingLevel } from "./settings/SettingLevel";
|
import { SettingLevel } from "./settings/SettingLevel";
|
||||||
import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
|
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
|
|
||||||
// XXX: MediaDeviceKind is a union type, so we make our own enum
|
// XXX: MediaDeviceKind is a union type, so we make our own enum
|
||||||
export enum MediaDeviceKindEnum {
|
export enum MediaDeviceKindEnum {
|
||||||
|
@ -74,8 +74,8 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||||
|
|
||||||
setMatrixCallAudioInput(audioDeviceId);
|
MatrixClientPeg.get().getMediaHandler().setAudioInput(audioDeviceId);
|
||||||
setMatrixCallVideoInput(videoDeviceId);
|
MatrixClientPeg.get().getMediaHandler().setVideoInput(videoDeviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setAudioOutput(deviceId: string): void {
|
public setAudioOutput(deviceId: string): void {
|
||||||
|
@ -90,7 +90,7 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
public setAudioInput(deviceId: string): void {
|
public setAudioInput(deviceId: string): void {
|
||||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||||
setMatrixCallAudioInput(deviceId);
|
MatrixClientPeg.get().getMediaHandler().setAudioInput(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,7 +100,7 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
public setVideoInput(deviceId: string): void {
|
public setVideoInput(deviceId: string): void {
|
||||||
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
||||||
setMatrixCallVideoInput(deviceId);
|
MatrixClientPeg.get().getMediaHandler().setVideoInput(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void {
|
public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void {
|
||||||
|
|
|
@ -18,6 +18,8 @@ import posthog, { PostHog } from 'posthog-js';
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import SettingsStore from './settings/SettingsStore';
|
import SettingsStore from './settings/SettingsStore';
|
||||||
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
/* Posthog analytics tracking.
|
/* Posthog analytics tracking.
|
||||||
*
|
*
|
||||||
|
@ -27,10 +29,11 @@ import SettingsStore from './settings/SettingsStore';
|
||||||
* - If [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is
|
* - 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
|
* enabled, events are not sent (this detection is built into posthog and turned on via the
|
||||||
* `respect_dnt` flag being passed to `posthog.init`).
|
* `respect_dnt` flag being passed to `posthog.init`).
|
||||||
* - If the `feature_pseudonymous_analytics_opt_in` labs flag is `true`, track pseudonomously, i.e.
|
* - If the `feature_pseudonymous_analytics_opt_in` labs flag is `true`, track pseudonomously by maintaining
|
||||||
* hash all matrix identifiers in tracking events (user IDs, room IDs etc) using SHA-256.
|
* a randomised analytics ID in account_data for that user (shared between devices) and sending it to posthog to
|
||||||
* - Otherwise, if the existing `analyticsOptIn` flag is `true`, track anonymously, i.e.
|
identify the user.
|
||||||
* redact all matrix identifiers in tracking events.
|
* - Otherwise, if the existing `analyticsOptIn` flag is `true`, track anonymously, i.e. do not identify the user
|
||||||
|
using any identifier that would be consistent across devices.
|
||||||
* - If both flags are false or not set, events are not sent.
|
* - If both flags are false or not set, events are not sent.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -71,12 +74,6 @@ interface IPageView extends IAnonymousEvent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashHex = async (input: string): Promise<string> => {
|
|
||||||
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([
|
const whitelistedScreens = new Set([
|
||||||
"register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
|
"register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
|
||||||
"start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group",
|
"start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group",
|
||||||
|
@ -89,7 +86,6 @@ export async function getRedactedCurrentLocation(
|
||||||
anonymity: Anonymity,
|
anonymity: Anonymity,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// Redact PII from the current location.
|
// 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 /<screen name>/might/be/pii
|
// For known screens, assumes a URL structure of /<screen name>/might/be/pii
|
||||||
if (origin.startsWith('file://')) {
|
if (origin.startsWith('file://')) {
|
||||||
pathname = "/<redacted_file_scheme_url>/";
|
pathname = "/<redacted_file_scheme_url>/";
|
||||||
|
@ -99,17 +95,13 @@ export async function getRedactedCurrentLocation(
|
||||||
if (hash == "") {
|
if (hash == "") {
|
||||||
hashStr = "";
|
hashStr = "";
|
||||||
} else {
|
} else {
|
||||||
let [beforeFirstSlash, screen, ...parts] = hash.split("/");
|
let [beforeFirstSlash, screen] = hash.split("/");
|
||||||
|
|
||||||
if (!whitelistedScreens.has(screen)) {
|
if (!whitelistedScreens.has(screen)) {
|
||||||
screen = "<redacted_screen_name>";
|
screen = "<redacted_screen_name>";
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
hashStr = `${beforeFirstSlash}/${screen}/<redacted>`;
|
||||||
parts[i] = anonymity === Anonymity.Anonymous ? `<redacted>` : await hashHex(parts[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
hashStr = `${beforeFirstSlash}/${screen}/${parts.join("/")}`;
|
|
||||||
}
|
}
|
||||||
return origin + pathname + hashStr;
|
return origin + pathname + hashStr;
|
||||||
}
|
}
|
||||||
|
@ -123,15 +115,15 @@ export class PosthogAnalytics {
|
||||||
/* Wrapper for Posthog analytics.
|
/* Wrapper for Posthog analytics.
|
||||||
* 3 modes of anonymity are supported, governed by this.anonymity
|
* 3 modes of anonymity are supported, governed by this.anonymity
|
||||||
* - Anonymity.Disabled means *no data* is passed to posthog
|
* - Anonymity.Disabled means *no data* is passed to posthog
|
||||||
* - Anonymity.Anonymous means all identifers will be redacted before being passed to posthog
|
* - Anonymity.Anonymous means no identifier is passed to posthog
|
||||||
* - Anonymity.Pseudonymous means all identifiers will be hashed via SHA-256 before being passed
|
* - Anonymity.Pseudonymous means an analytics ID stored in account_data and shared between devices
|
||||||
* to Posthog
|
* is passed to posthog.
|
||||||
*
|
*
|
||||||
* To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity().
|
* To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity().
|
||||||
*
|
*
|
||||||
* To pass an event to Posthog:
|
* To pass an event to Posthog:
|
||||||
*
|
*
|
||||||
* 1. Declare a type for the event, extending IAnonymousEvent, IPseudonymousEvent or IRoomEvent.
|
* 1. Declare a type for the event, extending IAnonymousEvent or IPseudonymousEvent.
|
||||||
* 2. Call the appropriate track*() method. Pseudonymous events will be dropped when anonymity is
|
* 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.
|
* Anonymous or Disabled; Anonymous events will be dropped when anonymity is Disabled.
|
||||||
*/
|
*/
|
||||||
|
@ -141,6 +133,7 @@ export class PosthogAnalytics {
|
||||||
private enabled = false;
|
private enabled = false;
|
||||||
private static _instance = null;
|
private static _instance = null;
|
||||||
private platformSuperProperties = {};
|
private platformSuperProperties = {};
|
||||||
|
private static ANALYTICS_ID_EVENT_TYPE = "im.vector.web.analytics_id";
|
||||||
|
|
||||||
public static get instance(): PosthogAnalytics {
|
public static get instance(): PosthogAnalytics {
|
||||||
if (!this._instance) {
|
if (!this._instance) {
|
||||||
|
@ -274,9 +267,32 @@ export class PosthogAnalytics {
|
||||||
this.anonymity = anonymity;
|
this.anonymity = anonymity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async identifyUser(userId: string): Promise<void> {
|
private static getRandomAnalyticsId(): string {
|
||||||
|
return [...crypto.getRandomValues(new Uint8Array(16))].map((c) => c.toString(16)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async identifyUser(client: MatrixClient, analyticsIdGenerator: () => string): Promise<void> {
|
||||||
if (this.anonymity == Anonymity.Pseudonymous) {
|
if (this.anonymity == Anonymity.Pseudonymous) {
|
||||||
this.posthog.identify(await hashHex(userId));
|
// Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows
|
||||||
|
// different devices to send the same ID.
|
||||||
|
try {
|
||||||
|
const accountData = await client.getAccountDataFromServer(PosthogAnalytics.ANALYTICS_ID_EVENT_TYPE);
|
||||||
|
let analyticsID = accountData?.id;
|
||||||
|
if (!analyticsID) {
|
||||||
|
// Couldn't retrieve an analytics ID from user settings, so create one and set it on the server.
|
||||||
|
// Note there's a race condition here - if two devices do these steps at the same time, last write
|
||||||
|
// wins, and the first writer will send tracking with an ID that doesn't match the one on the server
|
||||||
|
// until the next time account data is refreshed and this function is called (most likely on next
|
||||||
|
// page load). This will happen pretty infrequently, so we can tolerate the possibility.
|
||||||
|
analyticsID = analyticsIdGenerator();
|
||||||
|
await client.setAccountData("im.vector.web.analytics_id", { id: analyticsID });
|
||||||
|
}
|
||||||
|
this.posthog.identify(analyticsID);
|
||||||
|
} catch (e) {
|
||||||
|
// The above could fail due to network requests, but not essential to starting the application,
|
||||||
|
// so swallow it.
|
||||||
|
console.log("Unable to identify user for tracking" + e.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,18 +323,6 @@ export class PosthogAnalytics {
|
||||||
await this.capture(eventName, properties);
|
await this.capture(eventName, properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async trackRoomEvent<E extends IRoomEvent>(
|
|
||||||
eventName: E["eventName"],
|
|
||||||
roomId: string,
|
|
||||||
properties: Omit<E["properties"], "roomId">,
|
|
||||||
): Promise<void> {
|
|
||||||
const updatedProperties = {
|
|
||||||
...properties,
|
|
||||||
hashedRoomId: roomId ? await hashHex(roomId) : null,
|
|
||||||
};
|
|
||||||
await this.trackPseudonymousEvent(eventName, updatedProperties);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async trackPageView(durationMs: number): Promise<void> {
|
public async trackPageView(durationMs: number): Promise<void> {
|
||||||
const hash = window.location.hash;
|
const hash = window.location.hash;
|
||||||
|
|
||||||
|
@ -349,7 +353,7 @@ export class PosthogAnalytics {
|
||||||
// Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous
|
// Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous
|
||||||
this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings());
|
this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings());
|
||||||
if (userId && this.getAnonymity() == Anonymity.Pseudonymous) {
|
if (userId && this.getAnonymity() == Anonymity.Pseudonymous) {
|
||||||
await this.identifyUser(userId);
|
await this.identifyUser(MatrixClientPeg.get(), PosthogAnalytics.getRandomAnalyticsId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,11 +48,6 @@ export default class Resend {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/element-web/issues/3148
|
// https://github.com/vector-im/element-web/issues/3148
|
||||||
console.log('Resend got send failure: ' + err.name + '(' + err + ')');
|
console.log('Resend got send failure: ' + err.name + '(' + err + ')');
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'message_send_failed',
|
|
||||||
event: event,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,10 +26,9 @@ import { SettingLevel } from "../../../../settings/SettingLevel";
|
||||||
import Field from '../../../../components/views/elements/Field';
|
import Field from '../../../../components/views/elements/Field';
|
||||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||||
|
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps extends IDialogProps {}
|
||||||
onFinished: (confirmed: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
eventIndexSize: number;
|
eventIndexSize: number;
|
||||||
|
|
|
@ -322,10 +322,16 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
const menuClasses = classNames({
|
const menuClasses = classNames({
|
||||||
'mx_ContextualMenu': true,
|
'mx_ContextualMenu': true,
|
||||||
'mx_ContextualMenu_left': !hasChevron && position.left,
|
/**
|
||||||
'mx_ContextualMenu_right': !hasChevron && position.right,
|
* In some cases we may get the number of 0, which still means that we're supposed to properly
|
||||||
'mx_ContextualMenu_top': !hasChevron && position.top,
|
* add the specific position class, but as it was falsy things didn't work as intended.
|
||||||
'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
|
* In addition, defensively check for counter cases where we may get more than one value,
|
||||||
|
* even if we shouldn't.
|
||||||
|
*/
|
||||||
|
'mx_ContextualMenu_left': !hasChevron && position.left !== undefined && !position.right,
|
||||||
|
'mx_ContextualMenu_right': !hasChevron && position.right !== undefined && !position.left,
|
||||||
|
'mx_ContextualMenu_top': !hasChevron && position.top !== undefined && !position.bottom,
|
||||||
|
'mx_ContextualMenu_bottom': !hasChevron && position.bottom !== undefined && !position.top,
|
||||||
'mx_ContextualMenu_withChevron_left': chevronFace === ChevronFace.Left,
|
'mx_ContextualMenu_withChevron_left': chevronFace === ChevronFace.Left,
|
||||||
'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right,
|
'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right,
|
||||||
'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top,
|
'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top,
|
||||||
|
@ -404,17 +410,27 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ToRightOf = {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
chevronOffset: number;
|
||||||
|
};
|
||||||
|
|
||||||
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
||||||
export const toRightOf = (elementRect: Pick<DOMRect, "right" | "top" | "height">, chevronOffset = 12) => {
|
export const toRightOf = (elementRect: Pick<DOMRect, "right" | "top" | "height">, chevronOffset = 12): ToRightOf => {
|
||||||
const left = elementRect.right + window.pageXOffset + 3;
|
const left = elementRect.right + window.pageXOffset + 3;
|
||||||
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
||||||
top -= chevronOffset + 8; // where 8 is half the height of the chevron
|
top -= chevronOffset + 8; // where 8 is half the height of the chevron
|
||||||
return { left, top, chevronOffset };
|
return { left, top, chevronOffset };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AboveLeftOf = IPosition & {
|
||||||
|
chevronFace: ChevronFace;
|
||||||
|
};
|
||||||
|
|
||||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
|
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
|
||||||
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
|
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
|
||||||
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
|
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0): AboveLeftOf => {
|
||||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||||
|
|
||||||
const buttonRight = elementRect.right + window.pageXOffset;
|
const buttonRight = elementRect.right + window.pageXOffset;
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import request from 'browser-request';
|
import request from 'browser-request';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
@ -26,38 +25,43 @@ import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
|
||||||
export default class EmbeddedPage extends React.PureComponent {
|
interface IProps {
|
||||||
static propTypes = {
|
|
||||||
// URL to request embedded page content from
|
// URL to request embedded page content from
|
||||||
url: PropTypes.string,
|
url?: string;
|
||||||
// Class name prefix to apply for a given instance
|
// Class name prefix to apply for a given instance
|
||||||
className: PropTypes.string,
|
className?: string;
|
||||||
// Whether to wrap the page in a scrollbar
|
// Whether to wrap the page in a scrollbar
|
||||||
scrollbar: PropTypes.bool,
|
scrollbar?: boolean;
|
||||||
// Map of keys to replace with values, e.g {$placeholder: "value"}
|
// Map of keys to replace with values, e.g {$placeholder: "value"}
|
||||||
replaceMap: PropTypes.object,
|
replaceMap?: Map<string, string>;
|
||||||
};
|
}
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
interface IState {
|
||||||
|
page: string;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props, context) {
|
export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
|
||||||
|
public static contextType = MatrixClientContext;
|
||||||
|
private unmounted = false;
|
||||||
|
private dispatcherRef: string = null;
|
||||||
|
|
||||||
|
constructor(props: IProps, context: typeof MatrixClientContext) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this._dispatcherRef = null;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
page: '',
|
page: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
translate(s) {
|
protected translate(s: string): string {
|
||||||
// default implementation - skins may wish to extend this
|
// default implementation - skins may wish to extend this
|
||||||
return sanitizeHtml(_t(s));
|
return sanitizeHtml(_t(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this._unmounted = false;
|
this.unmounted = false;
|
||||||
|
|
||||||
if (!this.props.url) {
|
if (!this.props.url) {
|
||||||
return;
|
return;
|
||||||
|
@ -70,7 +74,7 @@ export default class EmbeddedPage extends React.PureComponent {
|
||||||
request(
|
request(
|
||||||
{ method: "GET", url: this.props.url },
|
{ method: "GET", url: this.props.url },
|
||||||
(err, response, body) => {
|
(err, response, body) => {
|
||||||
if (this._unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,22 +96,22 @@ export default class EmbeddedPage extends React.PureComponent {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this._dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
if (this._dispatcherRef !== null) dis.unregister(this._dispatcherRef);
|
if (this.dispatcherRef !== null) dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction = (payload) => {
|
private onAction = (payload: ActionPayload): void => {
|
||||||
// HACK: Workaround for the context's MatrixClient not being set up at render time.
|
// HACK: Workaround for the context's MatrixClient not being set up at render time.
|
||||||
if (payload.action === 'client_started') {
|
if (payload.action === 'client_started') {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
// HACK: Workaround for the context's MatrixClient not updating.
|
// HACK: Workaround for the context's MatrixClient not updating.
|
||||||
const client = this.context || MatrixClientPeg.get();
|
const client = this.context || MatrixClientPeg.get();
|
||||||
const isGuest = client ? client.isGuest() : true;
|
const isGuest = client ? client.isGuest() : true;
|
|
@ -15,16 +15,15 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("structures.GenericErrorPage")
|
interface IProps {
|
||||||
export default class GenericErrorPage extends React.PureComponent {
|
title: React.ReactNode;
|
||||||
static propTypes = {
|
message: React.ReactNode;
|
||||||
title: PropTypes.object.isRequired, // jsx for title
|
}
|
||||||
message: PropTypes.object.isRequired, // jsx to display
|
|
||||||
};
|
|
||||||
|
|
||||||
|
@replaceableComponent("structures.GenericErrorPage")
|
||||||
|
export default class GenericErrorPage extends React.PureComponent<IProps> {
|
||||||
render() {
|
render() {
|
||||||
return <div className='mx_GenericErrorPage'>
|
return <div className='mx_GenericErrorPage'>
|
||||||
<div className='mx_GenericErrorPage_box'>
|
<div className='mx_GenericErrorPage_box'>
|
|
@ -146,19 +146,13 @@ class GroupFilterPanel extends React.Component<IGroupFilterPanelProps, IGroupFil
|
||||||
mx_GroupFilterPanel_items_selected: itemsSelected,
|
mx_GroupFilterPanel_items_selected: itemsSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
let betaDot;
|
|
||||||
if (SettingsStore.getBetaInfo("feature_spaces") && !localStorage.getItem("mx_seenSpacesBeta")) {
|
|
||||||
betaDot = <div className="mx_BetaDot" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let createButton = (
|
let createButton = (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
tooltip
|
tooltip
|
||||||
label={_t("Communities")}
|
label={_t("Communities")}
|
||||||
action="toggle_my_groups"
|
action="toggle_my_groups"
|
||||||
className="mx_TagTile mx_TagTile_plus">
|
className="mx_TagTile mx_TagTile_plus"
|
||||||
{ betaDot }
|
/>
|
||||||
</ActionButton>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||||
|
|
|
@ -14,34 +14,39 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { createRef } from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("structures.IndicatorScrollbar")
|
interface IProps {
|
||||||
export default class IndicatorScrollbar extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
|
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
|
||||||
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
|
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
|
||||||
// by the parent element.
|
// by the parent element.
|
||||||
trackHorizontalOverflow: PropTypes.bool,
|
trackHorizontalOverflow?: boolean;
|
||||||
|
|
||||||
// If true, when the user tries to use their mouse wheel in the component it will
|
// If true, when the user tries to use their mouse wheel in the component it will
|
||||||
// scroll horizontally rather than vertically. This should only be used on components
|
// scroll horizontally rather than vertically. This should only be used on components
|
||||||
// with no vertical scroll opportunity.
|
// with no vertical scroll opportunity.
|
||||||
verticalScrollsHorizontally: PropTypes.bool,
|
verticalScrollsHorizontally?: boolean;
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
children: React.ReactNode;
|
||||||
|
className: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
leftIndicatorOffset: number | string;
|
||||||
|
rightIndicatorOffset: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("structures.IndicatorScrollbar")
|
||||||
|
export default class IndicatorScrollbar extends React.Component<IProps, IState> {
|
||||||
|
private autoHideScrollbar = createRef<AutoHideScrollbar>();
|
||||||
|
private scrollElement: HTMLDivElement;
|
||||||
|
private likelyTrackpadUser: boolean = null;
|
||||||
|
private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this._collectScroller = this._collectScroller.bind(this);
|
|
||||||
this._collectScrollerComponent = this._collectScrollerComponent.bind(this);
|
|
||||||
this.checkOverflow = this.checkOverflow.bind(this);
|
|
||||||
this._scrollElement = null;
|
|
||||||
this._autoHideScrollbar = null;
|
|
||||||
this._likelyTrackpadUser = null;
|
|
||||||
this._checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
leftIndicatorOffset: 0,
|
leftIndicatorOffset: 0,
|
||||||
|
@ -49,30 +54,19 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
moveToOrigin() {
|
private collectScroller = (scroller: HTMLDivElement): void => {
|
||||||
if (!this._scrollElement) return;
|
if (scroller && !this.scrollElement) {
|
||||||
|
this.scrollElement = scroller;
|
||||||
this._scrollElement.scrollLeft = 0;
|
|
||||||
this._scrollElement.scrollTop = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_collectScroller(scroller) {
|
|
||||||
if (scroller && !this._scrollElement) {
|
|
||||||
this._scrollElement = scroller;
|
|
||||||
// Using the passive option to not block the main thread
|
// Using the passive option to not block the main thread
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
this._scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
|
this.scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
|
||||||
this.checkOverflow();
|
this.checkOverflow();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_collectScrollerComponent(autoHideScrollbar) {
|
public componentDidUpdate(prevProps: IProps): void {
|
||||||
this._autoHideScrollbar = autoHideScrollbar;
|
const prevLen = React.Children.count(prevProps.children);
|
||||||
}
|
const curLen = React.Children.count(this.props.children);
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const prevLen = prevProps && prevProps.children && prevProps.children.length || 0;
|
|
||||||
const curLen = this.props.children && this.props.children.length || 0;
|
|
||||||
// check overflow only if amount of children changes.
|
// check overflow only if amount of children changes.
|
||||||
// if we don't guard here, we end up with an infinite
|
// if we don't guard here, we end up with an infinite
|
||||||
// render > componentDidUpdate > checkOverflow > setState > render loop
|
// render > componentDidUpdate > checkOverflow > setState > render loop
|
||||||
|
@ -81,62 +75,58 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.checkOverflow();
|
this.checkOverflow();
|
||||||
}
|
}
|
||||||
|
|
||||||
checkOverflow() {
|
private checkOverflow = (): void => {
|
||||||
const hasTopOverflow = this._scrollElement.scrollTop > 0;
|
const hasTopOverflow = this.scrollElement.scrollTop > 0;
|
||||||
const hasBottomOverflow = this._scrollElement.scrollHeight >
|
const hasBottomOverflow = this.scrollElement.scrollHeight >
|
||||||
(this._scrollElement.scrollTop + this._scrollElement.clientHeight);
|
(this.scrollElement.scrollTop + this.scrollElement.clientHeight);
|
||||||
const hasLeftOverflow = this._scrollElement.scrollLeft > 0;
|
const hasLeftOverflow = this.scrollElement.scrollLeft > 0;
|
||||||
const hasRightOverflow = this._scrollElement.scrollWidth >
|
const hasRightOverflow = this.scrollElement.scrollWidth >
|
||||||
(this._scrollElement.scrollLeft + this._scrollElement.clientWidth);
|
(this.scrollElement.scrollLeft + this.scrollElement.clientWidth);
|
||||||
|
|
||||||
if (hasTopOverflow) {
|
if (hasTopOverflow) {
|
||||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
|
this.scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
|
||||||
} else {
|
} else {
|
||||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
|
this.scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
|
||||||
}
|
}
|
||||||
if (hasBottomOverflow) {
|
if (hasBottomOverflow) {
|
||||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
|
this.scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
|
||||||
} else {
|
} else {
|
||||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
|
this.scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
|
||||||
}
|
}
|
||||||
if (hasLeftOverflow) {
|
if (hasLeftOverflow) {
|
||||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_leftOverflow");
|
this.scrollElement.classList.add("mx_IndicatorScrollbar_leftOverflow");
|
||||||
} else {
|
} else {
|
||||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_leftOverflow");
|
this.scrollElement.classList.remove("mx_IndicatorScrollbar_leftOverflow");
|
||||||
}
|
}
|
||||||
if (hasRightOverflow) {
|
if (hasRightOverflow) {
|
||||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_rightOverflow");
|
this.scrollElement.classList.add("mx_IndicatorScrollbar_rightOverflow");
|
||||||
} else {
|
} else {
|
||||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
|
this.scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.trackHorizontalOverflow) {
|
if (this.props.trackHorizontalOverflow) {
|
||||||
this.setState({
|
this.setState({
|
||||||
// Offset from absolute position of the container
|
// Offset from absolute position of the container
|
||||||
leftIndicatorOffset: hasLeftOverflow ? `${this._scrollElement.scrollLeft}px` : '0',
|
leftIndicatorOffset: hasLeftOverflow ? `${this.scrollElement.scrollLeft}px` : '0',
|
||||||
|
|
||||||
// Negative because we're coming from the right
|
// Negative because we're coming from the right
|
||||||
rightIndicatorOffset: hasRightOverflow ? `-${this._scrollElement.scrollLeft}px` : '0',
|
rightIndicatorOffset: hasRightOverflow ? `-${this.scrollElement.scrollLeft}px` : '0',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
getScrollTop() {
|
public componentWillUnmount(): void {
|
||||||
return this._autoHideScrollbar.getScrollTop();
|
if (this.scrollElement) {
|
||||||
}
|
this.scrollElement.removeEventListener("scroll", this.checkOverflow);
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this._scrollElement) {
|
|
||||||
this._scrollElement.removeEventListener("scroll", this.checkOverflow);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseWheel = (e) => {
|
private onMouseWheel = (e: React.WheelEvent): void => {
|
||||||
if (this.props.verticalScrollsHorizontally && this._scrollElement) {
|
if (this.props.verticalScrollsHorizontally && this.scrollElement) {
|
||||||
// xyThreshold is the amount of horizontal motion required for the component to
|
// xyThreshold is the amount of horizontal motion required for the component to
|
||||||
// ignore the vertical delta in a scroll. Used to stop trackpads from acting in
|
// ignore the vertical delta in a scroll. Used to stop trackpads from acting in
|
||||||
// strange ways. Should be positive.
|
// strange ways. Should be positive.
|
||||||
|
@ -150,19 +140,19 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
// for at least the next 1 minute.
|
// for at least the next 1 minute.
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
if (Math.abs(e.deltaX) > 0) {
|
if (Math.abs(e.deltaX) > 0) {
|
||||||
this._likelyTrackpadUser = true;
|
this.likelyTrackpadUser = true;
|
||||||
this._checkAgainForTrackpad = now + (1 * 60 * 1000);
|
this.checkAgainForTrackpad = now + (1 * 60 * 1000);
|
||||||
} else {
|
} else {
|
||||||
// if we haven't seen any horizontal scrolling for a while, assume
|
// if we haven't seen any horizontal scrolling for a while, assume
|
||||||
// the user might have plugged in a mousewheel
|
// the user might have plugged in a mousewheel
|
||||||
if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) {
|
if (this.likelyTrackpadUser && now >= this.checkAgainForTrackpad) {
|
||||||
this._likelyTrackpadUser = false;
|
this.likelyTrackpadUser = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't mess with the horizontal scroll for trackpad users
|
// don't mess with the horizontal scroll for trackpad users
|
||||||
// See https://github.com/vector-im/element-web/issues/10005
|
// See https://github.com/vector-im/element-web/issues/10005
|
||||||
if (this._likelyTrackpadUser) {
|
if (this.likelyTrackpadUser) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,13 +168,13 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
|
|
||||||
// noinspection JSSuspiciousNameCombination
|
// noinspection JSSuspiciousNameCombination
|
||||||
const val = Math.abs(e.deltaY) < 25 ? (e.deltaY + additionalScroll) : e.deltaY;
|
const val = Math.abs(e.deltaY) < 25 ? (e.deltaY + additionalScroll) : e.deltaY;
|
||||||
this._scrollElement.scrollLeft += val * yRetention;
|
this.scrollElement.scrollLeft += val * yRetention;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
|
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
|
||||||
|
|
||||||
const leftIndicatorStyle = { left: this.state.leftIndicatorOffset };
|
const leftIndicatorStyle = { left: this.state.leftIndicatorOffset };
|
||||||
|
@ -195,8 +185,8 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
||||||
|
|
||||||
return (<AutoHideScrollbar
|
return (<AutoHideScrollbar
|
||||||
ref={this._collectScrollerComponent}
|
ref={this.autoHideScrollbar}
|
||||||
wrappedRef={this._collectScroller}
|
wrappedRef={this.collectScroller}
|
||||||
onWheel={this.onMouseWheel}
|
onWheel={this.onMouseWheel}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
|
@ -76,7 +76,6 @@ const LeftPanelWidget: React.FC = () => {
|
||||||
<AppTile
|
<AppTile
|
||||||
app={app}
|
app={app}
|
||||||
fullWidth
|
fullWidth
|
||||||
show
|
|
||||||
showMenubar={false}
|
showMenubar={false}
|
||||||
userWidget
|
userWidget
|
||||||
userId={cli.getUserId()}
|
userId={cli.getUserId()}
|
||||||
|
|
115
src/components/structures/LegacyCommunityPreview.tsx
Normal file
115
src/components/structures/LegacyCommunityPreview.tsx
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
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 MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
import { _t } from "../../languageHandler";
|
||||||
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
|
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||||
|
import { IGroupSummary } from "../views/dialogs/CreateSpaceFromCommunityDialog";
|
||||||
|
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
||||||
|
import Spinner from "../views/elements/Spinner";
|
||||||
|
import GroupAvatar from "../views/avatars/GroupAvatar";
|
||||||
|
import { linkifyElement } from "../../HtmlUtils";
|
||||||
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
import { Action } from "../../dispatcher/actions";
|
||||||
|
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
groupId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSwapClick = () => {
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: Action.ViewUserSettings,
|
||||||
|
initialTabId: UserTab.Preferences,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// XXX: temporary community migration component, reuses SpaceRoomView & SpacePreview classes for simplicity
|
||||||
|
const LegacyCommunityPreview = ({ groupId }: IProps) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
|
const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [cli, groupId]);
|
||||||
|
|
||||||
|
if (!groupSummary) {
|
||||||
|
return <main className="mx_SpaceRoomView">
|
||||||
|
<div className="mx_MainSplit">
|
||||||
|
<div className="mx_SpaceRoomView_preview">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibilitySection: JSX.Element;
|
||||||
|
if (groupSummary.profile.is_public) {
|
||||||
|
visibilitySection = <span className="mx_SpaceRoomView_info_public">
|
||||||
|
{ _t("Public community") }
|
||||||
|
</span>;
|
||||||
|
} else {
|
||||||
|
visibilitySection = <span className="mx_SpaceRoomView_info_private">
|
||||||
|
{ _t("Private community") }
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <main className="mx_SpaceRoomView">
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div className="mx_MainSplit">
|
||||||
|
<div className="mx_SpaceRoomView_preview">
|
||||||
|
<GroupAvatar
|
||||||
|
groupId={groupId}
|
||||||
|
groupName={groupSummary.profile.name}
|
||||||
|
groupAvatarUrl={groupSummary.profile.avatar_url}
|
||||||
|
height={80}
|
||||||
|
width={80}
|
||||||
|
resizeMethod='crop'
|
||||||
|
/>
|
||||||
|
<h1 className="mx_SpaceRoomView_preview_name">
|
||||||
|
{ groupSummary.profile.name }
|
||||||
|
</h1>
|
||||||
|
<div className="mx_SpaceRoomView_info">
|
||||||
|
{ visibilitySection }
|
||||||
|
</div>
|
||||||
|
<div className="mx_SpaceRoomView_preview_topic" ref={e => e && linkifyElement(e)}>
|
||||||
|
{ groupSummary.profile.short_description }
|
||||||
|
</div>
|
||||||
|
<div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
||||||
|
{ groupSummary.user?.membership === "join"
|
||||||
|
? _t("To view %(communityName)s, swap to communities in your <a>preferences</a>", {
|
||||||
|
communityName: groupSummary.profile.name,
|
||||||
|
}, {
|
||||||
|
a: sub => (
|
||||||
|
<AccessibleButton onClick={onSwapClick} kind="link">{ sub }</AccessibleButton>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: _t("To join %(communityName)s, swap to communities in your <a>preferences</a>", {
|
||||||
|
communityName: groupSummary.profile.name,
|
||||||
|
}, {
|
||||||
|
a: sub => (
|
||||||
|
<AccessibleButton onClick={onSwapClick} kind="link">{ sub }</AccessibleButton>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</main>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LegacyCommunityPreview;
|
|
@ -69,6 +69,7 @@ import classNames from 'classnames';
|
||||||
import GroupFilterPanel from './GroupFilterPanel';
|
import GroupFilterPanel from './GroupFilterPanel';
|
||||||
import CustomRoomTagPanel from './CustomRoomTagPanel';
|
import CustomRoomTagPanel from './CustomRoomTagPanel';
|
||||||
import { mediaFromMxc } from "../../customisations/Media";
|
import { mediaFromMxc } from "../../customisations/Media";
|
||||||
|
import LegacyCommunityPreview from "./LegacyCommunityPreview";
|
||||||
|
|
||||||
// We need to fetch each pinned message individually (if we don't already have it)
|
// We need to fetch each pinned message individually (if we don't already have it)
|
||||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||||
|
@ -629,11 +630,15 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
pageElement = <UserView userId={this.props.currentUserId} resizeNotifier={this.props.resizeNotifier} />;
|
pageElement = <UserView userId={this.props.currentUserId} resizeNotifier={this.props.resizeNotifier} />;
|
||||||
break;
|
break;
|
||||||
case PageTypes.GroupView:
|
case PageTypes.GroupView:
|
||||||
|
if (SpaceStore.spacesEnabled) {
|
||||||
|
pageElement = <LegacyCommunityPreview groupId={this.props.currentGroupId} />;
|
||||||
|
} else {
|
||||||
pageElement = <GroupView
|
pageElement = <GroupView
|
||||||
groupId={this.props.currentGroupId}
|
groupId={this.props.currentGroupId}
|
||||||
isNew={this.props.currentGroupIsNew}
|
isNew={this.props.currentGroupIsNew}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
/>;
|
/>;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,25 +16,35 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Resizable } from 're-resizable';
|
import { NumberSize, Resizable } from 're-resizable';
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
|
import { Direction } from "re-resizable/lib/resizer";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
collapsedRhs?: boolean;
|
||||||
|
panel?: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.MainSplit")
|
@replaceableComponent("structures.MainSplit")
|
||||||
export default class MainSplit extends React.Component {
|
export default class MainSplit extends React.Component<IProps> {
|
||||||
_onResizeStart = () => {
|
private onResizeStart = (): void => {
|
||||||
this.props.resizeNotifier.startResizing();
|
this.props.resizeNotifier.startResizing();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onResize = () => {
|
private onResize = (): void => {
|
||||||
this.props.resizeNotifier.notifyRightHandleResized();
|
this.props.resizeNotifier.notifyRightHandleResized();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onResizeStop = (event, direction, refToElement, delta) => {
|
private onResizeStop = (
|
||||||
|
event: MouseEvent | TouchEvent, direction: Direction, elementRef: HTMLElement, delta: NumberSize,
|
||||||
|
): void => {
|
||||||
this.props.resizeNotifier.stopResizing();
|
this.props.resizeNotifier.stopResizing();
|
||||||
window.localStorage.setItem("mx_rhs_size", this._loadSidePanelSize().width + delta.width);
|
window.localStorage.setItem("mx_rhs_size", (this.loadSidePanelSize().width + delta.width).toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
_loadSidePanelSize() {
|
private loadSidePanelSize(): {height: string | number, width: number} {
|
||||||
let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
|
let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
|
||||||
|
|
||||||
if (isNaN(rhsSize)) {
|
if (isNaN(rhsSize)) {
|
||||||
|
@ -47,7 +57,7 @@ export default class MainSplit extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const bodyView = React.Children.only(this.props.children);
|
const bodyView = React.Children.only(this.props.children);
|
||||||
const panelView = this.props.panel;
|
const panelView = this.props.panel;
|
||||||
|
|
||||||
|
@ -56,7 +66,7 @@ export default class MainSplit extends React.Component {
|
||||||
let children;
|
let children;
|
||||||
if (hasResizer) {
|
if (hasResizer) {
|
||||||
children = <Resizable
|
children = <Resizable
|
||||||
defaultSize={this._loadSidePanelSize()}
|
defaultSize={this.loadSidePanelSize()}
|
||||||
minWidth={264}
|
minWidth={264}
|
||||||
maxWidth="50%"
|
maxWidth="50%"
|
||||||
enable={{
|
enable={{
|
||||||
|
@ -69,9 +79,9 @@ export default class MainSplit extends React.Component {
|
||||||
bottomLeft: false,
|
bottomLeft: false,
|
||||||
topLeft: false,
|
topLeft: false,
|
||||||
}}
|
}}
|
||||||
onResizeStart={this._onResizeStart}
|
onResizeStart={this.onResizeStart}
|
||||||
onResize={this._onResize}
|
onResize={this.onResize}
|
||||||
onResizeStop={this._onResizeStop}
|
onResizeStop={this.onResizeStop}
|
||||||
className="mx_RightPanel_ResizeWrapper"
|
className="mx_RightPanel_ResizeWrapper"
|
||||||
handleClasses={{ left: "mx_RightPanel_ResizeHandle" }}
|
handleClasses={{ left: "mx_RightPanel_ResizeHandle" }}
|
||||||
>
|
>
|
|
@ -143,7 +143,7 @@ export enum Views {
|
||||||
SOFT_LOGOUT,
|
SOFT_LOGOUT,
|
||||||
}
|
}
|
||||||
|
|
||||||
const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas"];
|
const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas", "welcome"];
|
||||||
|
|
||||||
// Actions that are redirected through the onboarding process prior to being
|
// Actions that are redirected through the onboarding process prior to being
|
||||||
// re-dispatched. NOTE: some actions are non-trivial and would require
|
// re-dispatched. NOTE: some actions are non-trivial and would require
|
||||||
|
@ -1800,11 +1800,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
subAction: params.action,
|
subAction: params.action,
|
||||||
});
|
});
|
||||||
} else if (screen.indexOf('group/') === 0) {
|
} else if (screen.indexOf('group/') === 0) {
|
||||||
if (SpaceStore.spacesEnabled) {
|
|
||||||
dis.dispatch({ action: "view_home_page" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupId = screen.substring(6);
|
const groupId = screen.substring(6);
|
||||||
|
|
||||||
// TODO: Check valid group ID
|
// TODO: Check valid group ID
|
||||||
|
@ -1897,15 +1892,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
onSendEvent(roomId: string, event: MatrixEvent) {
|
onSendEvent(roomId: string, event: MatrixEvent) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (!cli) {
|
if (!cli) return;
|
||||||
dis.dispatch({ action: 'message_send_failed' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => {
|
cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => {
|
||||||
dis.dispatch({ action: 'message_sent' });
|
dis.dispatch({ action: 'message_sent' });
|
||||||
}, (err) => {
|
|
||||||
dis.dispatch({ action: 'message_send_failed' });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -448,7 +448,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
// Always show highlighted event
|
// Always show highlighted event
|
||||||
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
||||||
|
|
||||||
if (mxEv.replyInThread
|
// Checking if the message has a "parentEventId" as we do not
|
||||||
|
// want to hide the root event of the thread
|
||||||
|
if (mxEv.replyInThread && mxEv.parentEventId
|
||||||
&& this.props.hideThreadedMessages
|
&& this.props.hideThreadedMessages
|
||||||
&& SettingsStore.getValue("feature_thread")) {
|
&& SettingsStore.getValue("feature_thread")) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -25,7 +25,6 @@ import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import BetaCard from "../views/beta/BetaCard";
|
|
||||||
|
|
||||||
@replaceableComponent("structures.MyGroups")
|
@replaceableComponent("structures.MyGroups")
|
||||||
export default class MyGroups extends React.Component {
|
export default class MyGroups extends React.Component {
|
||||||
|
@ -138,7 +137,6 @@ export default class MyGroups extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</div>*/ }
|
</div>*/ }
|
||||||
</div>
|
</div>
|
||||||
<BetaCard featureId="feature_spaces" title={_t("Communities are changing to Spaces")} />
|
|
||||||
<div className="mx_MyGroups_content">
|
<div className="mx_MyGroups_content">
|
||||||
{ contentHeader }
|
{ contentHeader }
|
||||||
{ content }
|
{ content }
|
||||||
|
|
|
@ -15,95 +15,110 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t, _td } from '../../languageHandler';
|
import { _t, _td } from '../../languageHandler';
|
||||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
|
||||||
import Resend from '../../Resend';
|
import Resend from '../../Resend';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import { EventStatus } from "matrix-js-sdk/src/models/event";
|
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import NotificationBadge from "../views/rooms/NotificationBadge";
|
import NotificationBadge from "../views/rooms/NotificationBadge";
|
||||||
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||||
|
import { SyncState } from "matrix-js-sdk/src/sync.api";
|
||||||
|
import { ISyncStateData } from "matrix-js-sdk/src/sync";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
const STATUS_BAR_HIDDEN = 0;
|
const STATUS_BAR_HIDDEN = 0;
|
||||||
const STATUS_BAR_EXPANDED = 1;
|
const STATUS_BAR_EXPANDED = 1;
|
||||||
const STATUS_BAR_EXPANDED_LARGE = 2;
|
const STATUS_BAR_EXPANDED_LARGE = 2;
|
||||||
|
|
||||||
export function getUnsentMessages(room) {
|
export function getUnsentMessages(room: Room): MatrixEvent[] {
|
||||||
if (!room) { return []; }
|
if (!room) { return []; }
|
||||||
return room.getPendingEvents().filter(function(ev) {
|
return room.getPendingEvents().filter(function(ev) {
|
||||||
return ev.status === EventStatus.NOT_SENT;
|
return ev.status === EventStatus.NOT_SENT;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.RoomStatusBar")
|
interface IProps {
|
||||||
export default class RoomStatusBar extends React.PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
// the room this statusbar is representing.
|
// the room this statusbar is representing.
|
||||||
room: PropTypes.object.isRequired,
|
room: Room;
|
||||||
|
|
||||||
// true if the room is being peeked at. This affects components that shouldn't
|
// true if the room is being peeked at. This affects components that shouldn't
|
||||||
// logically be shown when peeking, such as a prompt to invite people to a room.
|
// logically be shown when peeking, such as a prompt to invite people to a room.
|
||||||
isPeeking: PropTypes.bool,
|
isPeeking?: boolean;
|
||||||
|
|
||||||
// callback for when the user clicks on the 'resend all' button in the
|
// callback for when the user clicks on the 'resend all' button in the
|
||||||
// 'unsent messages' bar
|
// 'unsent messages' bar
|
||||||
onResendAllClick: PropTypes.func,
|
onResendAllClick?: () => void;
|
||||||
|
|
||||||
// callback for when the user clicks on the 'cancel all' button in the
|
// callback for when the user clicks on the 'cancel all' button in the
|
||||||
// 'unsent messages' bar
|
// 'unsent messages' bar
|
||||||
onCancelAllClick: PropTypes.func,
|
onCancelAllClick?: () => void;
|
||||||
|
|
||||||
// callback for when the user clicks on the 'invite others' button in the
|
// callback for when the user clicks on the 'invite others' button in the
|
||||||
// 'you are alone' bar
|
// 'you are alone' bar
|
||||||
onInviteClick: PropTypes.func,
|
onInviteClick?: () => void;
|
||||||
|
|
||||||
// callback for when we do something that changes the size of the
|
// callback for when we do something that changes the size of the
|
||||||
// status bar. This is used to trigger a re-layout in the parent
|
// status bar. This is used to trigger a re-layout in the parent
|
||||||
// component.
|
// component.
|
||||||
onResize: PropTypes.func,
|
onResize?: () => void;
|
||||||
|
|
||||||
// callback for when the status bar can be hidden from view, as it is
|
// callback for when the status bar can be hidden from view, as it is
|
||||||
// not displaying anything
|
// not displaying anything
|
||||||
onHidden: PropTypes.func,
|
onHidden?: () => void;
|
||||||
|
|
||||||
// callback for when the status bar is displaying something and should
|
// callback for when the status bar is displaying something and should
|
||||||
// be visible
|
// be visible
|
||||||
onVisible: PropTypes.func,
|
onVisible?: () => void;
|
||||||
};
|
}
|
||||||
|
|
||||||
state = {
|
interface IState {
|
||||||
syncState: MatrixClientPeg.get().getSyncState(),
|
syncState: SyncState;
|
||||||
syncStateData: MatrixClientPeg.get().getSyncStateData(),
|
syncStateData: ISyncStateData;
|
||||||
|
unsentMessages: MatrixEvent[];
|
||||||
|
isResending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("structures.RoomStatusBar")
|
||||||
|
export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
|
||||||
|
public static contextType = MatrixClientContext;
|
||||||
|
|
||||||
|
constructor(props: IProps, context: typeof MatrixClientContext) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
syncState: this.context.getSyncState(),
|
||||||
|
syncStateData: this.context.getSyncStateData(),
|
||||||
unsentMessages: getUnsentMessages(this.props.room),
|
unsentMessages: getUnsentMessages(this.props.room),
|
||||||
isResending: false,
|
isResending: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
|
||||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
|
||||||
|
|
||||||
this._checkSize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
public componentDidMount(): void {
|
||||||
this._checkSize();
|
const client = this.context;
|
||||||
|
client.on("sync", this.onSyncStateChange);
|
||||||
|
client.on("Room.localEchoUpdated", this.onRoomLocalEchoUpdated);
|
||||||
|
|
||||||
|
this.checkSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentDidUpdate(): void {
|
||||||
|
this.checkSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount(): void {
|
||||||
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
||||||
const client = MatrixClientPeg.get();
|
const client = this.context;
|
||||||
if (client) {
|
if (client) {
|
||||||
client.removeListener("sync", this.onSyncStateChange);
|
client.removeListener("sync", this.onSyncStateChange);
|
||||||
client.removeListener("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
client.removeListener("Room.localEchoUpdated", this.onRoomLocalEchoUpdated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSyncStateChange = (state, prevState, data) => {
|
private onSyncStateChange = (state: SyncState, prevState: SyncState, data: ISyncStateData): void => {
|
||||||
if (state === "SYNCING" && prevState === "SYNCING") {
|
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -113,7 +128,7 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onResendAllClick = () => {
|
private onResendAllClick = (): void => {
|
||||||
Resend.resendUnsentEvents(this.props.room).then(() => {
|
Resend.resendUnsentEvents(this.props.room).then(() => {
|
||||||
this.setState({ isResending: false });
|
this.setState({ isResending: false });
|
||||||
});
|
});
|
||||||
|
@ -121,12 +136,12 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
dis.fire(Action.FocusSendMessageComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCancelAllClick = () => {
|
private onCancelAllClick = (): void => {
|
||||||
Resend.cancelUnsentEvents(this.props.room);
|
Resend.cancelUnsentEvents(this.props.room);
|
||||||
dis.fire(Action.FocusSendMessageComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {
|
private onRoomLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
|
||||||
if (room.roomId !== this.props.room.roomId) return;
|
if (room.roomId !== this.props.room.roomId) return;
|
||||||
const messages = getUnsentMessages(this.props.room);
|
const messages = getUnsentMessages(this.props.room);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -136,8 +151,8 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check whether current size is greater than 0, if yes call props.onVisible
|
// Check whether current size is greater than 0, if yes call props.onVisible
|
||||||
_checkSize() {
|
private checkSize(): void {
|
||||||
if (this._getSize()) {
|
if (this.getSize()) {
|
||||||
if (this.props.onVisible) this.props.onVisible();
|
if (this.props.onVisible) this.props.onVisible();
|
||||||
} else {
|
} else {
|
||||||
if (this.props.onHidden) this.props.onHidden();
|
if (this.props.onHidden) this.props.onHidden();
|
||||||
|
@ -147,8 +162,8 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
// We don't need the actual height - just whether it is likely to have
|
// We don't need the actual height - just whether it is likely to have
|
||||||
// changed - so we use '0' to indicate normal size, and other values to
|
// changed - so we use '0' to indicate normal size, and other values to
|
||||||
// indicate other sizes.
|
// indicate other sizes.
|
||||||
_getSize() {
|
private getSize(): number {
|
||||||
if (this._shouldShowConnectionError()) {
|
if (this.shouldShowConnectionError()) {
|
||||||
return STATUS_BAR_EXPANDED;
|
return STATUS_BAR_EXPANDED;
|
||||||
} else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
} else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
||||||
return STATUS_BAR_EXPANDED_LARGE;
|
return STATUS_BAR_EXPANDED_LARGE;
|
||||||
|
@ -156,7 +171,7 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
return STATUS_BAR_HIDDEN;
|
return STATUS_BAR_HIDDEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
_shouldShowConnectionError() {
|
private shouldShowConnectionError(): boolean {
|
||||||
// no conn bar trumps the "some not sent" msg since you can't resend without
|
// no conn bar trumps the "some not sent" msg since you can't resend without
|
||||||
// a connection!
|
// a connection!
|
||||||
// There's one situation in which we don't show this 'no connection' bar, and that's
|
// There's one situation in which we don't show this 'no connection' bar, and that's
|
||||||
|
@ -164,12 +179,12 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
const errorIsMauError = Boolean(
|
const errorIsMauError = Boolean(
|
||||||
this.state.syncStateData &&
|
this.state.syncStateData &&
|
||||||
this.state.syncStateData.error &&
|
this.state.syncStateData.error &&
|
||||||
this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED',
|
this.state.syncStateData.error.name === 'M_RESOURCE_LIMIT_EXCEEDED',
|
||||||
);
|
);
|
||||||
return this.state.syncState === "ERROR" && !errorIsMauError;
|
return this.state.syncState === "ERROR" && !errorIsMauError;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getUnsentMessageContent() {
|
private getUnsentMessageContent(): JSX.Element {
|
||||||
const unsentMessages = this.state.unsentMessages;
|
const unsentMessages = this.state.unsentMessages;
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
|
@ -221,10 +236,10 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
let buttonRow = <>
|
let buttonRow = <>
|
||||||
<AccessibleButton onClick={this._onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
|
<AccessibleButton onClick={this.onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
|
||||||
{ _t("Delete all") }
|
{ _t("Delete all") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton onClick={this._onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
|
<AccessibleButton onClick={this.onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
|
||||||
{ _t("Retry all") }
|
{ _t("Retry all") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</>;
|
</>;
|
||||||
|
@ -260,8 +275,8 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this._shouldShowConnectionError()) {
|
if (this.shouldShowConnectionError()) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar">
|
<div className="mx_RoomStatusBar">
|
||||||
<div role="alert">
|
<div role="alert">
|
||||||
|
@ -287,7 +302,7 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
||||||
return this._getUnsentMessageContent();
|
return this.getUnsentMessageContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Key } from '../../Keyboard';
|
import { Key } from '../../Keyboard';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
|
@ -24,106 +23,116 @@ import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("structures.SearchBox")
|
interface IProps {
|
||||||
export default class SearchBox extends React.Component {
|
onSearch?: (query: string) => void;
|
||||||
static propTypes = {
|
onCleared?: (source?: string) => void;
|
||||||
onSearch: PropTypes.func,
|
onKeyDown?: (ev: React.KeyboardEvent) => void;
|
||||||
onCleared: PropTypes.func,
|
onFocus?: (ev: React.FocusEvent) => void;
|
||||||
onKeyDown: PropTypes.func,
|
onBlur?: (ev: React.FocusEvent) => void;
|
||||||
className: PropTypes.string,
|
className?: string;
|
||||||
placeholder: PropTypes.string.isRequired,
|
placeholder: string;
|
||||||
autoFocus: PropTypes.bool,
|
blurredPlaceholder?: string;
|
||||||
initialValue: PropTypes.string,
|
autoFocus?: boolean;
|
||||||
|
initialValue?: string;
|
||||||
|
collapsed?: boolean;
|
||||||
|
|
||||||
// If true, the search box will focus and clear itself
|
// If true, the search box will focus and clear itself
|
||||||
// on room search focus action (it would be nicer to take
|
// on room search focus action (it would be nicer to take
|
||||||
// this functionality out, but not obvious how that would work)
|
// this functionality out, but not obvious how that would work)
|
||||||
enableRoomSearchFocus: PropTypes.bool,
|
enableRoomSearchFocus?: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
interface IState {
|
||||||
|
searchTerm: string;
|
||||||
|
blurred: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("structures.SearchBox")
|
||||||
|
export default class SearchBox extends React.Component<IProps, IState> {
|
||||||
|
private dispatcherRef: string;
|
||||||
|
private search = createRef<HTMLInputElement>();
|
||||||
|
|
||||||
|
static defaultProps: Partial<IProps> = {
|
||||||
enableRoomSearchFocus: false,
|
enableRoomSearchFocus: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._search = createRef();
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
searchTerm: this.props.initialValue || "",
|
searchTerm: props.initialValue || "",
|
||||||
blurred: true,
|
blurred: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction = payload => {
|
private onAction = (payload): void => {
|
||||||
if (!this.props.enableRoomSearchFocus) return;
|
if (!this.props.enableRoomSearchFocus) return;
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'view_room':
|
case 'view_room':
|
||||||
if (this._search.current && payload.clear_search) {
|
if (this.search.current && payload.clear_search) {
|
||||||
this._clearSearch();
|
this.clearSearch();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'focus_room_filter':
|
case 'focus_room_filter':
|
||||||
if (this._search.current) {
|
if (this.search.current) {
|
||||||
this._search.current.focus();
|
this.search.current.focus();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onChange = () => {
|
private onChange = (): void => {
|
||||||
if (!this._search.current) return;
|
if (!this.search.current) return;
|
||||||
this.setState({ searchTerm: this._search.current.value });
|
this.setState({ searchTerm: this.search.current.value });
|
||||||
this.onSearch();
|
this.onSearch();
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearch = throttle(() => {
|
private onSearch = throttle((): void => {
|
||||||
this.props.onSearch(this._search.current.value);
|
this.props.onSearch(this.search.current.value);
|
||||||
}, 200, { trailing: true, leading: true });
|
}, 200, { trailing: true, leading: true });
|
||||||
|
|
||||||
_onKeyDown = ev => {
|
private onKeyDown = (ev: React.KeyboardEvent): void => {
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case Key.ESCAPE:
|
case Key.ESCAPE:
|
||||||
this._clearSearch("keyboard");
|
this.clearSearch("keyboard");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (this.props.onKeyDown) this.props.onKeyDown(ev);
|
if (this.props.onKeyDown) this.props.onKeyDown(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onFocus = ev => {
|
private onFocus = (ev: React.FocusEvent): void => {
|
||||||
this.setState({ blurred: false });
|
this.setState({ blurred: false });
|
||||||
ev.target.select();
|
(ev.target as HTMLInputElement).select();
|
||||||
if (this.props.onFocus) {
|
if (this.props.onFocus) {
|
||||||
this.props.onFocus(ev);
|
this.props.onFocus(ev);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onBlur = ev => {
|
private onBlur = (ev: React.FocusEvent): void => {
|
||||||
this.setState({ blurred: true });
|
this.setState({ blurred: true });
|
||||||
if (this.props.onBlur) {
|
if (this.props.onBlur) {
|
||||||
this.props.onBlur(ev);
|
this.props.onBlur(ev);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_clearSearch(source) {
|
private clearSearch(source?: string): void {
|
||||||
this._search.current.value = "";
|
this.search.current.value = "";
|
||||||
this.onChange();
|
this.onChange();
|
||||||
if (this.props.onCleared) {
|
if (this.props.onCleared) {
|
||||||
this.props.onCleared(source);
|
this.props.onCleared(source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
// check for collapsed here and
|
// check for collapsed here and
|
||||||
// not at parent so we keep
|
// not at parent so we keep
|
||||||
// searchTerm in our state
|
// searchTerm in our state
|
||||||
|
@ -136,7 +145,7 @@ export default class SearchBox extends React.Component {
|
||||||
key="button"
|
key="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="mx_SearchBox_closeButton"
|
className="mx_SearchBox_closeButton"
|
||||||
onClick={() => {this._clearSearch("button"); }}
|
onClick={() => {this.clearSearch("button"); }}
|
||||||
/>) : undefined;
|
/>) : undefined;
|
||||||
|
|
||||||
// show a shorter placeholder when blurred, if requested
|
// show a shorter placeholder when blurred, if requested
|
||||||
|
@ -151,13 +160,13 @@ export default class SearchBox extends React.Component {
|
||||||
<input
|
<input
|
||||||
key="searchfield"
|
key="searchfield"
|
||||||
type="text"
|
type="text"
|
||||||
ref={this._search}
|
ref={this.search}
|
||||||
className={"mx_textinput_icon mx_textinput_search " + className}
|
className={"mx_textinput_icon mx_textinput_search " + className}
|
||||||
value={this.state.searchTerm}
|
value={this.state.searchTerm}
|
||||||
onFocus={this._onFocus}
|
onFocus={this.onFocus}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onKeyDown={this._onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onBlur={this._onBlur}
|
onBlur={this.onBlur}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoFocus={this.props.autoFocus}
|
autoFocus={this.props.autoFocus}
|
|
@ -57,12 +57,20 @@ import { Key } from "../../Keyboard";
|
||||||
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
|
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
|
||||||
import { getDisplayAliasForRoom } from "./RoomDirectory";
|
import { getDisplayAliasForRoom } from "./RoomDirectory";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
import { useEventEmitterState } from "../../hooks/useEventEmitter";
|
||||||
|
import { IOOBData } from "../../stores/ThreepidInviteStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
additionalButtons?: ReactNode;
|
additionalButtons?: ReactNode;
|
||||||
showRoom(cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, autoJoin?: boolean): void;
|
showRoom(
|
||||||
|
cli: MatrixClient,
|
||||||
|
hierarchy: RoomHierarchy,
|
||||||
|
roomId: string,
|
||||||
|
autoJoin?: boolean,
|
||||||
|
roomType?: RoomType,
|
||||||
|
): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ITileProps {
|
interface ITileProps {
|
||||||
|
@ -71,7 +79,7 @@ interface ITileProps {
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
numChildRooms?: number;
|
numChildRooms?: number;
|
||||||
hasPermissions?: boolean;
|
hasPermissions?: boolean;
|
||||||
onViewRoomClick(autoJoin: boolean): void;
|
onViewRoomClick(autoJoin: boolean, roomType: RoomType): void;
|
||||||
onToggleClick?(): void;
|
onToggleClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +95,8 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null;
|
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]
|
const joinedRoomName = useEventEmitterState(joinedRoom, "Room.name", room => room?.name);
|
||||||
|
const name = joinedRoomName || room.name || room.canonical_alias || room.aliases?.[0]
|
||||||
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
||||||
|
|
||||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||||
|
@ -96,12 +105,12 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
const onPreviewClick = (ev: ButtonEvent) => {
|
const onPreviewClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
onViewRoomClick(false);
|
onViewRoomClick(false, room.room_type as RoomType);
|
||||||
};
|
};
|
||||||
const onJoinClick = (ev: ButtonEvent) => {
|
const onJoinClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
onViewRoomClick(true);
|
onViewRoomClick(true, room.room_type as RoomType);
|
||||||
};
|
};
|
||||||
|
|
||||||
let button;
|
let button;
|
||||||
|
@ -278,7 +287,13 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
</li>;
|
</li>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, autoJoin = false) => {
|
export const showRoom = (
|
||||||
|
cli: MatrixClient,
|
||||||
|
hierarchy: RoomHierarchy,
|
||||||
|
roomId: string,
|
||||||
|
autoJoin = false,
|
||||||
|
roomType?: RoomType,
|
||||||
|
) => {
|
||||||
const room = hierarchy.roomMap.get(roomId);
|
const room = hierarchy.roomMap.get(roomId);
|
||||||
|
|
||||||
// Don't let the user view a room they won't be able to either peek or join:
|
// Don't let the user view a room they won't be able to either peek or join:
|
||||||
|
@ -303,7 +318,8 @@ export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
|
||||||
avatarUrl: room.avatar_url,
|
avatarUrl: room.avatar_url,
|
||||||
// XXX: This logic is duplicated from the JS SDK which would normally decide what the name is.
|
// XXX: This logic is duplicated from the JS SDK which would normally decide what the name is.
|
||||||
name: room.name || roomAlias || _t("Unnamed room"),
|
name: room.name || roomAlias || _t("Unnamed room"),
|
||||||
},
|
roomType,
|
||||||
|
} as IOOBData,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -313,7 +329,7 @@ interface IHierarchyLevelProps {
|
||||||
hierarchy: RoomHierarchy;
|
hierarchy: RoomHierarchy;
|
||||||
parents: Set<string>;
|
parents: Set<string>;
|
||||||
selectedMap?: Map<string, Set<string>>;
|
selectedMap?: Map<string, Set<string>>;
|
||||||
onViewRoomClick(roomId: string, autoJoin: boolean): void;
|
onViewRoomClick(roomId: string, autoJoin: boolean, roomType?: RoomType): void;
|
||||||
onToggleClick?(parentId: string, childId: string): void;
|
onToggleClick?(parentId: string, childId: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,8 +367,8 @@ export const HierarchyLevel = ({
|
||||||
room={room}
|
room={room}
|
||||||
suggested={hierarchy.isSuggested(root.room_id, room.room_id)}
|
suggested={hierarchy.isSuggested(root.room_id, room.room_id)}
|
||||||
selected={selectedMap?.get(root.room_id)?.has(room.room_id)}
|
selected={selectedMap?.get(root.room_id)?.has(room.room_id)}
|
||||||
onViewRoomClick={(autoJoin) => {
|
onViewRoomClick={(autoJoin, roomType) => {
|
||||||
onViewRoomClick(room.room_id, autoJoin);
|
onViewRoomClick(room.room_id, autoJoin, roomType);
|
||||||
}}
|
}}
|
||||||
hasPermissions={hasPermissions}
|
hasPermissions={hasPermissions}
|
||||||
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, room.room_id) : undefined}
|
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, room.room_id) : undefined}
|
||||||
|
@ -371,8 +387,8 @@ export const HierarchyLevel = ({
|
||||||
}).length}
|
}).length}
|
||||||
suggested={hierarchy.isSuggested(root.room_id, space.room_id)}
|
suggested={hierarchy.isSuggested(root.room_id, space.room_id)}
|
||||||
selected={selectedMap?.get(root.room_id)?.has(space.room_id)}
|
selected={selectedMap?.get(root.room_id)?.has(space.room_id)}
|
||||||
onViewRoomClick={(autoJoin) => {
|
onViewRoomClick={(autoJoin, roomType) => {
|
||||||
onViewRoomClick(space.room_id, autoJoin);
|
onViewRoomClick(space.room_id, autoJoin, roomType);
|
||||||
}}
|
}}
|
||||||
hasPermissions={hasPermissions}
|
hasPermissions={hasPermissions}
|
||||||
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, space.room_id) : undefined}
|
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, space.room_id) : undefined}
|
||||||
|
@ -574,7 +590,7 @@ const SpaceHierarchy = ({
|
||||||
const { loading, rooms, hierarchy, loadMore } = useSpaceSummary(space);
|
const { loading, rooms, hierarchy, loadMore } = useSpaceSummary(space);
|
||||||
|
|
||||||
const filteredRoomSet = useMemo<Set<IHierarchyRoom>>(() => {
|
const filteredRoomSet = useMemo<Set<IHierarchyRoom>>(() => {
|
||||||
if (!rooms.length) return new Set();
|
if (!rooms?.length) return new Set();
|
||||||
const lcQuery = query.toLowerCase().trim();
|
const lcQuery = query.toLowerCase().trim();
|
||||||
if (!lcQuery) return new Set(rooms);
|
if (!lcQuery) return new Set(rooms);
|
||||||
|
|
||||||
|
@ -650,8 +666,8 @@ const SpaceHierarchy = ({
|
||||||
parents={new Set()}
|
parents={new Set()}
|
||||||
selectedMap={selected}
|
selectedMap={selected}
|
||||||
onToggleClick={hasPermissions ? onToggleClick : undefined}
|
onToggleClick={hasPermissions ? onToggleClick : undefined}
|
||||||
onViewRoomClick={(roomId, autoJoin) => {
|
onViewRoomClick={(roomId, autoJoin, roomType) => {
|
||||||
showRoom(cli, hierarchy, roomId, autoJoin);
|
showRoom(cli, hierarchy, roomId, autoJoin, roomType);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>;
|
</>;
|
||||||
|
|
|
@ -78,6 +78,7 @@ import { CreateEventField, IGroupSummary } from "../views/dialogs/CreateSpaceFro
|
||||||
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import GroupAvatar from "../views/avatars/GroupAvatar";
|
import GroupAvatar from "../views/avatars/GroupAvatar";
|
||||||
|
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -155,10 +156,10 @@ const SpaceInfo = ({ space }) => {
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBetaClick = () => {
|
const onPreferencesClick = () => {
|
||||||
defaultDispatcher.dispatch({
|
defaultDispatcher.dispatch({
|
||||||
action: Action.ViewUserSettings,
|
action: Action.ViewUserSettings,
|
||||||
initialTabId: UserTab.Labs,
|
initialTabId: UserTab.Preferences,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -191,6 +192,11 @@ interface ISpacePreviewProps {
|
||||||
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISpacePreviewProps) => {
|
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISpacePreviewProps) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const myMembership = useMyRoomMembership(space);
|
const myMembership = useMyRoomMembership(space);
|
||||||
|
useDispatcher(defaultDispatcher, payload => {
|
||||||
|
if (payload.action === Action.JoinRoomError && payload.roomId === space.roomId) {
|
||||||
|
setBusy(false); // stop the spinner, join failed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
@ -280,15 +286,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
|
||||||
if (!spacesEnabled) {
|
if (!spacesEnabled) {
|
||||||
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
||||||
{ myMembership === "join"
|
{ myMembership === "join"
|
||||||
? _t("To view %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
? _t("To view this Space, hide communities in your <a>preferences</a>", {}, {
|
||||||
spaceName: space.name,
|
a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
|
||||||
}, {
|
|
||||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
|
||||||
})
|
})
|
||||||
: _t("To join %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
: _t("To join this Space, hide communities in your <a>preferences</a>", {}, {
|
||||||
spaceName: space.name,
|
a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
|
||||||
}, {
|
|
||||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -725,7 +727,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx_SpaceRoomView_inviteTeammates_betaDisclaimer">
|
<div className="mx_SpaceRoomView_inviteTeammates_betaDisclaimer">
|
||||||
<BetaPill onClick={onBetaClick} />
|
<BetaPill />
|
||||||
{ _t("<b>This is an experimental feature.</b> For now, " +
|
{ _t("<b>This is an experimental feature.</b> For now, " +
|
||||||
"new users receiving an invite will have to open the invite on <link/> to actually join.", {}, {
|
"new users receiving an invite will have to open the invite on <link/> to actually join.", {}, {
|
||||||
b: sub => <b>{ sub }</b>,
|
b: sub => <b>{ sub }</b>,
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
|
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
|
||||||
import { Thread } from 'matrix-js-sdk/src/models/thread';
|
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||||
|
|
||||||
import BaseCard from "../views/right_panel/BaseCard";
|
import BaseCard from "../views/right_panel/BaseCard";
|
||||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||||
|
@ -46,13 +46,13 @@ export default class ThreadPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
this.room.on("Thread.update", this.onThreadEventReceived);
|
this.room.on(ThreadEvent.Update, this.onThreadEventReceived);
|
||||||
this.room.on("Thread.ready", this.onThreadEventReceived);
|
this.room.on(ThreadEvent.Ready, this.onThreadEventReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
this.room.removeListener("Thread.update", this.onThreadEventReceived);
|
this.room.removeListener(ThreadEvent.Update, this.onThreadEventReceived);
|
||||||
this.room.removeListener("Thread.ready", this.onThreadEventReceived);
|
this.room.removeListener(ThreadEvent.Ready, this.onThreadEventReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onThreadEventReceived = () => this.updateThreads();
|
private onThreadEventReceived = () => this.updateThreads();
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
|
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
|
||||||
import { Thread } from 'matrix-js-sdk/src/models/thread';
|
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||||
|
|
||||||
import BaseCard from "../views/right_panel/BaseCard";
|
import BaseCard from "../views/right_panel/BaseCard";
|
||||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||||
|
@ -99,15 +99,15 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
thread = new Thread([mxEv], this.props.room, client);
|
thread = new Thread([mxEv], this.props.room, client);
|
||||||
mxEv.setThread(thread);
|
mxEv.setThread(thread);
|
||||||
}
|
}
|
||||||
thread.on("Thread.update", this.updateThread);
|
thread.on(ThreadEvent.Update, this.updateThread);
|
||||||
thread.once("Thread.ready", this.updateThread);
|
thread.once(ThreadEvent.Ready, this.updateThread);
|
||||||
this.updateThread(thread);
|
this.updateThread(thread);
|
||||||
};
|
};
|
||||||
|
|
||||||
private teardownThread = () => {
|
private teardownThread = () => {
|
||||||
if (this.state.thread) {
|
if (this.state.thread) {
|
||||||
this.state.thread.removeListener("Thread.update", this.updateThread);
|
this.state.thread.removeListener(ThreadEvent.Update, this.updateThread);
|
||||||
this.state.thread.removeListener("Thread.ready", this.updateThread);
|
this.state.thread.removeListener(ThreadEvent.Ready, this.updateThread);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -16,52 +16,60 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import * as sdk from "../../index";
|
|
||||||
import Modal from '../../Modal';
|
import Modal from '../../Modal';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import HomePage from "./HomePage";
|
import HomePage from "./HomePage";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||||
|
import MainSplit from "./MainSplit";
|
||||||
|
import RightPanel from "./RightPanel";
|
||||||
|
import Spinner from "../views/elements/Spinner";
|
||||||
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
userId?: string;
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
loading: boolean;
|
||||||
|
member?: RoomMember;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.UserView")
|
@replaceableComponent("structures.UserView")
|
||||||
export default class UserView extends React.Component {
|
export default class UserView extends React.Component<IProps, IState> {
|
||||||
static get propTypes() {
|
constructor(props: IProps) {
|
||||||
return {
|
super(props);
|
||||||
userId: PropTypes.string,
|
this.state = {
|
||||||
|
loading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
public componentDidMount(): void {
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.props.userId) {
|
if (this.props.userId) {
|
||||||
this._loadProfileInfo();
|
this.loadProfileInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
public componentDidUpdate(prevProps: IProps): void {
|
||||||
// XXX: We shouldn't need to null check the userId here, but we declare
|
// XXX: We shouldn't need to null check the userId here, but we declare
|
||||||
// it as optional and MatrixChat sometimes fires in a way which results
|
// it as optional and MatrixChat sometimes fires in a way which results
|
||||||
// in an NPE when we try to update the profile info.
|
// in an NPE when we try to update the profile info.
|
||||||
if (prevProps.userId !== this.props.userId && this.props.userId) {
|
if (prevProps.userId !== this.props.userId && this.props.userId) {
|
||||||
this._loadProfileInfo();
|
this.loadProfileInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadProfileInfo() {
|
private async loadProfileInfo(): Promise<void> {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
let profileInfo;
|
let profileInfo;
|
||||||
try {
|
try {
|
||||||
profileInfo = await cli.getProfileInfo(this.props.userId);
|
profileInfo = await cli.getProfileInfo(this.props.userId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
|
||||||
Modal.createTrackedDialog(_t('Could not load user profile'), '', ErrorDialog, {
|
Modal.createTrackedDialog(_t('Could not load user profile'), '', ErrorDialog, {
|
||||||
title: _t('Could not load user profile'),
|
title: _t('Could not load user profile'),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
|
@ -75,14 +83,11 @@ export default class UserView extends React.Component {
|
||||||
this.setState({ member, loading: false });
|
this.setState({ member, loading: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
} else if (this.state.member) {
|
} else if (this.state.member?.user) {
|
||||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
const panel = <RightPanel user={this.state.member.user} resizeNotifier={this.props.resizeNotifier} />;
|
||||||
const MainSplit = sdk.getComponent('structures.MainSplit');
|
|
||||||
const panel = <RightPanel user={this.state.member} />;
|
|
||||||
return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
|
return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
|
||||||
<HomePage />
|
<HomePage />
|
||||||
</MainSplit>);
|
</MainSplit>);
|
|
@ -17,24 +17,28 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import SyntaxHighlight from "../views/elements/SyntaxHighlight";
|
import SyntaxHighlight from "../views/elements/SyntaxHighlight";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import * as sdk from "../../index";
|
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
|
import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
|
||||||
import { canEditContent } from "../../utils/EventUtils";
|
import { canEditContent } from "../../utils/EventUtils";
|
||||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { IDialogProps } from "../views/dialogs/IDialogProps";
|
||||||
|
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
mxEvent: MatrixEvent; // the MatrixEvent associated with the context menu
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
isEditing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.ViewSource")
|
@replaceableComponent("structures.ViewSource")
|
||||||
export default class ViewSource extends React.Component {
|
export default class ViewSource extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
mxEvent: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -42,19 +46,20 @@ export default class ViewSource extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack() {
|
private onBack(): void {
|
||||||
// TODO: refresh the "Event ID:" modal header
|
// TODO: refresh the "Event ID:" modal header
|
||||||
this.setState({ isEditing: false });
|
this.setState({ isEditing: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
onEdit() {
|
private onEdit(): void {
|
||||||
this.setState({ isEditing: true });
|
this.setState({ isEditing: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the dialog body for viewing the event source
|
// returns the dialog body for viewing the event source
|
||||||
viewSourceContent() {
|
private viewSourceContent(): JSX.Element {
|
||||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||||
const isEncrypted = mxEvent.isEncrypted();
|
const isEncrypted = mxEvent.isEncrypted();
|
||||||
|
// @ts-ignore
|
||||||
const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
|
const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
|
||||||
const originalEventSource = mxEvent.event;
|
const originalEventSource = mxEvent.event;
|
||||||
|
|
||||||
|
@ -86,7 +91,7 @@ export default class ViewSource extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the id of the initial message, not the id of the previous edit
|
// returns the id of the initial message, not the id of the previous edit
|
||||||
getBaseEventId() {
|
private getBaseEventId(): string {
|
||||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||||
const isEncrypted = mxEvent.isEncrypted();
|
const isEncrypted = mxEvent.isEncrypted();
|
||||||
const baseMxEvent = this.props.mxEvent;
|
const baseMxEvent = this.props.mxEvent;
|
||||||
|
@ -100,7 +105,7 @@ export default class ViewSource extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the SendCustomEvent component prefilled with the correct details
|
// returns the SendCustomEvent component prefilled with the correct details
|
||||||
editSourceContent() {
|
private editSourceContent(): JSX.Element {
|
||||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||||
|
|
||||||
const isStateEvent = mxEvent.isState();
|
const isStateEvent = mxEvent.isState();
|
||||||
|
@ -159,14 +164,13 @@ export default class ViewSource extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canSendStateEvent(mxEvent) {
|
private canSendStateEvent(mxEvent: MatrixEvent): boolean {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const room = cli.getRoom(mxEvent.getRoomId());
|
const room = cli.getRoom(mxEvent.getRoomId());
|
||||||
return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
|
return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
|
||||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||||
|
|
||||||
const isEditing = this.state.isEditing;
|
const isEditing = this.state.isEditing;
|
|
@ -15,43 +15,48 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
|
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
import { ChevronFace, ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
member: RoomMember;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
resizeMethod?: ResizeMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
hasStatus: boolean;
|
||||||
|
menuDisplayed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
|
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
|
||||||
export default class MemberStatusMessageAvatar extends React.Component {
|
export default class MemberStatusMessageAvatar extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
public static defaultProps: Partial<IProps> = {
|
||||||
member: PropTypes.object.isRequired,
|
|
||||||
width: PropTypes.number,
|
|
||||||
height: PropTypes.number,
|
|
||||||
resizeMethod: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
};
|
};
|
||||||
|
private button = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hasStatus: this.hasStatus,
|
hasStatus: this.hasStatus,
|
||||||
menuDisplayed: false,
|
menuDisplayed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._button = createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
|
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
|
||||||
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
|
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
|
||||||
}
|
}
|
||||||
|
@ -62,44 +67,44 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
const { user } = this.props.member;
|
const { user } = this.props.member;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.removeListener(
|
user.removeListener(
|
||||||
"User._unstable_statusMessage",
|
"User._unstable_statusMessage",
|
||||||
this._onStatusMessageCommitted,
|
this.onStatusMessageCommitted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasStatus() {
|
private get hasStatus(): boolean {
|
||||||
const { user } = this.props.member;
|
const { user } = this.props.member;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !!user._unstable_statusMessage;
|
return !!user.unstable_statusMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onStatusMessageCommitted = () => {
|
private onStatusMessageCommitted = (): void => {
|
||||||
// The `User` object has observed a status message change.
|
// The `User` object has observed a status message change.
|
||||||
this.setState({
|
this.setState({
|
||||||
hasStatus: this.hasStatus,
|
hasStatus: this.hasStatus,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
openMenu = () => {
|
private openMenu = (): void => {
|
||||||
this.setState({ menuDisplayed: true });
|
this.setState({ menuDisplayed: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
closeMenu = () => {
|
private closeMenu = (): void => {
|
||||||
this.setState({ menuDisplayed: false });
|
this.setState({ menuDisplayed: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const avatar = <MemberAvatar
|
const avatar = <MemberAvatar
|
||||||
member={this.props.member}
|
member={this.props.member}
|
||||||
width={this.props.width}
|
width={this.props.width}
|
||||||
|
@ -118,7 +123,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
|
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
if (this.state.menuDisplayed) {
|
if (this.state.menuDisplayed) {
|
||||||
const elementRect = this._button.current.getBoundingClientRect();
|
const elementRect = this.button.current.getBoundingClientRect();
|
||||||
|
|
||||||
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
|
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
|
||||||
const chevronMargin = 1; // Add some spacing away from target
|
const chevronMargin = 1; // Add some spacing away from target
|
||||||
|
@ -126,13 +131,13 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
chevronOffset={(elementRect.width - chevronWidth) / 2}
|
chevronOffset={(elementRect.width - chevronWidth) / 2}
|
||||||
chevronFace="bottom"
|
chevronFace={ChevronFace.Bottom}
|
||||||
left={elementRect.left + window.pageXOffset}
|
left={elementRect.left + window.pageXOffset}
|
||||||
top={elementRect.top + window.pageYOffset - chevronMargin}
|
top={elementRect.top + window.pageYOffset - chevronMargin}
|
||||||
menuWidth={226}
|
menuWidth={226}
|
||||||
onFinished={this.closeMenu}
|
onFinished={this.closeMenu}
|
||||||
>
|
>
|
||||||
<StatusMessageContextMenu user={this.props.member.user} onFinished={this.closeMenu} />
|
<StatusMessageContextMenu user={this.props.member.user} />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -140,7 +145,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
className={classes}
|
className={classes}
|
||||||
inputRef={this._button}
|
inputRef={this.button}
|
||||||
onClick={this.openMenu}
|
onClick={this.openMenu}
|
||||||
isExpanded={this.state.menuDisplayed}
|
isExpanded={this.state.menuDisplayed}
|
||||||
label={_t("User Status")}
|
label={_t("User Status")}
|
|
@ -15,45 +15,41 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
/*
|
interface IProps {
|
||||||
* This component can be used to display generic HTML content in a contextual
|
element: React.ReactNode;
|
||||||
* menu.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.GenericElementContextMenu")
|
|
||||||
export default class GenericElementContextMenu extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
element: PropTypes.element.isRequired,
|
|
||||||
// Function to be called when the parent window is resized
|
// Function to be called when the parent window is resized
|
||||||
// This can be used to reposition or close the menu on resize and
|
// This can be used to reposition or close the menu on resize and
|
||||||
// ensure that it is not displayed in a stale position.
|
// ensure that it is not displayed in a stale position.
|
||||||
onResize: PropTypes.func,
|
onResize?: () => void;
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.resize = this.resize.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
/**
|
||||||
this.resize = this.resize.bind(this);
|
* This component can be used to display generic HTML content in a contextual
|
||||||
|
* menu.
|
||||||
|
*/
|
||||||
|
@replaceableComponent("views.context_menus.GenericElementContextMenu")
|
||||||
|
export default class GenericElementContextMenu extends React.Component<IProps> {
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount(): void {
|
||||||
window.addEventListener("resize", this.resize);
|
window.addEventListener("resize", this.resize);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
window.removeEventListener("resize", this.resize);
|
window.removeEventListener("resize", this.resize);
|
||||||
}
|
}
|
||||||
|
|
||||||
resize() {
|
private resize = (): void => {
|
||||||
if (this.props.onResize) {
|
if (this.props.onResize) {
|
||||||
this.props.onResize();
|
this.props.onResize();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
return <div>{ this.props.element }</div>;
|
return <div>{ this.props.element }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,16 +15,15 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.GenericTextContextMenu")
|
interface IProps {
|
||||||
export default class GenericTextContextMenu extends React.Component {
|
message: string;
|
||||||
static propTypes = {
|
}
|
||||||
message: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
@replaceableComponent("views.context_menus.GenericTextContextMenu")
|
||||||
|
export default class GenericTextContextMenu extends React.Component<IProps> {
|
||||||
|
public render(): JSX.Element {
|
||||||
return <div>{ this.props.message }</div>;
|
return <div>{ this.props.message }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,53 +14,59 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ChangeEvent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import * as sdk from '../../../index';
|
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// js-sdk User object. Not required because it might not exist.
|
||||||
|
user?: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
message: string;
|
||||||
|
waiting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
|
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
|
||||||
export default class StatusMessageContextMenu extends React.Component {
|
export default class StatusMessageContextMenu extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
// js-sdk User object. Not required because it might not exist.
|
|
||||||
user: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
message: this.comittedStatusMessage,
|
message: this.comittedStatusMessage,
|
||||||
|
waiting: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.removeListener(
|
user.removeListener(
|
||||||
"User._unstable_statusMessage",
|
"User._unstable_statusMessage",
|
||||||
this._onStatusMessageCommitted,
|
this.onStatusMessageCommitted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get comittedStatusMessage() {
|
get comittedStatusMessage(): string {
|
||||||
return this.props.user ? this.props.user._unstable_statusMessage : "";
|
return this.props.user ? this.props.user.unstable_statusMessage : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
_onStatusMessageCommitted = () => {
|
private onStatusMessageCommitted = (): void => {
|
||||||
// The `User` object has observed a status message change.
|
// The `User` object has observed a status message change.
|
||||||
this.setState({
|
this.setState({
|
||||||
message: this.comittedStatusMessage,
|
message: this.comittedStatusMessage,
|
||||||
|
@ -68,14 +74,14 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onClearClick = (e) => {
|
private onClearClick = (): void=> {
|
||||||
MatrixClientPeg.get()._unstable_setStatusMessage("");
|
MatrixClientPeg.get()._unstable_setStatusMessage("");
|
||||||
this.setState({
|
this.setState({
|
||||||
waiting: true,
|
waiting: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onSubmit = (e) => {
|
private onSubmit = (e: ButtonEvent): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
|
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -83,27 +89,25 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onStatusChange = (e) => {
|
private onStatusChange = (e: ChangeEvent): void => {
|
||||||
// The input field's value was changed.
|
// The input field's value was changed.
|
||||||
this.setState({
|
this.setState({
|
||||||
message: e.target.value,
|
message: (e.target as HTMLInputElement).value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
|
||||||
|
|
||||||
let actionButton;
|
let actionButton;
|
||||||
if (this.comittedStatusMessage) {
|
if (this.comittedStatusMessage) {
|
||||||
if (this.state.message === this.comittedStatusMessage) {
|
if (this.state.message === this.comittedStatusMessage) {
|
||||||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
|
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
|
||||||
onClick={this._onClearClick}
|
onClick={this.onClearClick}
|
||||||
>
|
>
|
||||||
<span>{ _t("Clear status") }</span>
|
<span>{ _t("Clear status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
} else {
|
} else {
|
||||||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
||||||
onClick={this._onSubmit}
|
onClick={this.onSubmit}
|
||||||
>
|
>
|
||||||
<span>{ _t("Update status") }</span>
|
<span>{ _t("Update status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
@ -112,7 +116,7 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
actionButton = <AccessibleButton
|
actionButton = <AccessibleButton
|
||||||
className="mx_StatusMessageContextMenu_submit"
|
className="mx_StatusMessageContextMenu_submit"
|
||||||
disabled={!this.state.message}
|
disabled={!this.state.message}
|
||||||
onClick={this._onSubmit}
|
onClick={this.onSubmit}
|
||||||
>
|
>
|
||||||
<span>{ _t("Set status") }</span>
|
<span>{ _t("Set status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
@ -120,13 +124,13 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
|
|
||||||
let spinner = null;
|
let spinner = null;
|
||||||
if (this.state.waiting) {
|
if (this.state.waiting) {
|
||||||
spinner = <Spinner w="24" h="24" />;
|
spinner = <Spinner w={24} h={24} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = <form
|
const form = <form
|
||||||
className="mx_StatusMessageContextMenu_form"
|
className="mx_StatusMessageContextMenu_form"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onSubmit={this._onSubmit}
|
onSubmit={this.onSubmit}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -134,9 +138,9 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
key="message"
|
key="message"
|
||||||
placeholder={_t("Set a new status...")}
|
placeholder={_t("Set a new status...")}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
maxLength="60"
|
maxLength={60}
|
||||||
value={this.state.message}
|
value={this.state.message}
|
||||||
onChange={this._onStatusChange}
|
onChange={this.onStatusChange}
|
||||||
/>
|
/>
|
||||||
<div className="mx_StatusMessageContextMenu_actionContainer">
|
<div className="mx_StatusMessageContextMenu_actionContainer">
|
||||||
{ actionButton }
|
{ actionButton }
|
|
@ -258,7 +258,6 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={filterPlaceholder}
|
placeholder={filterPlaceholder}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoComplete={true}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
|
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
|
||||||
|
|
|
@ -23,10 +23,9 @@ import Modal from '../../../Modal';
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
import QuestionDialog from "./QuestionDialog";
|
import QuestionDialog from "./QuestionDialog";
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps extends IDialogProps {}
|
||||||
onFinished: (success: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
|
const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
|
|
|
@ -243,7 +243,6 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={_t("Search for rooms or people")}
|
placeholder={_t("Search for rooms or people")}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoComplete={true}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_ForwardList_content">
|
<AutoHideScrollbar className="mx_ForwardList_content">
|
||||||
|
|
|
@ -57,7 +57,6 @@ const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => {
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={filterPlaceholder}
|
placeholder={filterPlaceholder}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoComplete={true}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">
|
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">
|
||||||
|
@ -98,13 +97,13 @@ const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave
|
||||||
definitions={[
|
definitions={[
|
||||||
{
|
{
|
||||||
value: RoomsToLeave.None,
|
value: RoomsToLeave.None,
|
||||||
label: _t("Don't leave any"),
|
label: _t("Don't leave any rooms"),
|
||||||
}, {
|
}, {
|
||||||
value: RoomsToLeave.All,
|
value: RoomsToLeave.All,
|
||||||
label: _t("Leave all rooms and spaces"),
|
label: _t("Leave all rooms"),
|
||||||
}, {
|
}, {
|
||||||
value: RoomsToLeave.Specific,
|
value: RoomsToLeave.Specific,
|
||||||
label: _t("Leave specific rooms and spaces"),
|
label: _t("Leave some rooms"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -167,11 +166,13 @@ const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content" id="mx_LeaveSpaceDialog">
|
<div className="mx_Dialog_content" id="mx_LeaveSpaceDialog">
|
||||||
<p>
|
<p>
|
||||||
{ _t("Are you sure you want to leave <spaceName/>?", {}, {
|
{ _t("You are about to leave <spaceName/>.", {}, {
|
||||||
spaceName: () => <b>{ space.name }</b>,
|
spaceName: () => <b>{ space.name }</b>,
|
||||||
}) }
|
}) }
|
||||||
|
|
||||||
{ rejoinWarning }
|
{ rejoinWarning }
|
||||||
|
{ rejoinWarning && (<> </>) }
|
||||||
|
{ spaceChildren.length > 0 && _t("Would you like to leave the rooms in this space?") }
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{ spaceChildren.length > 0 && <LeaveRoomsPicker
|
{ spaceChildren.length > 0 && <LeaveRoomsPicker
|
||||||
|
|
|
@ -126,7 +126,6 @@ const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [],
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={_t("Search spaces")}
|
placeholder={_t("Search spaces")}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoComplete={true}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">
|
<AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { User } from "matrix-js-sdk/src/models/user";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import E2EIcon from "../rooms/E2EIcon";
|
import E2EIcon, { E2EState } from "../rooms/E2EIcon";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import { IDialogProps } from "./IDialogProps";
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
@ -47,7 +47,7 @@ const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) =
|
||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
className="mx_UntrustedDeviceDialog"
|
className="mx_UntrustedDeviceDialog"
|
||||||
title={<>
|
title={<>
|
||||||
<E2EIcon status="warning" size={24} hideTooltip={true} />
|
<E2EIcon status={E2EState.Warning} size={24} hideTooltip={true} />
|
||||||
{ _t("Not Trusted") }
|
{ _t("Not Trusted") }
|
||||||
</>}
|
</>}
|
||||||
>
|
>
|
||||||
|
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
||||||
|
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import AccessibleButton from './AccessibleButton';
|
import AccessibleButton from './AccessibleButton';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -39,33 +38,95 @@ import { MatrixCapabilities } from "matrix-widget-api";
|
||||||
import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
|
import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { IApp } from "../../../stores/WidgetStore";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
app: IApp;
|
||||||
|
// If room is not specified then it is an account level widget
|
||||||
|
// which bypasses permission prompts as it was added explicitly by that user
|
||||||
|
room: Room;
|
||||||
|
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||||
|
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||||
|
fullWidth?: boolean;
|
||||||
|
// Optional. If set, renders a smaller view of the widget
|
||||||
|
miniMode?: boolean;
|
||||||
|
// UserId of the current user
|
||||||
|
userId: string;
|
||||||
|
// UserId of the entity that added / modified the widget
|
||||||
|
creatorUserId: string;
|
||||||
|
waitForIframeLoad: boolean;
|
||||||
|
showMenubar?: boolean;
|
||||||
|
// Optional onEditClickHandler (overrides default behaviour)
|
||||||
|
onEditClick?: () => void;
|
||||||
|
// Optional onDeleteClickHandler (overrides default behaviour)
|
||||||
|
onDeleteClick?: () => void;
|
||||||
|
// Optionally hide the tile title
|
||||||
|
showTitle?: boolean;
|
||||||
|
// Optionally handle minimise button pointer events (default false)
|
||||||
|
handleMinimisePointerEvents?: boolean;
|
||||||
|
// Optionally hide the popout widget icon
|
||||||
|
showPopout?: boolean;
|
||||||
|
// Is this an instance of a user widget
|
||||||
|
userWidget: boolean;
|
||||||
|
// sets the pointer-events property on the iframe
|
||||||
|
pointerEvents?: string;
|
||||||
|
widgetPageTitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
initialising: boolean; // True while we are mangling the widget URL
|
||||||
|
// True while the iframe content is loading
|
||||||
|
loading: boolean;
|
||||||
|
// Assume that widget has permission to load if we are the user who
|
||||||
|
// added it to the room, or if explicitly granted by the user
|
||||||
|
hasPermissionToLoad: boolean;
|
||||||
|
error: Error;
|
||||||
|
menuDisplayed: boolean;
|
||||||
|
widgetPageTitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.AppTile")
|
@replaceableComponent("views.elements.AppTile")
|
||||||
export default class AppTile extends React.Component {
|
export default class AppTile extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
public static defaultProps: Partial<IProps> = {
|
||||||
|
waitForIframeLoad: true,
|
||||||
|
showMenubar: true,
|
||||||
|
showTitle: true,
|
||||||
|
showPopout: true,
|
||||||
|
handleMinimisePointerEvents: false,
|
||||||
|
userWidget: false,
|
||||||
|
miniMode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
private contextMenuButton = createRef<any>();
|
||||||
|
private iframe: HTMLIFrameElement; // ref to the iframe (callback style)
|
||||||
|
private allowedWidgetsWatchRef: string;
|
||||||
|
private persistKey: string;
|
||||||
|
private sgWidget: StopGapWidget;
|
||||||
|
private dispatcherRef: string;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// The key used for PersistedElement
|
// The key used for PersistedElement
|
||||||
this._persistKey = getPersistKey(this.props.app.id);
|
this.persistKey = getPersistKey(this.props.app.id);
|
||||||
try {
|
try {
|
||||||
this._sgWidget = new StopGapWidget(this.props);
|
this.sgWidget = new StopGapWidget(this.props);
|
||||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
this.sgWidget.on("preparing", this.onWidgetPrepared);
|
||||||
this._sgWidget.on("ready", this._onWidgetReady);
|
this.sgWidget.on("ready", this.onWidgetReady);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Failed to construct widget", e);
|
console.log("Failed to construct widget", e);
|
||||||
this._sgWidget = null;
|
this.sgWidget = null;
|
||||||
}
|
}
|
||||||
this.iframe = null; // ref to the iframe (callback style)
|
|
||||||
|
|
||||||
this.state = this._getNewState(props);
|
this.state = this.getNewState(props);
|
||||||
this._contextMenuButton = createRef();
|
|
||||||
|
|
||||||
this._allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
|
this.allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a function to make the impact of calling SettingsStore slightly less
|
// This is a function to make the impact of calling SettingsStore slightly less
|
||||||
hasPermissionToLoad = (props) => {
|
private hasPermissionToLoad = (props: IProps): boolean => {
|
||||||
if (this._usingLocalWidget()) return true;
|
if (this.usingLocalWidget()) return true;
|
||||||
if (!props.room) return true; // user widgets always have permissions
|
if (!props.room) return true; // user widgets always have permissions
|
||||||
|
|
||||||
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
|
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
|
||||||
|
@ -81,34 +142,34 @@ export default class AppTile extends React.Component {
|
||||||
* @param {Object} newProps The new properties of the component
|
* @param {Object} newProps The new properties of the component
|
||||||
* @return {Object} Updated component state to be set with setState
|
* @return {Object} Updated component state to be set with setState
|
||||||
*/
|
*/
|
||||||
_getNewState(newProps) {
|
private getNewState(newProps: IProps): IState {
|
||||||
return {
|
return {
|
||||||
initialising: true, // True while we are mangling the widget URL
|
initialising: true, // True while we are mangling the widget URL
|
||||||
// True while the iframe content is loading
|
// True while the iframe content is loading
|
||||||
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this.persistKey),
|
||||||
// Assume that widget has permission to load if we are the user who
|
// Assume that widget has permission to load if we are the user who
|
||||||
// added it to the room, or if explicitly granted by the user
|
// added it to the room, or if explicitly granted by the user
|
||||||
hasPermissionToLoad: this.hasPermissionToLoad(newProps),
|
hasPermissionToLoad: this.hasPermissionToLoad(newProps),
|
||||||
error: null,
|
error: null,
|
||||||
widgetPageTitle: newProps.widgetPageTitle,
|
|
||||||
menuDisplayed: false,
|
menuDisplayed: false,
|
||||||
|
widgetPageTitle: this.props.widgetPageTitle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onAllowedWidgetsChange = () => {
|
private onAllowedWidgetsChange = (): void => {
|
||||||
const hasPermissionToLoad = this.hasPermissionToLoad(this.props);
|
const hasPermissionToLoad = this.hasPermissionToLoad(this.props);
|
||||||
|
|
||||||
if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
|
if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
|
||||||
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this.persistKey);
|
||||||
if (this._sgWidget) this._sgWidget.stop();
|
if (this.sgWidget) this.sgWidget.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ hasPermissionToLoad });
|
this.setState({ hasPermissionToLoad });
|
||||||
};
|
};
|
||||||
|
|
||||||
isMixedContent() {
|
private isMixedContent(): boolean {
|
||||||
const parentContentProtocol = window.location.protocol;
|
const parentContentProtocol = window.location.protocol;
|
||||||
const u = url.parse(this.props.app.url);
|
const u = url.parse(this.props.app.url);
|
||||||
const childContentProtocol = u.protocol;
|
const childContentProtocol = u.protocol;
|
||||||
|
@ -120,69 +181,70 @@ export default class AppTile extends React.Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
// Only fetch IM token on mount if we're showing and have permission to load
|
// Only fetch IM token on mount if we're showing and have permission to load
|
||||||
if (this._sgWidget && this.state.hasPermissionToLoad) {
|
if (this.sgWidget && this.state.hasPermissionToLoad) {
|
||||||
this._startWidget();
|
this.startWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget action listeners
|
// Widget action listeners
|
||||||
this.dispatcherRef = dis.register(this._onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
// Widget action listeners
|
// Widget action listeners
|
||||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||||
|
|
||||||
// if it's not remaining on screen, get rid of the PersistedElement container
|
// if it's not remaining on screen, get rid of the PersistedElement container
|
||||||
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
|
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this.persistKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._sgWidget) {
|
if (this.sgWidget) {
|
||||||
this._sgWidget.stop();
|
this.sgWidget.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsStore.unwatchSetting(this._allowedWidgetsWatchRef);
|
SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
_resetWidget(newProps) {
|
private resetWidget(newProps: IProps): void {
|
||||||
if (this._sgWidget) {
|
if (this.sgWidget) {
|
||||||
this._sgWidget.stop();
|
this.sgWidget.stop();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this._sgWidget = new StopGapWidget(newProps);
|
this.sgWidget = new StopGapWidget(newProps);
|
||||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
this.sgWidget.on("preparing", this.onWidgetPrepared);
|
||||||
this._sgWidget.on("ready", this._onWidgetReady);
|
this.sgWidget.on("ready", this.onWidgetReady);
|
||||||
this._startWidget();
|
this.startWidget();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Failed to construct widget", e);
|
console.log("Failed to construct widget", e);
|
||||||
this._sgWidget = null;
|
this.sgWidget = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_startWidget() {
|
private startWidget(): void {
|
||||||
this._sgWidget.prepare().then(() => {
|
this.sgWidget.prepare().then(() => {
|
||||||
this.setState({ initialising: false });
|
this.setState({ initialising: false });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_iframeRefChange = (ref) => {
|
private iframeRefChange = (ref: HTMLIFrameElement): void => {
|
||||||
this.iframe = ref;
|
this.iframe = ref;
|
||||||
if (ref) {
|
if (ref) {
|
||||||
if (this._sgWidget) this._sgWidget.start(ref);
|
if (this.sgWidget) this.sgWidget.start(ref);
|
||||||
} else {
|
} else {
|
||||||
this._resetWidget(this.props);
|
this.resetWidget(this.props);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
public UNSAFE_componentWillReceiveProps(nextProps: IProps): void { // eslint-disable-line camelcase
|
||||||
if (nextProps.app.url !== this.props.app.url) {
|
if (nextProps.app.url !== this.props.app.url) {
|
||||||
this._getNewState(nextProps);
|
this.getNewState(nextProps);
|
||||||
if (this.state.hasPermissionToLoad) {
|
if (this.state.hasPermissionToLoad) {
|
||||||
this._resetWidget(nextProps);
|
this.resetWidget(nextProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,7 +260,7 @@ export default class AppTile extends React.Component {
|
||||||
* @private
|
* @private
|
||||||
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
|
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
|
||||||
*/
|
*/
|
||||||
async _endWidgetActions() { // widget migration dev note: async to maintain signature
|
private async endWidgetActions(): Promise<void> { // widget migration dev note: async to maintain signature
|
||||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||||
// its hold on the webcam. Without this, the widget holds a media
|
// its hold on the webcam. Without this, the widget holds a media
|
||||||
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
|
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
|
||||||
|
@ -217,27 +279,27 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the widget from the persisted store for good measure.
|
// Delete the widget from the persisted store for good measure.
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this.persistKey);
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||||
|
|
||||||
if (this._sgWidget) this._sgWidget.stop({ forceDestroy: true });
|
if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWidgetPrepared = () => {
|
private onWidgetPrepared = (): void => {
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onWidgetReady = () => {
|
private onWidgetReady = (): void => {
|
||||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||||
this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
|
this.sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAction = payload => {
|
private onAction = (payload): void => {
|
||||||
if (payload.widgetId === this.props.app.id) {
|
if (payload.widgetId === this.props.app.id) {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'm.sticker':
|
case 'm.sticker':
|
||||||
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
if (this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||||
dis.dispatch({ action: 'post_sticker_message', data: payload.data });
|
dis.dispatch({ action: 'post_sticker_message', data: payload.data });
|
||||||
dis.dispatch({ action: 'stickerpicker_close' });
|
dis.dispatch({ action: 'stickerpicker_close' });
|
||||||
} else {
|
} else {
|
||||||
|
@ -248,7 +310,7 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_grantWidgetPermission = () => {
|
private grantWidgetPermission = (): void => {
|
||||||
const roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
||||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||||
|
@ -258,14 +320,14 @@ export default class AppTile extends React.Component {
|
||||||
this.setState({ hasPermissionToLoad: true });
|
this.setState({ hasPermissionToLoad: true });
|
||||||
|
|
||||||
// Fetch a token for the integration manager, now that we're allowed to
|
// Fetch a token for the integration manager, now that we're allowed to
|
||||||
this._startWidget();
|
this.startWidget();
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
// We don't really need to do anything about this - the user will just hit the button again.
|
// We don't really need to do anything about this - the user will just hit the button again.
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
formatAppTileName() {
|
private formatAppTileName(): string {
|
||||||
let appTileName = "No name";
|
let appTileName = "No name";
|
||||||
if (this.props.app.name && this.props.app.name.trim()) {
|
if (this.props.app.name && this.props.app.name.trim()) {
|
||||||
appTileName = this.props.app.name.trim();
|
appTileName = this.props.app.name.trim();
|
||||||
|
@ -278,11 +340,11 @@ export default class AppTile extends React.Component {
|
||||||
* actual widget URL
|
* actual widget URL
|
||||||
* @returns {bool} true If using a local version of the widget
|
* @returns {bool} true If using a local version of the widget
|
||||||
*/
|
*/
|
||||||
_usingLocalWidget() {
|
private usingLocalWidget(): boolean {
|
||||||
return WidgetType.JITSI.matches(this.props.app.type);
|
return WidgetType.JITSI.matches(this.props.app.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getTileTitle() {
|
private getTileTitle(): JSX.Element {
|
||||||
const name = this.formatAppTileName();
|
const name = this.formatAppTileName();
|
||||||
const titleSpacer = <span> - </span>;
|
const titleSpacer = <span> - </span>;
|
||||||
let title = '';
|
let title = '';
|
||||||
|
@ -300,32 +362,32 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO replace with full screen interactions
|
// TODO replace with full screen interactions
|
||||||
_onPopoutWidgetClick = () => {
|
private onPopoutWidgetClick = (): void => {
|
||||||
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
|
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
|
||||||
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
|
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
|
||||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||||
this._endWidgetActions().then(() => {
|
this.endWidgetActions().then(() => {
|
||||||
if (this.iframe) {
|
if (this.iframe) {
|
||||||
// Reload iframe
|
// Reload iframe
|
||||||
this.iframe.src = this._sgWidget.embedUrl;
|
this.iframe.src = this.sgWidget.embedUrl;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||||
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
||||||
Object.assign(document.createElement('a'),
|
Object.assign(document.createElement('a'),
|
||||||
{ target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
|
{ target: '_blank', href: this.sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onContextMenuClick = () => {
|
private onContextMenuClick = (): void => {
|
||||||
this.setState({ menuDisplayed: true });
|
this.setState({ menuDisplayed: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
_closeContextMenu = () => {
|
private closeContextMenu = (): void => {
|
||||||
this.setState({ menuDisplayed: false });
|
this.setState({ menuDisplayed: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
let appTileBody;
|
let appTileBody;
|
||||||
|
|
||||||
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
|
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
|
||||||
|
@ -351,7 +413,7 @@ export default class AppTile extends React.Component {
|
||||||
<Spinner message={_t("Loading...")} />
|
<Spinner message={_t("Loading...")} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (this._sgWidget === null) {
|
if (this.sgWidget === null) {
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
||||||
<AppWarning errorMsg={_t("Error loading Widget")} />
|
<AppWarning errorMsg={_t("Error loading Widget")} />
|
||||||
|
@ -365,9 +427,9 @@ export default class AppTile extends React.Component {
|
||||||
<AppPermission
|
<AppPermission
|
||||||
roomId={this.props.room.roomId}
|
roomId={this.props.room.roomId}
|
||||||
creatorUserId={this.props.creatorUserId}
|
creatorUserId={this.props.creatorUserId}
|
||||||
url={this._sgWidget.embedUrl}
|
url={this.sgWidget.embedUrl}
|
||||||
isRoomEncrypted={isEncrypted}
|
isRoomEncrypted={isEncrypted}
|
||||||
onPermissionGranted={this._grantWidgetPermission}
|
onPermissionGranted={this.grantWidgetPermission}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -390,8 +452,8 @@ export default class AppTile extends React.Component {
|
||||||
{ this.state.loading && loadingElement }
|
{ this.state.loading && loadingElement }
|
||||||
<iframe
|
<iframe
|
||||||
allow={iframeFeatures}
|
allow={iframeFeatures}
|
||||||
ref={this._iframeRefChange}
|
ref={this.iframeRefChange}
|
||||||
src={this._sgWidget.embedUrl}
|
src={this.sgWidget.embedUrl}
|
||||||
allowFullScreen={true}
|
allowFullScreen={true}
|
||||||
sandbox={sandboxFlags}
|
sandbox={sandboxFlags}
|
||||||
/>
|
/>
|
||||||
|
@ -407,7 +469,7 @@ export default class AppTile extends React.Component {
|
||||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||||
// AppTile's border is in the wrong place
|
// AppTile's border is in the wrong place
|
||||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||||
<PersistedElement persistKey={this._persistKey}>
|
<PersistedElement persistKey={this.persistKey}>
|
||||||
{ appTileBody }
|
{ appTileBody }
|
||||||
</PersistedElement>
|
</PersistedElement>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -429,9 +491,9 @@ export default class AppTile extends React.Component {
|
||||||
if (this.state.menuDisplayed) {
|
if (this.state.menuDisplayed) {
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<RoomWidgetContextMenu
|
<RoomWidgetContextMenu
|
||||||
{...aboveLeftOf(this._contextMenuButton.current.getBoundingClientRect(), null)}
|
{...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect(), null)}
|
||||||
app={this.props.app}
|
app={this.props.app}
|
||||||
onFinished={this._closeContextMenu}
|
onFinished={this.closeContextMenu}
|
||||||
showUnpin={!this.props.userWidget}
|
showUnpin={!this.props.userWidget}
|
||||||
userWidget={this.props.userWidget}
|
userWidget={this.props.userWidget}
|
||||||
onEditClick={this.props.onEditClick}
|
onEditClick={this.props.onEditClick}
|
||||||
|
@ -444,21 +506,21 @@ export default class AppTile extends React.Component {
|
||||||
<div className={appTileClasses} id={this.props.app.id}>
|
<div className={appTileClasses} id={this.props.app.id}>
|
||||||
{ this.props.showMenubar &&
|
{ this.props.showMenubar &&
|
||||||
<div className="mx_AppTileMenuBar">
|
<div className="mx_AppTileMenuBar">
|
||||||
<span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false) }}>
|
<span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : "none") }}>
|
||||||
{ this.props.showTitle && this._getTileTitle() }
|
{ this.props.showTitle && this.getTileTitle() }
|
||||||
</span>
|
</span>
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
{ this.props.showPopout && <AccessibleButton
|
{ this.props.showPopout && <AccessibleButton
|
||||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
||||||
title={_t('Popout widget')}
|
title={_t('Popout widget')}
|
||||||
onClick={this._onPopoutWidgetClick}
|
onClick={this.onPopoutWidgetClick}
|
||||||
/> }
|
/> }
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
||||||
label={_t("Options")}
|
label={_t("Options")}
|
||||||
isExpanded={this.state.menuDisplayed}
|
isExpanded={this.state.menuDisplayed}
|
||||||
inputRef={this._contextMenuButton}
|
inputRef={this.contextMenuButton}
|
||||||
onClick={this._onContextMenuClick}
|
onClick={this.onContextMenuClick}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div> }
|
</div> }
|
||||||
|
@ -469,49 +531,3 @@ export default class AppTile extends React.Component {
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppTile.displayName = 'AppTile';
|
|
||||||
|
|
||||||
AppTile.propTypes = {
|
|
||||||
app: PropTypes.object.isRequired,
|
|
||||||
// If room is not specified then it is an account level widget
|
|
||||||
// which bypasses permission prompts as it was added explicitly by that user
|
|
||||||
room: PropTypes.object,
|
|
||||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
|
||||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
|
||||||
fullWidth: PropTypes.bool,
|
|
||||||
// Optional. If set, renders a smaller view of the widget
|
|
||||||
miniMode: PropTypes.bool,
|
|
||||||
// UserId of the current user
|
|
||||||
userId: PropTypes.string.isRequired,
|
|
||||||
// UserId of the entity that added / modified the widget
|
|
||||||
creatorUserId: PropTypes.string,
|
|
||||||
waitForIframeLoad: PropTypes.bool,
|
|
||||||
showMenubar: PropTypes.bool,
|
|
||||||
// Optional onEditClickHandler (overrides default behaviour)
|
|
||||||
onEditClick: PropTypes.func,
|
|
||||||
// Optional onDeleteClickHandler (overrides default behaviour)
|
|
||||||
onDeleteClick: PropTypes.func,
|
|
||||||
// Optional onMinimiseClickHandler
|
|
||||||
onMinimiseClick: PropTypes.func,
|
|
||||||
// Optionally hide the tile title
|
|
||||||
showTitle: PropTypes.bool,
|
|
||||||
// Optionally handle minimise button pointer events (default false)
|
|
||||||
handleMinimisePointerEvents: PropTypes.bool,
|
|
||||||
// Optionally hide the popout widget icon
|
|
||||||
showPopout: PropTypes.bool,
|
|
||||||
// Is this an instance of a user widget
|
|
||||||
userWidget: PropTypes.bool,
|
|
||||||
// sets the pointer-events property on the iframe
|
|
||||||
pointerEvents: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
AppTile.defaultProps = {
|
|
||||||
waitForIframeLoad: true,
|
|
||||||
showMenubar: true,
|
|
||||||
showTitle: true,
|
|
||||||
showPopout: true,
|
|
||||||
handleMinimisePointerEvents: false,
|
|
||||||
userWidget: false,
|
|
||||||
miniMode: false,
|
|
||||||
};
|
|
|
@ -1,24 +1,20 @@
|
||||||
import React from 'react'; // eslint-disable-line no-unused-vars
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const AppWarning = (props) => {
|
interface IProps {
|
||||||
|
errorMsg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppWarning: React.FC<IProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className='mx_AppPermissionWarning'>
|
<div className='mx_AppPermissionWarning'>
|
||||||
<div className='mx_AppPermissionWarningImage'>
|
<div className='mx_AppPermissionWarningImage'>
|
||||||
<img src={require("../../../../res/img/warning.svg")} alt='' />
|
<img src={require("../../../../res/img/warning.svg")} alt='' />
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_AppPermissionWarningText'>
|
<div className='mx_AppPermissionWarningText'>
|
||||||
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span>
|
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg || "Error" }</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
AppWarning.propTypes = {
|
|
||||||
errorMsg: PropTypes.string,
|
|
||||||
};
|
|
||||||
AppWarning.defaultProps = {
|
|
||||||
errorMsg: 'Error',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppWarning;
|
export default AppWarning;
|
|
@ -17,60 +17,61 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// The primary button which is styled differently and has default focus.
|
||||||
|
primaryButton: React.ReactNode;
|
||||||
|
|
||||||
|
// A node to insert into the cancel button instead of default "Cancel"
|
||||||
|
cancelButton?: React.ReactNode;
|
||||||
|
|
||||||
|
// If true, make the primary button a form submit button (input type="submit")
|
||||||
|
primaryIsSubmit?: boolean;
|
||||||
|
|
||||||
|
// onClick handler for the primary button.
|
||||||
|
onPrimaryButtonClick?: (ev: React.MouseEvent) => void;
|
||||||
|
|
||||||
|
// should there be a cancel button? default: true
|
||||||
|
hasCancel?: boolean;
|
||||||
|
|
||||||
|
// The class of the cancel button, only used if a cancel button is
|
||||||
|
// enabled
|
||||||
|
cancelButtonClass?: string;
|
||||||
|
|
||||||
|
// onClick handler for the cancel button.
|
||||||
|
onCancel?: (...args: any[]) => void;
|
||||||
|
|
||||||
|
focus?: boolean;
|
||||||
|
|
||||||
|
// disables the primary and cancel buttons
|
||||||
|
disabled?: boolean;
|
||||||
|
|
||||||
|
// disables only the primary button
|
||||||
|
primaryDisabled?: boolean;
|
||||||
|
|
||||||
|
// something to stick next to the buttons, optionally
|
||||||
|
additive?: React.ReactNode;
|
||||||
|
|
||||||
|
primaryButtonClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic container for buttons in modal dialogs.
|
* Basic container for buttons in modal dialogs.
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.elements.DialogButtons")
|
@replaceableComponent("views.elements.DialogButtons")
|
||||||
export default class DialogButtons extends React.Component {
|
export default class DialogButtons extends React.Component<IProps> {
|
||||||
static propTypes = {
|
public static defaultProps: Partial<IProps> = {
|
||||||
// The primary button which is styled differently and has default focus.
|
|
||||||
primaryButton: PropTypes.node.isRequired,
|
|
||||||
|
|
||||||
// A node to insert into the cancel button instead of default "Cancel"
|
|
||||||
cancelButton: PropTypes.node,
|
|
||||||
|
|
||||||
// If true, make the primary button a form submit button (input type="submit")
|
|
||||||
primaryIsSubmit: PropTypes.bool,
|
|
||||||
|
|
||||||
// onClick handler for the primary button.
|
|
||||||
onPrimaryButtonClick: PropTypes.func,
|
|
||||||
|
|
||||||
// should there be a cancel button? default: true
|
|
||||||
hasCancel: PropTypes.bool,
|
|
||||||
|
|
||||||
// The class of the cancel button, only used if a cancel button is
|
|
||||||
// enabled
|
|
||||||
cancelButtonClass: PropTypes.node,
|
|
||||||
|
|
||||||
// onClick handler for the cancel button.
|
|
||||||
onCancel: PropTypes.func,
|
|
||||||
|
|
||||||
focus: PropTypes.bool,
|
|
||||||
|
|
||||||
// disables the primary and cancel buttons
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
|
|
||||||
// disables only the primary button
|
|
||||||
primaryDisabled: PropTypes.bool,
|
|
||||||
|
|
||||||
// something to stick next to the buttons, optionally
|
|
||||||
additive: PropTypes.element,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
hasCancel: true,
|
hasCancel: true,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCancelClick = () => {
|
private onCancelClick = (event: React.MouseEvent): void => {
|
||||||
this.props.onCancel();
|
this.props.onCancel(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
let primaryButtonClassName = "mx_Dialog_primary";
|
let primaryButtonClassName = "mx_Dialog_primary";
|
||||||
if (this.props.primaryButtonClass) {
|
if (this.props.primaryButtonClass) {
|
||||||
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
||||||
|
@ -82,7 +83,7 @@ export default class DialogButtons extends React.Component {
|
||||||
// important: the default type is 'submit' and this button comes before the
|
// important: the default type is 'submit' and this button comes before the
|
||||||
// primary in the DOM so will get form submissions unless we make it not a submit.
|
// primary in the DOM so will get form submissions unless we make it not a submit.
|
||||||
type="button"
|
type="button"
|
||||||
onClick={this._onCancelClick}
|
onClick={this.onCancelClick}
|
||||||
className={this.props.cancelButtonClass}
|
className={this.props.cancelButtonClass}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
>
|
>
|
|
@ -14,71 +14,73 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ChangeEvent, createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import AccessibleButton from "./AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
onClear?: () => void;
|
||||||
|
onJoinClick?: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
showJoinButton?: boolean;
|
||||||
|
initialText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.DirectorySearchBox")
|
@replaceableComponent("views.elements.DirectorySearchBox")
|
||||||
export default class DirectorySearchBox extends React.Component {
|
export default class DirectorySearchBox extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
private input = createRef<HTMLInputElement>();
|
||||||
super(props);
|
|
||||||
this._collectInput = this._collectInput.bind(this);
|
|
||||||
this._onClearClick = this._onClearClick.bind(this);
|
|
||||||
this._onChange = this._onChange.bind(this);
|
|
||||||
this._onKeyUp = this._onKeyUp.bind(this);
|
|
||||||
this._onJoinButtonClick = this._onJoinButtonClick.bind(this);
|
|
||||||
|
|
||||||
this.input = null;
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
value: this.props.initialText || '',
|
value: this.props.initialText || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_collectInput(e) {
|
private onClearClick = (): void => {
|
||||||
this.input = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onClearClick() {
|
|
||||||
this.setState({ value: '' });
|
this.setState({ value: '' });
|
||||||
|
|
||||||
if (this.input) {
|
if (this.input.current) {
|
||||||
this.input.focus();
|
this.input.current.focus();
|
||||||
|
|
||||||
if (this.props.onClear) {
|
if (this.props.onClear) {
|
||||||
this.props.onClear();
|
this.props.onClear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_onChange(ev) {
|
private onChange = (ev: ChangeEvent<HTMLInputElement>): void => {
|
||||||
if (!this.input) return;
|
if (!this.input.current) return;
|
||||||
this.setState({ value: ev.target.value });
|
this.setState({ value: ev.target.value });
|
||||||
|
|
||||||
if (this.props.onChange) {
|
if (this.props.onChange) {
|
||||||
this.props.onChange(ev.target.value);
|
this.props.onChange(ev.target.value);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_onKeyUp(ev) {
|
private onKeyUp = (ev: React.KeyboardEvent): void => {
|
||||||
if (ev.key == 'Enter' && this.props.showJoinButton) {
|
if (ev.key == 'Enter' && this.props.showJoinButton) {
|
||||||
if (this.props.onJoinClick) {
|
if (this.props.onJoinClick) {
|
||||||
this.props.onJoinClick(this.state.value);
|
this.props.onJoinClick(this.state.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_onJoinButtonClick() {
|
private onJoinButtonClick = (): void => {
|
||||||
if (this.props.onJoinClick) {
|
if (this.props.onJoinClick) {
|
||||||
this.props.onJoinClick(this.state.value);
|
this.props.onJoinClick(this.state.value);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
const searchboxClasses = {
|
const searchboxClasses = {
|
||||||
mx_DirectorySearchBox: true,
|
mx_DirectorySearchBox: true,
|
||||||
};
|
};
|
||||||
|
@ -87,7 +89,7 @@ export default class DirectorySearchBox extends React.Component {
|
||||||
let joinButton;
|
let joinButton;
|
||||||
if (this.props.showJoinButton) {
|
if (this.props.showJoinButton) {
|
||||||
joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton"
|
joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton"
|
||||||
onClick={this._onJoinButtonClick}
|
onClick={this.onJoinButtonClick}
|
||||||
>{ _t("Join") }</AccessibleButton>;
|
>{ _t("Join") }</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,24 +99,15 @@ export default class DirectorySearchBox extends React.Component {
|
||||||
name="dirsearch"
|
name="dirsearch"
|
||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
ref={this._collectInput}
|
ref={this.input}
|
||||||
onChange={this._onChange}
|
onChange={this.onChange}
|
||||||
onKeyUp={this._onKeyUp}
|
onKeyUp={this.onKeyUp}
|
||||||
placeholder={this.props.placeholder}
|
placeholder={this.props.placeholder}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
{ joinButton }
|
{ joinButton }
|
||||||
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} />
|
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this.onClearClick} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DirectorySearchBox.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
onClear: PropTypes.func,
|
|
||||||
onJoinClick: PropTypes.func,
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
showJoinButton: PropTypes.bool,
|
|
||||||
initialText: PropTypes.string,
|
|
||||||
};
|
|
|
@ -16,33 +16,42 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.elements.EditableText")
|
enum Phases {
|
||||||
export default class EditableText extends React.Component {
|
Display = "display",
|
||||||
static propTypes = {
|
Edit = "edit",
|
||||||
onValueChanged: PropTypes.func,
|
}
|
||||||
initialValue: PropTypes.string,
|
|
||||||
label: PropTypes.string,
|
interface IProps {
|
||||||
placeholder: PropTypes.string,
|
onValueChanged?: (value: string, shouldSubmit: boolean) => void;
|
||||||
className: PropTypes.string,
|
initialValue?: string;
|
||||||
labelClassName: PropTypes.string,
|
label?: string;
|
||||||
placeholderClassName: PropTypes.string,
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
labelClassName?: string;
|
||||||
|
placeholderClassName?: string;
|
||||||
// Overrides blurToSubmit if true
|
// Overrides blurToSubmit if true
|
||||||
blurToCancel: PropTypes.bool,
|
blurToCancel?: boolean;
|
||||||
// Will cause onValueChanged(value, true) to fire on blur
|
// Will cause onValueChanged(value, true) to fire on blur
|
||||||
blurToSubmit: PropTypes.bool,
|
blurToSubmit?: boolean;
|
||||||
editable: PropTypes.bool,
|
editable?: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
static Phases = {
|
interface IState {
|
||||||
Display: "display",
|
phase: Phases;
|
||||||
Edit: "edit",
|
}
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
@replaceableComponent("views.elements.EditableText")
|
||||||
|
export default class EditableText extends React.Component<IProps, IState> {
|
||||||
|
// we track value as an JS object field rather than in React state
|
||||||
|
// as React doesn't play nice with contentEditable.
|
||||||
|
public value = '';
|
||||||
|
private placeholder = false;
|
||||||
|
private editableDiv = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
public static defaultProps: Partial<IProps> = {
|
||||||
onValueChanged() {},
|
onValueChanged() {},
|
||||||
initialValue: '',
|
initialValue: '',
|
||||||
label: '',
|
label: '',
|
||||||
|
@ -53,81 +62,61 @@ export default class EditableText extends React.Component {
|
||||||
blurToSubmit: false,
|
blurToSubmit: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// we track value as an JS object field rather than in React state
|
this.state = {
|
||||||
// as React doesn't play nice with contentEditable.
|
phase: Phases.Display,
|
||||||
this.value = '';
|
|
||||||
this.placeholder = false;
|
|
||||||
|
|
||||||
this._editable_div = createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
phase: EditableText.Phases.Display,
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
public UNSAFE_componentWillReceiveProps(nextProps: IProps): void {
|
||||||
if (nextProps.initialValue !== this.props.initialValue) {
|
if (nextProps.initialValue !== this.props.initialValue) {
|
||||||
this.value = nextProps.initialValue;
|
this.value = nextProps.initialValue;
|
||||||
if (this._editable_div.current) {
|
if (this.editableDiv.current) {
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.value = this.props.initialValue;
|
this.value = this.props.initialValue;
|
||||||
if (this._editable_div.current) {
|
if (this.editableDiv.current) {
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showPlaceholder = show => {
|
private showPlaceholder = (show: boolean): void => {
|
||||||
if (show) {
|
if (show) {
|
||||||
this._editable_div.current.textContent = this.props.placeholder;
|
this.editableDiv.current.textContent = this.props.placeholder;
|
||||||
this._editable_div.current.setAttribute("class", this.props.className
|
this.editableDiv.current.setAttribute("class", this.props.className
|
||||||
+ " " + this.props.placeholderClassName);
|
+ " " + this.props.placeholderClassName);
|
||||||
this.placeholder = true;
|
this.placeholder = true;
|
||||||
this.value = '';
|
this.value = '';
|
||||||
} else {
|
} else {
|
||||||
this._editable_div.current.textContent = this.value;
|
this.editableDiv.current.textContent = this.value;
|
||||||
this._editable_div.current.setAttribute("class", this.props.className);
|
this.editableDiv.current.setAttribute("class", this.props.className);
|
||||||
this.placeholder = false;
|
this.placeholder = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getValue = () => this.value;
|
private cancelEdit = (): void => {
|
||||||
|
|
||||||
setValue = value => {
|
|
||||||
this.value = value;
|
|
||||||
this.showPlaceholder(!this.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
edit = () => {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: EditableText.Phases.Edit,
|
phase: Phases.Display,
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
cancelEdit = () => {
|
|
||||||
this.setState({
|
|
||||||
phase: EditableText.Phases.Display,
|
|
||||||
});
|
});
|
||||||
this.value = this.props.initialValue;
|
this.value = this.props.initialValue;
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
this.onValueChanged(false);
|
this.onValueChanged(false);
|
||||||
this._editable_div.current.blur();
|
this.editableDiv.current.blur();
|
||||||
};
|
};
|
||||||
|
|
||||||
onValueChanged = shouldSubmit => {
|
private onValueChanged = (shouldSubmit: boolean): void => {
|
||||||
this.props.onValueChanged(this.value, shouldSubmit);
|
this.props.onValueChanged(this.value, shouldSubmit);
|
||||||
};
|
};
|
||||||
|
|
||||||
onKeyDown = ev => {
|
private onKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||||
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||||
|
|
||||||
if (this.placeholder) {
|
if (this.placeholder) {
|
||||||
|
@ -142,13 +131,13 @@ export default class EditableText extends React.Component {
|
||||||
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||||
};
|
};
|
||||||
|
|
||||||
onKeyUp = ev => {
|
private onKeyUp = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||||
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||||
|
|
||||||
if (!ev.target.textContent) {
|
if (!(ev.target as HTMLDivElement).textContent) {
|
||||||
this.showPlaceholder(true);
|
this.showPlaceholder(true);
|
||||||
} else if (!this.placeholder) {
|
} else if (!this.placeholder) {
|
||||||
this.value = ev.target.textContent;
|
this.value = (ev.target as HTMLDivElement).textContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ev.key === Key.ENTER) {
|
if (ev.key === Key.ENTER) {
|
||||||
|
@ -160,22 +149,22 @@ export default class EditableText extends React.Component {
|
||||||
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||||
};
|
};
|
||||||
|
|
||||||
onClickDiv = ev => {
|
private onClickDiv = (): void => {
|
||||||
if (!this.props.editable) return;
|
if (!this.props.editable) return;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: EditableText.Phases.Edit,
|
phase: Phases.Edit,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onFocus = ev => {
|
private onFocus = (ev: React.FocusEvent<HTMLDivElement>): void => {
|
||||||
//ev.target.setSelectionRange(0, ev.target.textContent.length);
|
//ev.target.setSelectionRange(0, ev.target.textContent.length);
|
||||||
|
|
||||||
const node = ev.target.childNodes[0];
|
const node = ev.target.childNodes[0];
|
||||||
if (node) {
|
if (node) {
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.setStart(node, 0);
|
range.setStart(node, 0);
|
||||||
range.setEnd(node, node.length);
|
range.setEnd(node, ev.target.childNodes.length);
|
||||||
|
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
|
@ -183,11 +172,15 @@ export default class EditableText extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onFinish = (ev, shouldSubmit) => {
|
private onFinish = (
|
||||||
|
ev: React.KeyboardEvent<HTMLDivElement> | React.FocusEvent<HTMLDivElement>,
|
||||||
|
shouldSubmit?: boolean,
|
||||||
|
): void => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
const self = this;
|
const self = this;
|
||||||
const submit = (ev.key === Key.ENTER) || shouldSubmit;
|
const submit = ("key" in ev && ev.key === Key.ENTER) || shouldSubmit;
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: EditableText.Phases.Display,
|
phase: Phases.Display,
|
||||||
}, () => {
|
}, () => {
|
||||||
if (this.value !== this.props.initialValue) {
|
if (this.value !== this.props.initialValue) {
|
||||||
self.onValueChanged(submit);
|
self.onValueChanged(submit);
|
||||||
|
@ -195,7 +188,7 @@ export default class EditableText extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onBlur = ev => {
|
private onBlur = (ev: React.FocusEvent<HTMLDivElement>): void => {
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
|
|
||||||
|
@ -208,11 +201,11 @@ export default class EditableText extends React.Component {
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const { className, editable, initialValue, label, labelClassName } = this.props;
|
const { className, editable, initialValue, label, labelClassName } = this.props;
|
||||||
let editableEl;
|
let editableEl;
|
||||||
|
|
||||||
if (!editable || (this.state.phase === EditableText.Phases.Display &&
|
if (!editable || (this.state.phase === Phases.Display &&
|
||||||
(label || labelClassName) && !this.value)
|
(label || labelClassName) && !this.value)
|
||||||
) {
|
) {
|
||||||
// show the label
|
// show the label
|
||||||
|
@ -222,7 +215,7 @@ export default class EditableText extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||||
editableEl = <div
|
editableEl = <div
|
||||||
ref={this._editable_div}
|
ref={this.editableDiv}
|
||||||
contentEditable={true}
|
contentEditable={true}
|
||||||
className={className}
|
className={className}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
|
@ -15,9 +15,34 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Spinner from "./Spinner";
|
||||||
|
import EditableText from "./EditableText";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
/* callback to retrieve the initial value. */
|
||||||
|
getInitialValue?: () => Promise<string>;
|
||||||
|
|
||||||
|
/* initial value; used if getInitialValue is not given */
|
||||||
|
initialValue?: string;
|
||||||
|
|
||||||
|
/* placeholder text to use when the value is empty (and not being
|
||||||
|
* edited) */
|
||||||
|
placeholder?: string;
|
||||||
|
|
||||||
|
/* callback to update the value. Called with a single argument: the new
|
||||||
|
* value. */
|
||||||
|
onSubmit?: (value: string) => Promise<{} | void>;
|
||||||
|
|
||||||
|
/* should the input submit when focus is lost? */
|
||||||
|
blurToSubmit?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
busy: boolean;
|
||||||
|
errorString: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component which wraps an EditableText, with a spinner while updates take
|
* A component which wraps an EditableText, with a spinner while updates take
|
||||||
|
@ -31,50 +56,51 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
* taken from the 'initialValue' property.
|
* taken from the 'initialValue' property.
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.elements.EditableTextContainer")
|
@replaceableComponent("views.elements.EditableTextContainer")
|
||||||
export default class EditableTextContainer extends React.Component {
|
export default class EditableTextContainer extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
private unmounted = false;
|
||||||
|
public static defaultProps: Partial<IProps> = {
|
||||||
|
initialValue: "",
|
||||||
|
placeholder: "",
|
||||||
|
blurToSubmit: false,
|
||||||
|
onSubmit: () => { return Promise.resolve(); },
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._unmounted = false;
|
|
||||||
this.state = {
|
this.state = {
|
||||||
busy: false,
|
busy: false,
|
||||||
errorString: null,
|
errorString: null,
|
||||||
value: props.initialValue,
|
value: props.initialValue,
|
||||||
};
|
};
|
||||||
this._onValueChanged = this._onValueChanged.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public async componentDidMount(): Promise<void> {
|
||||||
if (this.props.getInitialValue === undefined) {
|
|
||||||
// use whatever was given in the initialValue property.
|
// use whatever was given in the initialValue property.
|
||||||
return;
|
if (this.props.getInitialValue === undefined) return;
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ busy: true });
|
this.setState({ busy: true });
|
||||||
|
try {
|
||||||
this.props.getInitialValue().then(
|
const initialValue = await this.props.getInitialValue();
|
||||||
(result) => {
|
if (this.unmounted) return;
|
||||||
if (this._unmounted) { return; }
|
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
value: result,
|
value: initialValue,
|
||||||
});
|
});
|
||||||
},
|
} catch (error) {
|
||||||
(error) => {
|
if (this.unmounted) return;
|
||||||
if (this._unmounted) { return; }
|
|
||||||
this.setState({
|
this.setState({
|
||||||
errorString: error.toString(),
|
errorString: error.toString(),
|
||||||
busy: false,
|
busy: false,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onValueChanged(value, shouldSubmit) {
|
private onValueChanged = (value: string, shouldSubmit: boolean): void => {
|
||||||
if (!shouldSubmit) {
|
if (!shouldSubmit) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -86,38 +112,36 @@ export default class EditableTextContainer extends React.Component {
|
||||||
|
|
||||||
this.props.onSubmit(value).then(
|
this.props.onSubmit(value).then(
|
||||||
() => {
|
() => {
|
||||||
if (this._unmounted) { return; }
|
if (this.unmounted) { return; }
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
value: value,
|
value: value,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
if (this._unmounted) { return; }
|
if (this.unmounted) { return; }
|
||||||
this.setState({
|
this.setState({
|
||||||
errorString: error.toString(),
|
errorString: error.toString(),
|
||||||
busy: false,
|
busy: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.busy) {
|
if (this.state.busy) {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
return (
|
return (
|
||||||
<Loader />
|
<Spinner />
|
||||||
);
|
);
|
||||||
} else if (this.state.errorString) {
|
} else if (this.state.errorString) {
|
||||||
return (
|
return (
|
||||||
<div className="error">{ this.state.errorString }</div>
|
<div className="error">{ this.state.errorString }</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const EditableText = sdk.getComponent('elements.EditableText');
|
|
||||||
return (
|
return (
|
||||||
<EditableText initialValue={this.state.value}
|
<EditableText initialValue={this.state.value}
|
||||||
placeholder={this.props.placeholder}
|
placeholder={this.props.placeholder}
|
||||||
onValueChanged={this._onValueChanged}
|
onValueChanged={this.onValueChanged}
|
||||||
blurToSubmit={this.props.blurToSubmit}
|
blurToSubmit={this.props.blurToSubmit}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -125,28 +149,3 @@ export default class EditableTextContainer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EditableTextContainer.propTypes = {
|
|
||||||
/* callback to retrieve the initial value. */
|
|
||||||
getInitialValue: PropTypes.func,
|
|
||||||
|
|
||||||
/* initial value; used if getInitialValue is not given */
|
|
||||||
initialValue: PropTypes.string,
|
|
||||||
|
|
||||||
/* placeholder text to use when the value is empty (and not being
|
|
||||||
* edited) */
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
|
|
||||||
/* callback to update the value. Called with a single argument: the new
|
|
||||||
* value. */
|
|
||||||
onSubmit: PropTypes.func,
|
|
||||||
|
|
||||||
/* should the input submit when focus is lost? */
|
|
||||||
blurToSubmit: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
EditableTextContainer.defaultProps = {
|
|
||||||
initialValue: "",
|
|
||||||
placeholder: "",
|
|
||||||
blurToSubmit: false,
|
|
||||||
onSubmit: function(v) {return Promise.resolve(); },
|
|
||||||
};
|
|
|
@ -16,13 +16,13 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import * as languageHandler from '../../../languageHandler';
|
import * as languageHandler from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Spinner from "./Spinner";
|
||||||
|
import Dropdown from "./Dropdown";
|
||||||
|
|
||||||
function languageMatchesSearchQuery(query, language) {
|
function languageMatchesSearchQuery(query, language) {
|
||||||
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
|
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
|
||||||
|
@ -30,11 +30,22 @@ function languageMatchesSearchQuery(query, language) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
onOptionChange: (language: string) => void;
|
||||||
|
value?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
searchQuery: string;
|
||||||
|
langs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.LanguageDropdown")
|
@replaceableComponent("views.elements.LanguageDropdown")
|
||||||
export default class LanguageDropdown extends React.Component {
|
export default class LanguageDropdown extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this._onSearchChange = this._onSearchChange.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
|
@ -42,7 +53,7 @@ export default class LanguageDropdown extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
languageHandler.getAllLanguagesFromJson().then((langs) => {
|
languageHandler.getAllLanguagesFromJson().then((langs) => {
|
||||||
langs.sort(function(a, b) {
|
langs.sort(function(a, b) {
|
||||||
if (a.label < b.label) return -1;
|
if (a.label < b.label) return -1;
|
||||||
|
@ -63,20 +74,17 @@ export default class LanguageDropdown extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSearchChange(search) {
|
private onSearchChange = (search: string): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
searchQuery: search,
|
searchQuery: search,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.langs === null) {
|
if (this.state.langs === null) {
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
|
||||||
|
|
||||||
let displayedLanguages;
|
let displayedLanguages;
|
||||||
if (this.state.searchQuery) {
|
if (this.state.searchQuery) {
|
||||||
displayedLanguages = this.state.langs.filter((lang) => {
|
displayedLanguages = this.state.langs.filter((lang) => {
|
||||||
|
@ -107,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
|
||||||
id="mx_LanguageDropdown"
|
id="mx_LanguageDropdown"
|
||||||
className={this.props.className}
|
className={this.props.className}
|
||||||
onOptionChange={this.props.onOptionChange}
|
onOptionChange={this.props.onOptionChange}
|
||||||
onSearchChange={this._onSearchChange}
|
onSearchChange={this.onSearchChange}
|
||||||
searchEnabled={true}
|
searchEnabled={true}
|
||||||
value={value}
|
value={value}
|
||||||
label={_t("Language Dropdown")}
|
label={_t("Language Dropdown")}
|
||||||
|
@ -118,8 +126,3 @@ export default class LanguageDropdown extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LanguageDropdown.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
onOptionChange: PropTypes.func.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
};
|
|
|
@ -15,17 +15,16 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
class ItemRange {
|
class ItemRange {
|
||||||
constructor(topCount, renderCount, bottomCount) {
|
constructor(
|
||||||
this.topCount = topCount;
|
public topCount: number,
|
||||||
this.renderCount = renderCount;
|
public renderCount: number,
|
||||||
this.bottomCount = bottomCount;
|
public bottomCount: number,
|
||||||
}
|
) { }
|
||||||
|
|
||||||
contains(range) {
|
public contains(range: ItemRange): boolean {
|
||||||
// don't contain empty ranges
|
// don't contain empty ranges
|
||||||
// as it will prevent clearing the list
|
// as it will prevent clearing the list
|
||||||
// once it is scrolled far enough out of view
|
// once it is scrolled far enough out of view
|
||||||
|
@ -36,7 +35,7 @@ class ItemRange {
|
||||||
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
|
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
expand(amount) {
|
public expand(amount: number): ItemRange {
|
||||||
// don't expand ranges that won't render anything
|
// don't expand ranges that won't render anything
|
||||||
if (this.renderCount === 0) {
|
if (this.renderCount === 0) {
|
||||||
return this;
|
return this;
|
||||||
|
@ -51,20 +50,55 @@ class ItemRange {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
totalSize() {
|
public totalSize(): number {
|
||||||
return this.topCount + this.renderCount + this.bottomCount;
|
return this.topCount + this.renderCount + this.bottomCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.LazyRenderList")
|
interface IProps<T> {
|
||||||
export default class LazyRenderList extends React.Component {
|
// height in pixels of the component returned by `renderItem`
|
||||||
constructor(props) {
|
itemHeight: number;
|
||||||
super(props);
|
// function to turn an element of `items` into a react component
|
||||||
|
renderItem: (item: T) => JSX.Element;
|
||||||
|
// scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
|
||||||
|
scrollTop: number;
|
||||||
|
// the height of the viewport this content is scrolled in
|
||||||
|
height: number;
|
||||||
|
// all items for the list. These should not be react components, see `renderItem`.
|
||||||
|
items?: T[];
|
||||||
|
// the amount of items to scroll before causing a rerender,
|
||||||
|
// should typically be less than `overflowItems` unless applying
|
||||||
|
// margins in the parent component when using multiple LazyRenderList in one viewport.
|
||||||
|
// use 0 to only rerender when items will come into view.
|
||||||
|
overflowMargin?: number;
|
||||||
|
// the amount of items to add at the top and bottom to render,
|
||||||
|
// so not every scroll of causes a rerender.
|
||||||
|
overflowItems?: number;
|
||||||
|
|
||||||
this.state = {};
|
element?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
interface IState {
|
||||||
|
renderRange: ItemRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.elements.LazyRenderList")
|
||||||
|
export default class LazyRenderList<T = any> extends React.Component<IProps<T>, IState> {
|
||||||
|
public static defaultProps: Partial<IProps<unknown>> = {
|
||||||
|
overflowItems: 20,
|
||||||
|
overflowMargin: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: IProps<T>) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
renderRange: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getDerivedStateFromProps(props: IProps<unknown>, state: IState): Partial<IState> {
|
||||||
const range = LazyRenderList.getVisibleRangeFromProps(props);
|
const range = LazyRenderList.getVisibleRangeFromProps(props);
|
||||||
const intersectRange = range.expand(props.overflowMargin);
|
const intersectRange = range.expand(props.overflowMargin);
|
||||||
const renderRange = range.expand(props.overflowItems);
|
const renderRange = range.expand(props.overflowItems);
|
||||||
|
@ -77,7 +111,7 @@ export default class LazyRenderList extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getVisibleRangeFromProps(props) {
|
private static getVisibleRangeFromProps(props: IProps<unknown>): ItemRange {
|
||||||
const { items, itemHeight, scrollTop, height } = props;
|
const { items, itemHeight, scrollTop, height } = props;
|
||||||
const length = items ? items.length : 0;
|
const length = items ? items.length : 0;
|
||||||
const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length);
|
const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length);
|
||||||
|
@ -88,7 +122,7 @@ export default class LazyRenderList extends React.Component {
|
||||||
return new ItemRange(topCount, renderCount, bottomCount);
|
return new ItemRange(topCount, renderCount, bottomCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const { itemHeight, items, renderItem } = this.props;
|
const { itemHeight, items, renderItem } = this.props;
|
||||||
const { renderRange } = this.state;
|
const { renderRange } = this.state;
|
||||||
const { topCount, renderCount, bottomCount } = renderRange;
|
const { topCount, renderCount, bottomCount } = renderRange;
|
||||||
|
@ -109,28 +143,3 @@ export default class LazyRenderList extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyRenderList.defaultProps = {
|
|
||||||
overflowItems: 20,
|
|
||||||
overflowMargin: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
LazyRenderList.propTypes = {
|
|
||||||
// height in pixels of the component returned by `renderItem`
|
|
||||||
itemHeight: PropTypes.number.isRequired,
|
|
||||||
// function to turn an element of `items` into a react component
|
|
||||||
renderItem: PropTypes.func.isRequired,
|
|
||||||
// scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
|
|
||||||
scrollTop: PropTypes.number.isRequired,
|
|
||||||
// the height of the viewport this content is scrolled in
|
|
||||||
height: PropTypes.number.isRequired,
|
|
||||||
// all items for the list. These should not be react components, see `renderItem`.
|
|
||||||
items: PropTypes.array,
|
|
||||||
// the amount of items to scroll before causing a rerender,
|
|
||||||
// should typically be less than `overflowItems` unless applying
|
|
||||||
// margins in the parent component when using multiple LazyRenderList in one viewport.
|
|
||||||
// use 0 to only rerender when items will come into view.
|
|
||||||
overflowMargin: PropTypes.number,
|
|
||||||
// the amount of items to add at the top and bottom to render,
|
|
||||||
// so not every scroll of causes a rerender.
|
|
||||||
overflowItems: PropTypes.number,
|
|
||||||
};
|
|
|
@ -135,7 +135,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
|
|
||||||
const desc = formatCommaSeparatedList(descs);
|
const desc = formatCommaSeparatedList(descs);
|
||||||
|
|
||||||
return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc });
|
return _t('%(nameList)s %(transitionList)s', { nameList, transitionList: desc });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!summaries) {
|
if (!summaries) {
|
||||||
|
|
|
@ -16,25 +16,26 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { throttle } from "lodash";
|
import { throttle } from "lodash";
|
||||||
import ResizeObserver from 'resize-observer-polyfill';
|
|
||||||
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
|
|
||||||
|
export const getPersistKey = (appId: string) => 'widget_' + appId;
|
||||||
|
|
||||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||||
// pass in a custom control as the actual body.
|
// pass in a custom control as the actual body.
|
||||||
|
|
||||||
function getContainer(containerId) {
|
function getContainer(containerId: string): HTMLDivElement {
|
||||||
return document.getElementById(containerId);
|
return document.getElementById(containerId) as HTMLDivElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrCreateContainer(containerId) {
|
function getOrCreateContainer(containerId: string): HTMLDivElement {
|
||||||
let container = getContainer(containerId);
|
let container = getContainer(containerId);
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
|
@ -46,7 +47,19 @@ function getOrCreateContainer(containerId) {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
interface IProps {
|
||||||
|
// Unique identifier for this PersistedElement instance
|
||||||
|
// Any PersistedElements with the same persistKey will use
|
||||||
|
// the same DOM container.
|
||||||
|
persistKey: string;
|
||||||
|
|
||||||
|
// z-index for the element. Defaults to 9.
|
||||||
|
zIndex?: number;
|
||||||
|
|
||||||
|
style?: React.StyleHTMLAttributes<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* Class of component that renders its children in a separate ReactDOM virtual tree
|
* Class of component that renders its children in a separate ReactDOM virtual tree
|
||||||
* in a container element appended to document.body.
|
* in a container element appended to document.body.
|
||||||
*
|
*
|
||||||
|
@ -58,42 +71,33 @@ function getOrCreateContainer(containerId) {
|
||||||
* bounding rect as the parent of PE.
|
* bounding rect as the parent of PE.
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.elements.PersistedElement")
|
@replaceableComponent("views.elements.PersistedElement")
|
||||||
export default class PersistedElement extends React.Component {
|
export default class PersistedElement extends React.Component<IProps> {
|
||||||
static propTypes = {
|
private resizeObserver: ResizeObserver;
|
||||||
// Unique identifier for this PersistedElement instance
|
private dispatcherRef: string;
|
||||||
// Any PersistedElements with the same persistKey will use
|
private childContainer: HTMLDivElement;
|
||||||
// the same DOM container.
|
private child: HTMLDivElement;
|
||||||
persistKey: PropTypes.string.isRequired,
|
|
||||||
|
|
||||||
// z-index for the element. Defaults to 9.
|
constructor(props: IProps) {
|
||||||
zIndex: PropTypes.number,
|
super(props);
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
this.resizeObserver = new ResizeObserver(this.repositionChild);
|
||||||
super();
|
|
||||||
this.collectChildContainer = this.collectChildContainer.bind(this);
|
|
||||||
this.collectChild = this.collectChild.bind(this);
|
|
||||||
this._repositionChild = this._repositionChild.bind(this);
|
|
||||||
this._onAction = this._onAction.bind(this);
|
|
||||||
|
|
||||||
this.resizeObserver = new ResizeObserver(this._repositionChild);
|
|
||||||
// Annoyingly, a resize observer is insufficient, since we also care
|
// Annoyingly, a resize observer is insufficient, since we also care
|
||||||
// about when the element moves on the screen without changing its
|
// about when the element moves on the screen without changing its
|
||||||
// dimensions. Doesn't look like there's a ResizeObserver equivalent
|
// dimensions. Doesn't look like there's a ResizeObserver equivalent
|
||||||
// for this, so we bodge it by listening for document resize and
|
// for this, so we bodge it by listening for document resize and
|
||||||
// the timeline_resize action.
|
// the timeline_resize action.
|
||||||
window.addEventListener('resize', this._repositionChild);
|
window.addEventListener('resize', this.repositionChild);
|
||||||
this._dispatcherRef = dis.register(this._onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the DOM elements created when a PersistedElement with the given
|
* Removes the DOM elements created when a PersistedElement with the given
|
||||||
* persistKey was mounted. The DOM elements will be re-added if another
|
* persistKey was mounted. The DOM elements will be re-added if another
|
||||||
* PeristedElement is mounted in the future.
|
* PersistedElement is mounted in the future.
|
||||||
*
|
*
|
||||||
* @param {string} persistKey Key used to uniquely identify this PersistedElement
|
* @param {string} persistKey Key used to uniquely identify this PersistedElement
|
||||||
*/
|
*/
|
||||||
static destroyElement(persistKey) {
|
public static destroyElement(persistKey: string): void {
|
||||||
const container = getContainer('mx_persistedElement_' + persistKey);
|
const container = getContainer('mx_persistedElement_' + persistKey);
|
||||||
if (container) {
|
if (container) {
|
||||||
container.remove();
|
container.remove();
|
||||||
|
@ -104,7 +108,7 @@ export default class PersistedElement extends React.Component {
|
||||||
return Boolean(getContainer('mx_persistedElement_' + persistKey));
|
return Boolean(getContainer('mx_persistedElement_' + persistKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
collectChildContainer(ref) {
|
private collectChildContainer = (ref: HTMLDivElement): void => {
|
||||||
if (this.childContainer) {
|
if (this.childContainer) {
|
||||||
this.resizeObserver.unobserve(this.childContainer);
|
this.resizeObserver.unobserve(this.childContainer);
|
||||||
}
|
}
|
||||||
|
@ -112,48 +116,48 @@ export default class PersistedElement extends React.Component {
|
||||||
if (ref) {
|
if (ref) {
|
||||||
this.resizeObserver.observe(ref);
|
this.resizeObserver.observe(ref);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
collectChild(ref) {
|
private collectChild = (ref: HTMLDivElement): void => {
|
||||||
this.child = ref;
|
this.child = ref;
|
||||||
this.updateChild();
|
this.updateChild();
|
||||||
}
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.updateChild();
|
this.updateChild();
|
||||||
this.renderApp();
|
this.renderApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
public componentDidUpdate(): void {
|
||||||
this.updateChild();
|
this.updateChild();
|
||||||
this.renderApp();
|
this.renderApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
this.updateChildVisibility(this.child, false);
|
this.updateChildVisibility(this.child, false);
|
||||||
this.resizeObserver.disconnect();
|
this.resizeObserver.disconnect();
|
||||||
window.removeEventListener('resize', this._repositionChild);
|
window.removeEventListener('resize', this.repositionChild);
|
||||||
dis.unregister(this._dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAction(payload) {
|
private onAction = (payload: ActionPayload): void => {
|
||||||
if (payload.action === 'timeline_resize') {
|
if (payload.action === 'timeline_resize') {
|
||||||
this._repositionChild();
|
this.repositionChild();
|
||||||
} else if (payload.action === 'logout') {
|
} else if (payload.action === 'logout') {
|
||||||
PersistedElement.destroyElement(this.props.persistKey);
|
PersistedElement.destroyElement(this.props.persistKey);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_repositionChild() {
|
private repositionChild = (): void => {
|
||||||
this.updateChildPosition(this.child, this.childContainer);
|
this.updateChildPosition(this.child, this.childContainer);
|
||||||
}
|
};
|
||||||
|
|
||||||
updateChild() {
|
private updateChild(): void {
|
||||||
this.updateChildPosition(this.child, this.childContainer);
|
this.updateChildPosition(this.child, this.childContainer);
|
||||||
this.updateChildVisibility(this.child, true);
|
this.updateChildVisibility(this.child, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderApp() {
|
private renderApp(): void {
|
||||||
const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}>
|
const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}>
|
||||||
<div ref={this.collectChild} style={this.props.style}>
|
<div ref={this.collectChild} style={this.props.style}>
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
|
@ -163,12 +167,12 @@ export default class PersistedElement extends React.Component {
|
||||||
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChildVisibility(child, visible) {
|
private updateChildVisibility(child: HTMLDivElement, visible: boolean): void {
|
||||||
if (!child) return;
|
if (!child) return;
|
||||||
child.style.display = visible ? 'block' : 'none';
|
child.style.display = visible ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChildPosition = throttle((child, parent) => {
|
private updateChildPosition = throttle((child: HTMLDivElement, parent: HTMLDivElement): void => {
|
||||||
if (!child || !parent) return;
|
if (!child || !parent) return;
|
||||||
|
|
||||||
const parentRect = parent.getBoundingClientRect();
|
const parentRect = parent.getBoundingClientRect();
|
||||||
|
@ -182,9 +186,8 @@ export default class PersistedElement extends React.Component {
|
||||||
});
|
});
|
||||||
}, 100, { trailing: true, leading: true });
|
}, 100, { trailing: true, leading: true });
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
return <div ref={this.collectChildContainer} />;
|
return <div ref={this.collectChildContainer} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPersistKey = (appId) => 'widget_' + appId;
|
|
|
@ -19,47 +19,60 @@ import React from 'react';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { EventSubscription } from 'fbemitter';
|
||||||
|
import AppTile from "./AppTile";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
roomId: string;
|
||||||
|
persistentWidgetId: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.PersistentApp")
|
@replaceableComponent("views.elements.PersistentApp")
|
||||||
export default class PersistentApp extends React.Component {
|
export default class PersistentApp extends React.Component<{}, IState> {
|
||||||
state = {
|
private roomStoreToken: EventSubscription;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({});
|
||||||
|
|
||||||
|
this.state = {
|
||||||
roomId: RoomViewStore.getRoomId(),
|
roomId: RoomViewStore.getRoomId(),
|
||||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
|
||||||
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
|
|
||||||
MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentDidMount(): void {
|
||||||
if (this._roomStoreToken) {
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
this._roomStoreToken.remove();
|
ActiveWidgetStore.on('update', this.onActiveWidgetStoreUpdate);
|
||||||
|
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
|
||||||
}
|
}
|
||||||
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
|
|
||||||
|
public componentWillUnmount(): void {
|
||||||
|
if (this.roomStoreToken) {
|
||||||
|
this.roomStoreToken.remove();
|
||||||
|
}
|
||||||
|
ActiveWidgetStore.removeListener('update', this.onActiveWidgetStoreUpdate);
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership);
|
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoomViewStoreUpdate = payload => {
|
private onRoomViewStoreUpdate = (): void => {
|
||||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
roomId: RoomViewStore.getRoomId(),
|
roomId: RoomViewStore.getRoomId(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onActiveWidgetStoreUpdate = () => {
|
private onActiveWidgetStoreUpdate = (): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onMyMembership = async (room, membership) => {
|
private onMyMembership = async (room: Room, membership: string): Promise<void> => {
|
||||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||||
if (membership !== "join") {
|
if (membership !== "join") {
|
||||||
// we're not in the room anymore - delete
|
// we're not in the room anymore - delete
|
||||||
|
@ -69,7 +82,7 @@ export default class PersistentApp extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.persistentWidgetId) {
|
if (this.state.persistentWidgetId) {
|
||||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||||
|
|
||||||
|
@ -89,7 +102,6 @@ export default class PersistentApp extends React.Component {
|
||||||
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
|
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
|
||||||
persistentWidgetInRoomId, appEvent.getId(),
|
persistentWidgetInRoomId, appEvent.getId(),
|
||||||
);
|
);
|
||||||
const AppTile = sdk.getComponent('elements.AppTile');
|
|
||||||
return <AppTile
|
return <AppTile
|
||||||
key={app.id}
|
key={app.id}
|
||||||
app={app}
|
app={app}
|
|
@ -15,40 +15,52 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as Roles from '../../../Roles';
|
import * as Roles from '../../../Roles';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import Field from "./Field";
|
import Field from "./Field";
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.elements.PowerSelector")
|
const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM";
|
||||||
export default class PowerSelector extends React.Component {
|
|
||||||
static propTypes = {
|
interface IProps {
|
||||||
value: PropTypes.number.isRequired,
|
value: number;
|
||||||
// The maximum value that can be set with the power selector
|
// The maximum value that can be set with the power selector
|
||||||
maxValue: PropTypes.number.isRequired,
|
maxValue: number;
|
||||||
|
|
||||||
// Default user power level for the room
|
// Default user power level for the room
|
||||||
usersDefault: PropTypes.number.isRequired,
|
usersDefault: number;
|
||||||
|
|
||||||
// should the user be able to change the value? false by default.
|
// should the user be able to change the value? false by default.
|
||||||
disabled: PropTypes.bool,
|
disabled?: boolean;
|
||||||
onChange: PropTypes.func,
|
onChange?: (value: number, powerLevelKey: string) => void;
|
||||||
|
|
||||||
// Optional key to pass as the second argument to `onChange`
|
// Optional key to pass as the second argument to `onChange`
|
||||||
powerLevelKey: PropTypes.string,
|
powerLevelKey?: string;
|
||||||
|
|
||||||
// The name to annotate the selector with
|
// The name to annotate the selector with
|
||||||
label: PropTypes.string,
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
interface IState {
|
||||||
|
levelRoleMap: {};
|
||||||
|
// List of power levels to show in the drop-down
|
||||||
|
options: number[];
|
||||||
|
|
||||||
|
customValue: number;
|
||||||
|
selectValue: number | string;
|
||||||
|
custom?: boolean;
|
||||||
|
customLevel?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.elements.PowerSelector")
|
||||||
|
export default class PowerSelector extends React.Component<IProps, IState> {
|
||||||
|
public static defaultProps: Partial<IProps> = {
|
||||||
maxValue: Infinity,
|
maxValue: Infinity,
|
||||||
usersDefault: 0,
|
usersDefault: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -62,26 +74,26 @@ export default class PowerSelector extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
|
||||||
UNSAFE_componentWillMount() {
|
public UNSAFE_componentWillMount(): void {
|
||||||
this._initStateFromProps(this.props);
|
this.initStateFromProps(this.props);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
|
||||||
UNSAFE_componentWillReceiveProps(newProps) {
|
public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
|
||||||
this._initStateFromProps(newProps);
|
this.initStateFromProps(newProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
_initStateFromProps(newProps) {
|
private initStateFromProps(newProps: IProps): void {
|
||||||
// This needs to be done now because levelRoleMap has translated strings
|
// This needs to be done now because levelRoleMap has translated strings
|
||||||
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
|
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
|
||||||
const options = Object.keys(levelRoleMap).filter(level => {
|
const options = Object.keys(levelRoleMap).filter(level => {
|
||||||
return (
|
return (
|
||||||
level === undefined ||
|
level === undefined ||
|
||||||
level <= newProps.maxValue ||
|
parseInt(level) <= newProps.maxValue ||
|
||||||
level == newProps.value
|
parseInt(level) == newProps.value
|
||||||
);
|
);
|
||||||
});
|
}).map(level => parseInt(level));
|
||||||
|
|
||||||
const isCustom = levelRoleMap[newProps.value] === undefined;
|
const isCustom = levelRoleMap[newProps.value] === undefined;
|
||||||
|
|
||||||
|
@ -90,32 +102,33 @@ export default class PowerSelector extends React.Component {
|
||||||
options,
|
options,
|
||||||
custom: isCustom,
|
custom: isCustom,
|
||||||
customLevel: newProps.value,
|
customLevel: newProps.value,
|
||||||
selectValue: isCustom ? "SELECT_VALUE_CUSTOM" : newProps.value,
|
selectValue: isCustom ? CUSTOM_VALUE : newProps.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectChange = event => {
|
private onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||||
const isCustom = event.target.value === "SELECT_VALUE_CUSTOM";
|
const isCustom = event.target.value === CUSTOM_VALUE;
|
||||||
if (isCustom) {
|
if (isCustom) {
|
||||||
this.setState({ custom: true });
|
this.setState({ custom: true });
|
||||||
} else {
|
} else {
|
||||||
this.props.onChange(event.target.value, this.props.powerLevelKey);
|
const powerLevel = parseInt(event.target.value);
|
||||||
this.setState({ selectValue: event.target.value });
|
this.props.onChange(powerLevel, this.props.powerLevelKey);
|
||||||
|
this.setState({ selectValue: powerLevel });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onCustomChange = event => {
|
private onCustomChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({ customValue: event.target.value });
|
this.setState({ customValue: parseInt(event.target.value) });
|
||||||
};
|
};
|
||||||
|
|
||||||
onCustomBlur = event => {
|
private onCustomBlur = (event: React.FocusEvent): void => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey);
|
this.props.onChange(this.state.customValue, this.props.powerLevelKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
onCustomKeyDown = event => {
|
private onCustomKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||||
if (event.key === Key.ENTER) {
|
if (event.key === Key.ENTER) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -125,11 +138,11 @@ export default class PowerSelector extends React.Component {
|
||||||
// raising a dialog which causes a blur which causes a dialog which causes a blur and
|
// raising a dialog which causes a blur which causes a dialog which causes a blur and
|
||||||
// so on. By not causing the onChange to be called here, we avoid the loop because we
|
// so on. By not causing the onChange to be called here, we avoid the loop because we
|
||||||
// handle the onBlur safely.
|
// handle the onBlur safely.
|
||||||
event.target.blur();
|
(event.target as HTMLInputElement).blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
let picker;
|
let picker;
|
||||||
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
|
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
|
||||||
if (this.state.custom) {
|
if (this.state.custom) {
|
||||||
|
@ -147,14 +160,14 @@ export default class PowerSelector extends React.Component {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Each level must have a definition in this.state.levelRoleMap
|
// Each level must have a definition in this.state.levelRoleMap
|
||||||
let options = this.state.options.map((level) => {
|
const options = this.state.options.map((level) => {
|
||||||
return {
|
return {
|
||||||
value: level,
|
value: String(level),
|
||||||
text: Roles.textualPowerLevel(level, this.props.usersDefault),
|
text: Roles.textualPowerLevel(level, this.props.usersDefault),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
|
options.push({ value: CUSTOM_VALUE, text: _t("Custom level") });
|
||||||
options = options.map((op) => {
|
const optionsElements = options.map((op) => {
|
||||||
return <option value={op.value} key={op.value}>{ op.text }</option>;
|
return <option value={op.value} key={op.value}>{ op.text }</option>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -166,7 +179,7 @@ export default class PowerSelector extends React.Component {
|
||||||
value={String(this.state.selectValue)}
|
value={String(this.state.selectValue)}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
>
|
>
|
||||||
{ options }
|
{ optionsElements }
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -88,7 +88,13 @@ export default class ReplyThread extends React.Component<IProps, IState> {
|
||||||
// could be used here for replies as well... However, the helper
|
// could be used here for replies as well... However, the helper
|
||||||
// currently assumes the relation has a `rel_type`, which older replies
|
// currently assumes the relation has a `rel_type`, which older replies
|
||||||
// do not, so this block is left as-is for now.
|
// do not, so this block is left as-is for now.
|
||||||
const mRelatesTo = ev.getWireContent()['m.relates_to'];
|
//
|
||||||
|
// We're prefer ev.getContent() over ev.getWireContent() to make sure
|
||||||
|
// we grab the latest edit with potentially new relations. But we also
|
||||||
|
// can't just rely on ev.getContent() by itself because historically we
|
||||||
|
// still show the reply from the original message even though the edit
|
||||||
|
// event does not include the relation reply.
|
||||||
|
const mRelatesTo = ev.getContent()['m.relates_to'] || ev.getWireContent()['m.relates_to'];
|
||||||
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
|
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
|
||||||
const mInReplyTo = mRelatesTo['m.in_reply_to'];
|
const mInReplyTo = mRelatesTo['m.in_reply_to'];
|
||||||
if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];
|
if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];
|
||||||
|
|
|
@ -17,25 +17,34 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
reason?: string;
|
||||||
|
contentHtml: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.Spoiler")
|
@replaceableComponent("views.elements.Spoiler")
|
||||||
export default class Spoiler extends React.Component {
|
export default class Spoiler extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
visible: false,
|
visible: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleVisible(e) {
|
private toggleVisible = (e: React.MouseEvent): void => {
|
||||||
if (!this.state.visible) {
|
if (!this.state.visible) {
|
||||||
// we are un-blurring, we don't want this click to propagate to potential child pills
|
// we are un-blurring, we don't want this click to propagate to potential child pills
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
this.setState({ visible: !this.state.visible });
|
this.setState({ visible: !this.state.visible });
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const reason = this.props.reason ? (
|
const reason = this.props.reason ? (
|
||||||
<span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span>
|
<span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span>
|
||||||
) : null;
|
) : null;
|
||||||
|
@ -43,7 +52,7 @@ export default class Spoiler extends React.Component {
|
||||||
// as such, we pass the this.props.contentHtml instead and then set the raw
|
// as such, we pass the this.props.contentHtml instead and then set the raw
|
||||||
// HTML content. This is secure as the contents have already been parsed previously
|
// HTML content. This is secure as the contents have already been parsed previously
|
||||||
return (
|
return (
|
||||||
<span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible.bind(this)}>
|
<span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible}>
|
||||||
{ reason }
|
{ reason }
|
||||||
|
|
||||||
<span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />
|
<span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />
|
|
@ -15,40 +15,40 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { highlightBlock } from 'highlight.js';
|
import { highlightBlock } from 'highlight.js';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.SyntaxHighlight")
|
@replaceableComponent("views.elements.SyntaxHighlight")
|
||||||
export default class SyntaxHighlight extends React.Component {
|
export default class SyntaxHighlight extends React.Component<IProps> {
|
||||||
static propTypes = {
|
private el: HTMLPreElement = null;
|
||||||
className: PropTypes.string,
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._ref = this._ref.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// componentDidUpdate used here for reusability
|
// componentDidUpdate used here for reusability
|
||||||
componentDidUpdate() {
|
public componentDidUpdate(): void {
|
||||||
if (this._el) highlightBlock(this._el);
|
if (this.el) highlightBlock(this.el);
|
||||||
}
|
}
|
||||||
|
|
||||||
// call componentDidUpdate because _ref is fired on initial render
|
// call componentDidUpdate because _ref is fired on initial render
|
||||||
// which does not fire componentDidUpdate
|
// which does not fire componentDidUpdate
|
||||||
_ref(el) {
|
private ref = (el: HTMLPreElement): void => {
|
||||||
this._el = el;
|
this.el = el;
|
||||||
this.componentDidUpdate();
|
this.componentDidUpdate();
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const { className, children } = this.props;
|
const { className, children } = this.props;
|
||||||
|
|
||||||
return <pre className={`${className} mx_SyntaxHighlight`} ref={this._ref}>
|
return <pre className={`${className} mx_SyntaxHighlight`} ref={this.ref}>
|
||||||
<code>{ children }</code>
|
<code>{ children }</code>
|
||||||
</pre>;
|
</pre>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,42 +15,44 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Tooltip from "./Tooltip";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
class?: string;
|
||||||
|
tooltipClass?: string;
|
||||||
|
tooltip: React.ReactNode;
|
||||||
|
tooltipProps?: {};
|
||||||
|
onClick?: (ev?: React.MouseEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
hover: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.TextWithTooltip")
|
@replaceableComponent("views.elements.TextWithTooltip")
|
||||||
export default class TextWithTooltip extends React.Component {
|
export default class TextWithTooltip extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
class: PropTypes.string,
|
super(props);
|
||||||
tooltipClass: PropTypes.string,
|
|
||||||
tooltip: PropTypes.node.isRequired,
|
|
||||||
tooltipProps: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hover: false,
|
hover: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseOver = () => {
|
private onMouseOver = (): void => {
|
||||||
this.setState({ hover: true });
|
this.setState({ hover: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
onMouseLeave = () => {
|
private onMouseLeave = (): void => {
|
||||||
this.setState({ hover: false });
|
this.setState({ hover: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
|
||||||
|
|
||||||
const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props;
|
const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
|
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} onClick={this.props.onClick} className={className}>
|
||||||
{ children }
|
{ children }
|
||||||
{ this.state.hover && <Tooltip
|
{ this.state.hover && <Tooltip
|
||||||
{...tooltipProps}
|
{...tooltipProps}
|
|
@ -15,20 +15,20 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||||
import QRCode from "../QRCode";
|
import QRCode from "../QRCode";
|
||||||
|
import { QRCodeData } from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
qrCodeData: QRCodeData;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.crypto.VerificationQRCode")
|
@replaceableComponent("views.elements.crypto.VerificationQRCode")
|
||||||
export default class VerificationQRCode extends React.PureComponent {
|
export default class VerificationQRCode extends React.PureComponent<IProps> {
|
||||||
static propTypes = {
|
public render(): JSX.Element {
|
||||||
qrCodeData: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<QRCode
|
<QRCode
|
||||||
data={[{ data: this.props.qrCodeData.buffer, mode: 'byte' }]}
|
data={[{ data: this.props.qrCodeData.getBuffer(), mode: 'byte' }]}
|
||||||
className="mx_VerificationQRCode"
|
className="mx_VerificationQRCode"
|
||||||
width={196} />
|
width={196} />
|
||||||
);
|
);
|
|
@ -106,31 +106,20 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
|
||||||
}
|
}
|
||||||
|
|
||||||
const room = this.context.getRoom(mxEvent.getRoomId());
|
const room = this.context.getRoom(mxEvent.getRoomId());
|
||||||
let label;
|
let label: string;
|
||||||
if (room) {
|
if (room) {
|
||||||
const senders = [];
|
const senders = [];
|
||||||
for (const reactionEvent of reactionEvents) {
|
for (const reactionEvent of reactionEvents) {
|
||||||
const member = room.getMember(reactionEvent.getSender());
|
const member = room.getMember(reactionEvent.getSender());
|
||||||
const name = member ? member.name : reactionEvent.getSender();
|
senders.push(member?.name || reactionEvent.getSender());
|
||||||
senders.push(name);
|
|
||||||
}
|
}
|
||||||
label = _t(
|
|
||||||
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
|
const reactors = formatCommaSeparatedList(senders, 6);
|
||||||
{
|
if (content) {
|
||||||
content,
|
label = _t("%(reactors)s reacted with %(content)s", { reactors, content });
|
||||||
},
|
} else {
|
||||||
{
|
label = reactors;
|
||||||
reactors: () => {
|
|
||||||
return formatCommaSeparatedList(senders, 6);
|
|
||||||
},
|
|
||||||
reactedWith: (sub) => {
|
|
||||||
if (!content) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return sub;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const isPeeking = room.getMyMembership() !== "join";
|
const isPeeking = room.getMyMembership() !== "join";
|
||||||
return <AccessibleButton
|
return <AccessibleButton
|
||||||
|
|
|
@ -429,7 +429,7 @@ const UserOptionsSection: React.FC<{
|
||||||
if (!isMe) {
|
if (!isMe) {
|
||||||
directMessageButton = (
|
directMessageButton = (
|
||||||
<AccessibleButton onClick={() => { openDMForUser(cli, member.userId); }} className="mx_UserInfo_field">
|
<AccessibleButton onClick={() => { openDMForUser(cli, member.userId); }} className="mx_UserInfo_field">
|
||||||
{ _t('Direct message') }
|
{ _t("Message") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -826,7 +826,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
||||||
if (canAffectUser && me.powerLevel >= banPowerLevel) {
|
if (canAffectUser && me.powerLevel >= banPowerLevel) {
|
||||||
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||||
}
|
}
|
||||||
if (canAffectUser && me.powerLevel >= editPowerLevel) {
|
if (canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) {
|
||||||
muteButton = (
|
muteButton = (
|
||||||
<MuteToggleButton
|
<MuteToggleButton
|
||||||
member={member}
|
member={member}
|
||||||
|
@ -1052,8 +1052,7 @@ const PowerLevelEditor: React.FC<{
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
|
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
|
||||||
const onPowerChange = useCallback(async (powerLevelStr: string) => {
|
const onPowerChange = useCallback(async (powerLevel: number) => {
|
||||||
const powerLevel = parseInt(powerLevelStr, 10);
|
|
||||||
setSelectedPowerLevel(powerLevel);
|
setSelectedPowerLevel(powerLevel);
|
||||||
|
|
||||||
const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { SAS } from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||||
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
|
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import E2EIcon from "../rooms/E2EIcon";
|
import E2EIcon, { E2EState } from "../rooms/E2EIcon";
|
||||||
import { Phase } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
import { Phase } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
@ -189,7 +189,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||||
// Element Web doesn't support scanning yet, so assume here we're the client being scanned.
|
// Element Web doesn't support scanning yet, so assume here we're the client being scanned.
|
||||||
body = <React.Fragment>
|
body = <React.Fragment>
|
||||||
<p>{ description }</p>
|
<p>{ description }</p>
|
||||||
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
|
<E2EIcon isUser={true} status={E2EState.Verified} size={128} hideTooltip={true} />
|
||||||
<div className="mx_VerificationPanel_reciprocateButtons">
|
<div className="mx_VerificationPanel_reciprocateButtons">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="danger"
|
kind="danger"
|
||||||
|
@ -252,7 +252,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||||
<div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
|
<div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
|
||||||
<h3>{ _t("Verified") }</h3>
|
<h3>{ _t("Verified") }</h3>
|
||||||
<p>{ description }</p>
|
<p>{ description }</p>
|
||||||
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
|
<E2EIcon isUser={true} status={E2EState.Verified} size={128} hideTooltip={true} />
|
||||||
{ text ? <p>{ text }</p> : null }
|
{ text ? <p>{ text }</p> : null }
|
||||||
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
|
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
|
||||||
{ _t("Got it") }
|
{ _t("Got it") }
|
||||||
|
|
|
@ -97,7 +97,6 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||||
<AppTile
|
<AppTile
|
||||||
app={app}
|
app={app}
|
||||||
fullWidth
|
fullWidth
|
||||||
show
|
|
||||||
showMenubar={false}
|
showMenubar={false}
|
||||||
room={room}
|
room={room}
|
||||||
userId={cli.getUserId()}
|
userId={cli.getUserId()}
|
||||||
|
|
|
@ -15,27 +15,43 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromMxc } from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import AvatarSetting from "../settings/AvatarSetting";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
roomId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
originalDisplayName: string;
|
||||||
|
displayName: string;
|
||||||
|
originalAvatarUrl: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
avatarFile: File;
|
||||||
|
originalTopic: string;
|
||||||
|
topic: string;
|
||||||
|
enableProfileSave: boolean;
|
||||||
|
canSetName: boolean;
|
||||||
|
canSetTopic: boolean;
|
||||||
|
canSetAvatar: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Merge with ProfileSettings?
|
// TODO: Merge with ProfileSettings?
|
||||||
@replaceableComponent("views.room_settings.RoomProfileSettings")
|
@replaceableComponent("views.room_settings.RoomProfileSettings")
|
||||||
export default class RoomProfileSettings extends React.Component {
|
export default class RoomProfileSettings extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private avatarUpload = createRef<HTMLInputElement>();
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(props.roomId);
|
const room = client.getRoom(props.roomId);
|
||||||
if (!room) throw new Error("Expected a room for ID: ", props.roomId);
|
if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`);
|
||||||
|
|
||||||
const avatarEvent = room.currentState.getStateEvents("m.room.avatar", "");
|
const avatarEvent = room.currentState.getStateEvents("m.room.avatar", "");
|
||||||
let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null;
|
let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null;
|
||||||
|
@ -60,17 +76,15 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()),
|
canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()),
|
||||||
canSetAvatar: room.currentState.maySendStateEvent('m.room.avatar', client.getUserId()),
|
canSetAvatar: room.currentState.maySendStateEvent('m.room.avatar', client.getUserId()),
|
||||||
};
|
};
|
||||||
|
|
||||||
this._avatarUpload = createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_uploadAvatar = () => {
|
private uploadAvatar = (): void => {
|
||||||
this._avatarUpload.current.click();
|
this.avatarUpload.current.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
_removeAvatar = () => {
|
private removeAvatar = (): void => {
|
||||||
// clear file upload field so same file can be selected
|
// clear file upload field so same file can be selected
|
||||||
this._avatarUpload.current.value = "";
|
this.avatarUpload.current.value = "";
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
avatarFile: null,
|
avatarFile: null,
|
||||||
|
@ -78,7 +92,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_cancelProfileChanges = async (e) => {
|
private cancelProfileChanges = async (e: React.MouseEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -92,7 +106,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_saveProfile = async (e) => {
|
private saveProfile = async (e: React.FormEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -100,35 +114,46 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
this.setState({ enableProfileSave: false });
|
this.setState({ enableProfileSave: false });
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const newState = {};
|
|
||||||
|
let originalDisplayName: string;
|
||||||
|
let avatarUrl: string;
|
||||||
|
let originalAvatarUrl: string;
|
||||||
|
let originalTopic: string;
|
||||||
|
let avatarFile: File;
|
||||||
|
|
||||||
// TODO: What do we do about errors?
|
// TODO: What do we do about errors?
|
||||||
const displayName = this.state.displayName.trim();
|
const displayName = this.state.displayName.trim();
|
||||||
if (this.state.originalDisplayName !== this.state.displayName) {
|
if (this.state.originalDisplayName !== this.state.displayName) {
|
||||||
await client.setRoomName(this.props.roomId, displayName);
|
await client.setRoomName(this.props.roomId, displayName);
|
||||||
newState.originalDisplayName = displayName;
|
originalDisplayName = displayName;
|
||||||
newState.displayName = displayName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.avatarFile) {
|
if (this.state.avatarFile) {
|
||||||
const uri = await client.uploadContent(this.state.avatarFile);
|
const uri = await client.uploadContent(this.state.avatarFile);
|
||||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', { url: uri }, '');
|
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', { url: uri }, '');
|
||||||
newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
|
avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
|
||||||
newState.originalAvatarUrl = newState.avatarUrl;
|
originalAvatarUrl = avatarUrl;
|
||||||
newState.avatarFile = null;
|
avatarFile = null;
|
||||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, '');
|
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.originalTopic !== this.state.topic) {
|
if (this.state.originalTopic !== this.state.topic) {
|
||||||
await client.setRoomTopic(this.props.roomId, this.state.topic);
|
await client.setRoomTopic(this.props.roomId, this.state.topic);
|
||||||
newState.originalTopic = this.state.topic;
|
originalTopic = this.state.topic;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(newState);
|
this.setState({
|
||||||
|
originalAvatarUrl,
|
||||||
|
avatarUrl,
|
||||||
|
originalDisplayName,
|
||||||
|
originalTopic,
|
||||||
|
displayName,
|
||||||
|
avatarFile,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDisplayNameChanged = (e) => {
|
private onDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({ displayName: e.target.value });
|
this.setState({ displayName: e.target.value });
|
||||||
if (this.state.originalDisplayName === e.target.value) {
|
if (this.state.originalDisplayName === e.target.value) {
|
||||||
this.setState({ enableProfileSave: false });
|
this.setState({ enableProfileSave: false });
|
||||||
|
@ -137,7 +162,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onTopicChanged = (e) => {
|
private onTopicChanged = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||||
this.setState({ topic: e.target.value });
|
this.setState({ topic: e.target.value });
|
||||||
if (this.state.originalTopic === e.target.value) {
|
if (this.state.originalTopic === e.target.value) {
|
||||||
this.setState({ enableProfileSave: false });
|
this.setState({ enableProfileSave: false });
|
||||||
|
@ -146,7 +171,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAvatarChanged = (e) => {
|
private onAvatarChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
if (!e.target.files || !e.target.files.length) {
|
if (!e.target.files || !e.target.files.length) {
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarUrl: this.state.originalAvatarUrl,
|
avatarUrl: this.state.originalAvatarUrl,
|
||||||
|
@ -160,7 +185,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (ev) => {
|
reader.onload = (ev) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarUrl: ev.target.result,
|
avatarUrl: String(ev.target.result),
|
||||||
avatarFile: file,
|
avatarFile: file,
|
||||||
enableProfileSave: true,
|
enableProfileSave: true,
|
||||||
});
|
});
|
||||||
|
@ -168,10 +193,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
|
||||||
|
|
||||||
let profileSettingsButtons;
|
let profileSettingsButtons;
|
||||||
if (
|
if (
|
||||||
this.state.canSetName ||
|
this.state.canSetName ||
|
||||||
|
@ -181,14 +203,14 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
profileSettingsButtons = (
|
profileSettingsButtons = (
|
||||||
<div className="mx_ProfileSettings_buttons">
|
<div className="mx_ProfileSettings_buttons">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._cancelProfileChanges}
|
onClick={this.cancelProfileChanges}
|
||||||
kind="link"
|
kind="link"
|
||||||
disabled={!this.state.enableProfileSave}
|
disabled={!this.state.enableProfileSave}
|
||||||
>
|
>
|
||||||
{ _t("Cancel") }
|
{ _t("Cancel") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._saveProfile}
|
onClick={this.saveProfile}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
disabled={!this.state.enableProfileSave}
|
disabled={!this.state.enableProfileSave}
|
||||||
>
|
>
|
||||||
|
@ -200,16 +222,16 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={this._saveProfile}
|
onSubmit={this.saveProfile}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
noValidate={true}
|
noValidate={true}
|
||||||
className="mx_ProfileSettings_profileForm"
|
className="mx_ProfileSettings_profileForm"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref={this._avatarUpload}
|
ref={this.avatarUpload}
|
||||||
className="mx_ProfileSettings_avatarUpload"
|
className="mx_ProfileSettings_avatarUpload"
|
||||||
onChange={this._onAvatarChanged}
|
onChange={this.onAvatarChanged}
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
/>
|
/>
|
||||||
<div className="mx_ProfileSettings_profile">
|
<div className="mx_ProfileSettings_profile">
|
||||||
|
@ -219,7 +241,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
type="text"
|
type="text"
|
||||||
value={this.state.displayName}
|
value={this.state.displayName}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onChange={this._onDisplayNameChanged}
|
onChange={this.onDisplayNameChanged}
|
||||||
disabled={!this.state.canSetName}
|
disabled={!this.state.canSetName}
|
||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
|
@ -230,7 +252,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
type="text"
|
type="text"
|
||||||
value={this.state.topic}
|
value={this.state.topic}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onChange={this._onTopicChanged}
|
onChange={this.onTopicChanged}
|
||||||
element="textarea"
|
element="textarea"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -238,8 +260,8 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
avatarUrl={this.state.avatarUrl}
|
avatarUrl={this.state.avatarUrl}
|
||||||
avatarName={this.state.displayName || this.props.roomId}
|
avatarName={this.state.displayName || this.props.roomId}
|
||||||
avatarAltText={_t("Room avatar")}
|
avatarAltText={_t("Room avatar")}
|
||||||
uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
|
uploadAvatar={this.state.canSetAvatar ? this.uploadAvatar : undefined}
|
||||||
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
|
removeAvatar={this.state.canSetAvatar ? this.removeAvatar : undefined} />
|
||||||
</div>
|
</div>
|
||||||
{ profileSettingsButtons }
|
{ profileSettingsButtons }
|
||||||
</form>
|
</form>
|
|
@ -18,8 +18,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
@ -27,21 +25,22 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import SettingsFlag from "../elements/SettingsFlag";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room: Room;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.room_settings.UrlPreviewSettings")
|
@replaceableComponent("views.room_settings.UrlPreviewSettings")
|
||||||
export default class UrlPreviewSettings extends React.Component {
|
export default class UrlPreviewSettings extends React.Component<IProps> {
|
||||||
static propTypes = {
|
private onClickUserSettings = (e: React.MouseEvent): void => {
|
||||||
room: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
_onClickUserSettings = (e) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dis.fire(Action.ViewUserSettings);
|
dis.fire(Action.ViewUserSettings);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
|
||||||
const roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
|
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
|
||||||
|
|
||||||
|
@ -54,18 +53,18 @@ export default class UrlPreviewSettings extends React.Component {
|
||||||
if (accountEnabled) {
|
if (accountEnabled) {
|
||||||
previewsForAccount = (
|
previewsForAccount = (
|
||||||
_t("You have <a>enabled</a> URL previews by default.", {}, {
|
_t("You have <a>enabled</a> URL previews by default.", {}, {
|
||||||
'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>,
|
'a': (sub)=><a onClick={this.onClickUserSettings} href=''>{ sub }</a>,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
previewsForAccount = (
|
previewsForAccount = (
|
||||||
_t("You have <a>disabled</a> URL previews by default.", {}, {
|
_t("You have <a>disabled</a> URL previews by default.", {}, {
|
||||||
'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>,
|
'a': (sub)=><a onClick={this.onClickUserSettings} href=''>{ sub }</a>,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) {
|
||||||
previewsForRoom = (
|
previewsForRoom = (
|
||||||
<label>
|
<label>
|
||||||
<SettingsFlag
|
<SettingsFlag
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Resizable } from "re-resizable";
|
import { Resizable } from "re-resizable";
|
||||||
|
|
||||||
|
@ -26,8 +25,6 @@ import * as sdk from '../../../index';
|
||||||
import * as ScalarMessaging from '../../../ScalarMessaging';
|
import * as ScalarMessaging from '../../../ScalarMessaging';
|
||||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
|
||||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||||
import ResizeHandle from "../elements/ResizeHandle";
|
import ResizeHandle from "../elements/ResizeHandle";
|
||||||
import Resizer from "../../../resizer/resizer";
|
import Resizer from "../../../resizer/resizer";
|
||||||
|
@ -37,60 +34,74 @@ import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers";
|
||||||
import { useStateCallback } from "../../../hooks/useStateCallback";
|
import { useStateCallback } from "../../../hooks/useStateCallback";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import UIStore from "../../../stores/UIStore";
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { IApp } from "../../../stores/WidgetStore";
|
||||||
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
userId: string;
|
||||||
|
room: Room;
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
showApps?: boolean; // Should apps be rendered
|
||||||
|
maxHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
apps: IApp[];
|
||||||
|
resizingVertical: boolean; // true when changing the height of the apps drawer
|
||||||
|
resizingHorizontal: boolean; // true when chagning the distribution of the width between widgets
|
||||||
|
resizing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.AppsDrawer")
|
@replaceableComponent("views.rooms.AppsDrawer")
|
||||||
export default class AppsDrawer extends React.Component {
|
export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private resizeContainer: HTMLDivElement;
|
||||||
userId: PropTypes.string.isRequired,
|
private resizer: Resizer;
|
||||||
room: PropTypes.object.isRequired,
|
private dispatcherRef: string;
|
||||||
resizeNotifier: PropTypes.instanceOf(ResizeNotifier).isRequired,
|
public static defaultProps: Partial<IProps> = {
|
||||||
showApps: PropTypes.bool, // Should apps be rendered
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
showApps: true,
|
showApps: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
apps: this._getApps(),
|
apps: this.getApps(),
|
||||||
resizingVertical: false, // true when changing the height of the apps drawer
|
resizingVertical: false,
|
||||||
resizingHorizontal: false, // true when chagning the distribution of the width between widgets
|
resizingHorizontal: false,
|
||||||
|
resizing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._resizeContainer = null;
|
this.resizer = this.createResizer();
|
||||||
this.resizer = this._createResizer();
|
|
||||||
|
|
||||||
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
|
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
ScalarMessaging.startListening();
|
ScalarMessaging.startListening();
|
||||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
ScalarMessaging.stopListening();
|
ScalarMessaging.stopListening();
|
||||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
|
||||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||||
if (this._resizeContainer) {
|
if (this.resizeContainer) {
|
||||||
this.resizer.detach();
|
this.resizer.detach();
|
||||||
}
|
}
|
||||||
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
|
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
|
||||||
}
|
}
|
||||||
|
|
||||||
onIsResizing = (resizing) => {
|
private onIsResizing = (resizing: boolean): void => {
|
||||||
// This one is the vertical, ie. change height of apps drawer
|
// This one is the vertical, ie. change height of apps drawer
|
||||||
this.setState({ resizingVertical: resizing });
|
this.setState({ resizingVertical: resizing });
|
||||||
if (!resizing) {
|
if (!resizing) {
|
||||||
this._relaxResizer();
|
this.relaxResizer();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_createResizer() {
|
private createResizer(): Resizer {
|
||||||
// This is the horizontal one, changing the distribution of the width between the app tiles
|
// This is the horizontal one, changing the distribution of the width between the app tiles
|
||||||
// (ie. a vertical resize handle because, the handle itself is vertical...)
|
// (ie. a vertical resize handle because, the handle itself is vertical...)
|
||||||
const classNames = {
|
const classNames = {
|
||||||
|
@ -100,11 +111,11 @@ export default class AppsDrawer extends React.Component {
|
||||||
};
|
};
|
||||||
const collapseConfig = {
|
const collapseConfig = {
|
||||||
onResizeStart: () => {
|
onResizeStart: () => {
|
||||||
this._resizeContainer.classList.add("mx_AppsDrawer_resizing");
|
this.resizeContainer.classList.add("mx_AppsDrawer_resizing");
|
||||||
this.setState({ resizingHorizontal: true });
|
this.setState({ resizingHorizontal: true });
|
||||||
},
|
},
|
||||||
onResizeStop: () => {
|
onResizeStop: () => {
|
||||||
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
this.resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||||
WidgetLayoutStore.instance.setResizerDistributions(
|
WidgetLayoutStore.instance.setResizerDistributions(
|
||||||
this.props.room, Container.Top,
|
this.props.room, Container.Top,
|
||||||
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||||
|
@ -113,13 +124,13 @@ export default class AppsDrawer extends React.Component {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// pass a truthy container for now, we won't call attach until we update it
|
// pass a truthy container for now, we won't call attach until we update it
|
||||||
const resizer = new Resizer({}, PercentageDistributor, collapseConfig);
|
const resizer = new Resizer(null, PercentageDistributor, collapseConfig);
|
||||||
resizer.setClassNames(classNames);
|
resizer.setClassNames(classNames);
|
||||||
return resizer;
|
return resizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
_collectResizer = (ref) => {
|
private collectResizer = (ref: HTMLDivElement): void => {
|
||||||
if (this._resizeContainer) {
|
if (this.resizeContainer) {
|
||||||
this.resizer.detach();
|
this.resizer.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,22 +138,22 @@ export default class AppsDrawer extends React.Component {
|
||||||
this.resizer.container = ref;
|
this.resizer.container = ref;
|
||||||
this.resizer.attach();
|
this.resizer.attach();
|
||||||
}
|
}
|
||||||
this._resizeContainer = ref;
|
this.resizeContainer = ref;
|
||||||
this._loadResizerPreferences();
|
this.loadResizerPreferences();
|
||||||
};
|
};
|
||||||
|
|
||||||
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
private getAppsHash = (apps: IApp[]): string => apps.map(app => app.id).join("~");
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||||
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
||||||
// Room has changed, update apps
|
// Room has changed, update apps
|
||||||
this._updateApps();
|
this.updateApps();
|
||||||
} else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
|
} else if (this.getAppsHash(this.state.apps) !== this.getAppsHash(prevState.apps)) {
|
||||||
this._loadResizerPreferences();
|
this.loadResizerPreferences();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_relaxResizer = () => {
|
private relaxResizer = (): void => {
|
||||||
const distributors = this.resizer.getDistributors();
|
const distributors = this.resizer.getDistributors();
|
||||||
|
|
||||||
// relax all items if they had any overconstrained flexboxes
|
// relax all items if they had any overconstrained flexboxes
|
||||||
|
@ -150,7 +161,7 @@ export default class AppsDrawer extends React.Component {
|
||||||
distributors.forEach(d => d.finish());
|
distributors.forEach(d => d.finish());
|
||||||
};
|
};
|
||||||
|
|
||||||
_loadResizerPreferences = () => {
|
private loadResizerPreferences = (): void => {
|
||||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
||||||
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
|
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
|
||||||
distributions.forEach((size, i) => {
|
distributions.forEach((size, i) => {
|
||||||
|
@ -168,11 +179,11 @@ export default class AppsDrawer extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
isResizing() {
|
private isResizing(): boolean {
|
||||||
return this.state.resizingVertical || this.state.resizingHorizontal;
|
return this.state.resizingVertical || this.state.resizingHorizontal;
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction = (action) => {
|
private onAction = (action: ActionPayload): void => {
|
||||||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||||
switch (action.action) {
|
switch (action.action) {
|
||||||
case 'appsDrawer':
|
case 'appsDrawer':
|
||||||
|
@ -190,23 +201,15 @@ export default class AppsDrawer extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_getApps = () => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
private getApps = (): IApp[] => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
||||||
|
|
||||||
_updateApps = () => {
|
private updateApps = (): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
apps: this._getApps(),
|
apps: this.getApps(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_launchManageIntegrations() {
|
public render(): JSX.Element {
|
||||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
|
||||||
IntegrationManagers.sharedInstance().openAll();
|
|
||||||
} else {
|
|
||||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.props.showApps) return <div />;
|
if (!this.props.showApps) return <div />;
|
||||||
|
|
||||||
const apps = this.state.apps.map((app, index, arr) => {
|
const apps = this.state.apps.map((app, index, arr) => {
|
||||||
|
@ -257,7 +260,7 @@ export default class AppsDrawer extends React.Component {
|
||||||
className="mx_AppsContainer_resizer"
|
className="mx_AppsContainer_resizer"
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
>
|
>
|
||||||
<div className="mx_AppsContainer" ref={this._collectResizer}>
|
<div className="mx_AppsContainer" ref={this.collectResizer}>
|
||||||
{ apps.map((app, i) => {
|
{ apps.map((app, i) => {
|
||||||
if (i < 1) return app;
|
if (i < 1) return app;
|
||||||
return <React.Fragment key={app.key}>
|
return <React.Fragment key={app.key}>
|
||||||
|
@ -273,7 +276,18 @@ export default class AppsDrawer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PersistentVResizer = ({
|
interface IPersistentResizerProps {
|
||||||
|
room: Room;
|
||||||
|
minHeight: number;
|
||||||
|
maxHeight: number;
|
||||||
|
className: string;
|
||||||
|
handleWrapperClass: string;
|
||||||
|
handleClass: string;
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
||||||
room,
|
room,
|
||||||
minHeight,
|
minHeight,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
|
@ -303,7 +317,7 @@ const PersistentVResizer = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
return <Resizable
|
return <Resizable
|
||||||
size={{ height: Math.min(height, maxHeight) }}
|
size={{ height: Math.min(height, maxHeight), width: null }}
|
||||||
minHeight={minHeight}
|
minHeight={minHeight}
|
||||||
maxHeight={maxHeight}
|
maxHeight={maxHeight}
|
||||||
onResizeStart={() => {
|
onResizeStart={() => {
|
|
@ -50,7 +50,8 @@ import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
// matches emoticons which follow the start of a line or whitespace
|
// matches emoticons which follow the start of a line or whitespace
|
||||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
|
||||||
|
export const REGEX_EMOTICON = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')$');
|
||||||
|
|
||||||
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
||||||
|
|
||||||
|
@ -161,7 +162,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private replaceEmoticon = (caretPosition: DocumentPosition): number => {
|
public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number {
|
||||||
const { model } = this.props;
|
const { model } = this.props;
|
||||||
const range = model.startRange(caretPosition);
|
const range = model.startRange(caretPosition);
|
||||||
// expand range max 8 characters backwards from caretPosition,
|
// expand range max 8 characters backwards from caretPosition,
|
||||||
|
@ -170,9 +171,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
range.expandBackwardsWhile((index, offset) => {
|
range.expandBackwardsWhile((index, offset) => {
|
||||||
const part = model.parts[index];
|
const part = model.parts[index];
|
||||||
n -= 1;
|
n -= 1;
|
||||||
return n >= 0 && (part.type === Type.Plain || part.type === Type.PillCandidate);
|
return n >= 0 && [Type.Plain, Type.PillCandidate, Type.Newline].includes(part.type);
|
||||||
});
|
});
|
||||||
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
|
const emoticonMatch = regex.exec(range.text);
|
||||||
if (emoticonMatch) {
|
if (emoticonMatch) {
|
||||||
const query = emoticonMatch[1].replace("-", "");
|
const query = emoticonMatch[1].replace("-", "");
|
||||||
// try both exact match and lower-case, this means that xd won't match xD but :P will match :p
|
// try both exact match and lower-case, this means that xd won't match xD but :P will match :p
|
||||||
|
@ -180,18 +181,25 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const { partCreator } = model;
|
const { partCreator } = model;
|
||||||
const hasPrecedingSpace = emoticonMatch[0][0] === " ";
|
const firstMatch = emoticonMatch[0];
|
||||||
|
const moveStart = firstMatch[0] === " " ? 1 : 0;
|
||||||
|
|
||||||
// we need the range to only comprise of the emoticon
|
// we need the range to only comprise of the emoticon
|
||||||
// because we'll replace the whole range with an emoji,
|
// because we'll replace the whole range with an emoji,
|
||||||
// so move the start forward to the start of the emoticon.
|
// so move the start forward to the start of the emoticon.
|
||||||
// Take + 1 because index is reported without the possible preceding space.
|
// Take + 1 because index is reported without the possible preceding space.
|
||||||
range.moveStart(emoticonMatch.index + (hasPrecedingSpace ? 1 : 0));
|
range.moveStartForwards(emoticonMatch.index + moveStart);
|
||||||
|
// If the end is a trailing space/newline move end backwards, so that we don't replace it
|
||||||
|
if (["\n", " "].includes(firstMatch[firstMatch.length - 1])) {
|
||||||
|
range.moveEndBackwards(1);
|
||||||
|
}
|
||||||
|
|
||||||
// this returns the amount of added/removed characters during the replace
|
// this returns the amount of added/removed characters during the replace
|
||||||
// so the caret position can be adjusted.
|
// so the caret position can be adjusted.
|
||||||
return range.replace([partCreator.plain(data.unicode + " ")]);
|
return range.replace([partCreator.plain(data.unicode)]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
|
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
|
||||||
renderModel(this.editorRef.current, this.props.model);
|
renderModel(this.editorRef.current, this.props.model);
|
||||||
|
@ -607,8 +615,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
};
|
};
|
||||||
|
|
||||||
private configureEmoticonAutoReplace = (): void => {
|
private configureEmoticonAutoReplace = (): void => {
|
||||||
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
|
this.props.model.setTransformCallback(this.transform);
|
||||||
this.props.model.setTransformCallback(shouldReplace ? this.replaceEmoticon : null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private configureShouldShowPillAvatar = (): void => {
|
private configureShouldShowPillAvatar = (): void => {
|
||||||
|
@ -621,6 +628,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
this.setState({ surroundWith });
|
this.setState({ surroundWith });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private transform = (documentPosition: DocumentPosition): void => {
|
||||||
|
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
|
||||||
|
if (shouldReplace) this.replaceEmoticon(documentPosition, REGEX_EMOTICON_WHITESPACE);
|
||||||
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener("selectionchange", this.onSelectionChange);
|
document.removeEventListener("selectionchange", this.onSelectionChange);
|
||||||
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
||||||
|
|
|
@ -16,41 +16,51 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import Tooltip from "../elements/Tooltip";
|
import Tooltip from "../elements/Tooltip";
|
||||||
|
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||||
|
|
||||||
export const E2E_STATE = {
|
export enum E2EState {
|
||||||
VERIFIED: "verified",
|
Verified = "verified",
|
||||||
WARNING: "warning",
|
Warning = "warning",
|
||||||
UNKNOWN: "unknown",
|
Unknown = "unknown",
|
||||||
NORMAL: "normal",
|
Normal = "normal",
|
||||||
UNAUTHENTICATED: "unauthenticated",
|
Unauthenticated = "unauthenticated",
|
||||||
|
}
|
||||||
|
|
||||||
|
const crossSigningUserTitles: { [key in E2EState]?: string } = {
|
||||||
|
[E2EState.Warning]: _td("This user has not verified all of their sessions."),
|
||||||
|
[E2EState.Normal]: _td("You have not verified this user."),
|
||||||
|
[E2EState.Verified]: _td("You have verified this user. This user has verified all of their sessions."),
|
||||||
|
};
|
||||||
|
const crossSigningRoomTitles: { [key in E2EState]?: string } = {
|
||||||
|
[E2EState.Warning]: _td("Someone is using an unknown session"),
|
||||||
|
[E2EState.Normal]: _td("This room is end-to-end encrypted"),
|
||||||
|
[E2EState.Verified]: _td("Everyone in this room is verified"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const crossSigningUserTitles = {
|
interface IProps {
|
||||||
[E2E_STATE.WARNING]: _td("This user has not verified all of their sessions."),
|
isUser?: boolean;
|
||||||
[E2E_STATE.NORMAL]: _td("You have not verified this user."),
|
status?: E2EState | E2EStatus;
|
||||||
[E2E_STATE.VERIFIED]: _td("You have verified this user. This user has verified all of their sessions."),
|
className?: string;
|
||||||
};
|
size?: number;
|
||||||
const crossSigningRoomTitles = {
|
onClick?: () => void;
|
||||||
[E2E_STATE.WARNING]: _td("Someone is using an unknown session"),
|
hideTooltip?: boolean;
|
||||||
[E2E_STATE.NORMAL]: _td("This room is end-to-end encrypted"),
|
bordered?: boolean;
|
||||||
[E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"),
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const E2EIcon = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
|
const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
|
||||||
const [hover, setHover] = useState(false);
|
const [hover, setHover] = useState(false);
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
mx_E2EIcon: true,
|
mx_E2EIcon: true,
|
||||||
mx_E2EIcon_bordered: bordered,
|
mx_E2EIcon_bordered: bordered,
|
||||||
mx_E2EIcon_warning: status === E2E_STATE.WARNING,
|
mx_E2EIcon_warning: status === E2EState.Warning,
|
||||||
mx_E2EIcon_normal: status === E2E_STATE.NORMAL,
|
mx_E2EIcon_normal: status === E2EState.Normal,
|
||||||
mx_E2EIcon_verified: status === E2E_STATE.VERIFIED,
|
mx_E2EIcon_verified: status === E2EState.Verified,
|
||||||
}, className);
|
}, className);
|
||||||
|
|
||||||
let e2eTitle;
|
let e2eTitle;
|
||||||
|
@ -92,12 +102,4 @@ const E2EIcon = ({ isUser, status, className, size, onClick, hideTooltip, border
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
E2EIcon.propTypes = {
|
|
||||||
isUser: PropTypes.bool,
|
|
||||||
status: PropTypes.oneOf(Object.values(E2E_STATE)),
|
|
||||||
className: PropTypes.string,
|
|
||||||
size: PropTypes.number,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default E2EIcon;
|
export default E2EIcon;
|
|
@ -27,7 +27,7 @@ import { findEditableEvent } from '../../../utils/EventUtils';
|
||||||
import { parseEvent } from '../../../editor/deserialize';
|
import { parseEvent } from '../../../editor/deserialize';
|
||||||
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
||||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||||
import BasicMessageComposer from "./BasicMessageComposer";
|
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
@ -42,6 +42,7 @@ import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||||
const html = mxEvent.getContent().formatted_body;
|
const html = mxEvent.getContent().formatted_body;
|
||||||
|
@ -315,6 +316,14 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
private sendEdit = async (): Promise<void> => {
|
private sendEdit = async (): Promise<void> => {
|
||||||
const startTime = CountlyAnalytics.getTimestamp();
|
const startTime = CountlyAnalytics.getTimestamp();
|
||||||
const editedEvent = this.props.editState.getEvent();
|
const editedEvent = this.props.editState.getEvent();
|
||||||
|
|
||||||
|
// Replace emoticon at the end of the message
|
||||||
|
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||||
|
const caret = this.editorRef.current?.getCaret();
|
||||||
|
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||||
|
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||||
|
}
|
||||||
|
|
||||||
const editContent = createEditContent(this.model, editedEvent);
|
const editContent = createEditContent(this.model, editedEvent);
|
||||||
const newContent = editContent["m.new_content"];
|
const newContent = editContent["m.new_content"];
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import React from 'react';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { _td } from '../../../languageHandler';
|
import { _td } from '../../../languageHandler';
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon, { E2EState } from './E2EIcon';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import BaseAvatar from '../avatars/BaseAvatar';
|
import BaseAvatar from '../avatars/BaseAvatar';
|
||||||
import PresenceLabel from "./PresenceLabel";
|
import PresenceLabel from "./PresenceLabel";
|
||||||
|
@ -75,7 +75,7 @@ interface IProps {
|
||||||
suppressOnHover?: boolean;
|
suppressOnHover?: boolean;
|
||||||
showPresence?: boolean;
|
showPresence?: boolean;
|
||||||
subtextLabel?: string;
|
subtextLabel?: string;
|
||||||
e2eStatus?: string;
|
e2eStatus?: E2EState;
|
||||||
powerStatus?: PowerStatus;
|
powerStatus?: PowerStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { Thread } from 'matrix-js-sdk/src/models/thread';
|
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||||
|
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -33,7 +33,7 @@ import { formatTime } from "../../../DateUtils";
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { ALL_RULE_TYPES } from "../../../mjolnir/BanList";
|
import { ALL_RULE_TYPES } from "../../../mjolnir/BanList";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { E2E_STATE } from "./E2EIcon";
|
import { E2EState } from "./E2EIcon";
|
||||||
import { toRem } from "../../../utils/units";
|
import { toRem } from "../../../utils/units";
|
||||||
import { WidgetType } from "../../../widgets/WidgetType";
|
import { WidgetType } from "../../../widgets/WidgetType";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
@ -464,8 +464,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_thread")) {
|
if (SettingsStore.getValue("feature_thread")) {
|
||||||
this.props.mxEvent.once("Thread.ready", this.updateThread);
|
this.props.mxEvent.once(ThreadEvent.Ready, this.updateThread);
|
||||||
this.props.mxEvent.on("Thread.update", this.updateThread);
|
this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -521,7 +521,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const thread = this.state.thread;
|
const thread = this.state.thread;
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||||
if (!thread || this.props.showThreadInfo === false) {
|
if (!thread || this.props.showThreadInfo === false || thread.length <= 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,7 +605,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
if (encryptionInfo.mismatchedSender) {
|
if (encryptionInfo.mismatchedSender) {
|
||||||
// something definitely wrong is going on here
|
// something definitely wrong is going on here
|
||||||
this.setState({
|
this.setState({
|
||||||
verified: E2E_STATE.WARNING,
|
verified: E2EState.Warning,
|
||||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -613,7 +613,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
if (!userTrust.isCrossSigningVerified()) {
|
if (!userTrust.isCrossSigningVerified()) {
|
||||||
// user is not verified, so default to everything is normal
|
// user is not verified, so default to everything is normal
|
||||||
this.setState({
|
this.setState({
|
||||||
verified: E2E_STATE.NORMAL,
|
verified: E2EState.Normal,
|
||||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -623,27 +623,27 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
if (!eventSenderTrust) {
|
if (!eventSenderTrust) {
|
||||||
this.setState({
|
this.setState({
|
||||||
verified: E2E_STATE.UNKNOWN,
|
verified: E2EState.Unknown,
|
||||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!eventSenderTrust.isVerified()) {
|
if (!eventSenderTrust.isVerified()) {
|
||||||
this.setState({
|
this.setState({
|
||||||
verified: E2E_STATE.WARNING,
|
verified: E2EState.Warning,
|
||||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!encryptionInfo.authenticated) {
|
if (!encryptionInfo.authenticated) {
|
||||||
this.setState({
|
this.setState({
|
||||||
verified: E2E_STATE.UNAUTHENTICATED,
|
verified: E2EState.Unauthenticated,
|
||||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
verified: E2E_STATE.VERIFIED,
|
verified: E2EState.Verified,
|
||||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -850,13 +850,13 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// event is encrypted, display padlock corresponding to whether or not it is verified
|
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||||
if (ev.isEncrypted()) {
|
if (ev.isEncrypted()) {
|
||||||
if (this.state.verified === E2E_STATE.NORMAL) {
|
if (this.state.verified === E2EState.Normal) {
|
||||||
return; // no icon if we've not even cross-signed the user
|
return; // no icon if we've not even cross-signed the user
|
||||||
} else if (this.state.verified === E2E_STATE.VERIFIED) {
|
} else if (this.state.verified === E2EState.Verified) {
|
||||||
return; // no icon for verified
|
return; // no icon for verified
|
||||||
} else if (this.state.verified === E2E_STATE.UNAUTHENTICATED) {
|
} else if (this.state.verified === E2EState.Unauthenticated) {
|
||||||
return (<E2ePadlockUnauthenticated />);
|
return (<E2ePadlockUnauthenticated />);
|
||||||
} else if (this.state.verified === E2E_STATE.UNKNOWN) {
|
} else if (this.state.verified === E2EState.Unknown) {
|
||||||
return (<E2ePadlockUnknown />);
|
return (<E2ePadlockUnknown />);
|
||||||
} else {
|
} else {
|
||||||
return (<E2ePadlockUnverified />);
|
return (<E2ePadlockUnverified />);
|
||||||
|
@ -961,9 +961,9 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
mx_EventTile_lastInSection: this.props.lastInSection,
|
mx_EventTile_lastInSection: this.props.lastInSection,
|
||||||
mx_EventTile_contextual: this.props.contextual,
|
mx_EventTile_contextual: this.props.contextual,
|
||||||
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
||||||
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED,
|
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2EState.Verified,
|
||||||
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2E_STATE.WARNING,
|
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2EState.Warning,
|
||||||
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
|
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2EState.Unknown,
|
||||||
mx_EventTile_bad: isEncryptionFailure,
|
mx_EventTile_bad: isEncryptionFailure,
|
||||||
mx_EventTile_emote: msgtype === 'm.emote',
|
mx_EventTile_emote: msgtype === 'm.emote',
|
||||||
mx_EventTile_noSender: this.props.hideSender,
|
mx_EventTile_noSender: this.props.hideSender,
|
||||||
|
@ -1192,7 +1192,11 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
const thread = ReplyThread.makeThread(
|
let thread;
|
||||||
|
// When the "showHiddenEventsInTimeline" lab is enabled,
|
||||||
|
// avoid showing replies for hidden events (events without tiles)
|
||||||
|
if (haveTileForEvent(this.props.mxEvent)) {
|
||||||
|
thread = ReplyThread.makeThread(
|
||||||
this.props.mxEvent,
|
this.props.mxEvent,
|
||||||
this.props.onHeightChanged,
|
this.props.onHeightChanged,
|
||||||
this.props.permalinkCreator,
|
this.props.permalinkCreator,
|
||||||
|
@ -1200,6 +1204,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
this.props.layout,
|
this.props.layout,
|
||||||
this.props.alwaysShowTimestamps || this.state.hover,
|
this.props.alwaysShowTimestamps || this.state.hover,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
|
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
useContextMenu,
|
useContextMenu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
AboveLeftOf,
|
||||||
} from "../../structures/ContextMenu";
|
} from "../../structures/ContextMenu";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import ReplyPreview from "./ReplyPreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
|
@ -56,7 +57,7 @@ let instanceCount = 0;
|
||||||
const NARROW_MODE_BREAKPOINT = 500;
|
const NARROW_MODE_BREAKPOINT = 500;
|
||||||
|
|
||||||
interface IComposerAvatarProps {
|
interface IComposerAvatarProps {
|
||||||
me: object;
|
me: RoomMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposerAvatar(props: IComposerAvatarProps) {
|
function ComposerAvatar(props: IComposerAvatarProps) {
|
||||||
|
@ -511,7 +512,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
null,
|
null,
|
||||||
];
|
];
|
||||||
|
|
||||||
let menuPosition;
|
let menuPosition: AboveLeftOf | undefined;
|
||||||
if (this.ref.current) {
|
if (this.ref.current) {
|
||||||
const contentRect = this.ref.current.getBoundingClientRect();
|
const contentRect = this.ref.current.getBoundingClientRect();
|
||||||
menuPosition = aboveLeftOf(contentRect);
|
menuPosition = aboveLeftOf(contentRect);
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { MouseEvent } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { formatCount } from "../../../utils/FormattingUtils";
|
import { formatCount } from "../../../utils/FormattingUtils";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
@ -22,6 +22,9 @@ import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import { XOR } from "../../../@types/common";
|
import { XOR } from "../../../@types/common";
|
||||||
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
|
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Tooltip from "../elements/Tooltip";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
notification: NotificationState;
|
notification: NotificationState;
|
||||||
|
@ -39,6 +42,7 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IClickableProps extends IProps, React.InputHTMLAttributes<Element> {
|
interface IClickableProps extends IProps, React.InputHTMLAttributes<Element> {
|
||||||
|
showUnsentTooltip?: boolean;
|
||||||
/**
|
/**
|
||||||
* If specified will return an AccessibleButton instead of a div.
|
* If specified will return an AccessibleButton instead of a div.
|
||||||
*/
|
*/
|
||||||
|
@ -47,6 +51,7 @@ interface IClickableProps extends IProps, React.InputHTMLAttributes<Element> {
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
|
showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
|
||||||
|
showTooltip: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.NotificationBadge")
|
@replaceableComponent("views.rooms.NotificationBadge")
|
||||||
|
@ -59,6 +64,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId),
|
showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId),
|
||||||
|
showTooltip: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.countWatcherRef = SettingsStore.watchSetting(
|
this.countWatcherRef = SettingsStore.watchSetting(
|
||||||
|
@ -93,9 +99,22 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
||||||
this.forceUpdate(); // notification state changed - update
|
this.forceUpdate(); // notification state changed - update
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onMouseOver = (e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setState({
|
||||||
|
showTooltip: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMouseLeave = () => {
|
||||||
|
this.setState({
|
||||||
|
showTooltip: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
public render(): React.ReactElement {
|
public render(): React.ReactElement {
|
||||||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||||
const { notification, forceCount, roomId, onClick, ...props } = this.props;
|
const { notification, showUnsentTooltip, forceCount, roomId, onClick, ...props } = this.props;
|
||||||
|
|
||||||
// Don't show a badge if we don't need to
|
// Don't show a badge if we don't need to
|
||||||
if (notification.isIdle) return null;
|
if (notification.isIdle) return null;
|
||||||
|
@ -124,9 +143,24 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
|
let label: string;
|
||||||
|
let tooltip: JSX.Element;
|
||||||
|
if (showUnsentTooltip && this.state.showTooltip && notification.color === NotificationColor.Unsent) {
|
||||||
|
label = _t("Message didn't send. Click for info.");
|
||||||
|
tooltip = <Tooltip className="mx_RoleButton_tooltip" label={label} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleButton {...props} className={classes} onClick={onClick}>
|
<AccessibleButton
|
||||||
|
aria-label={label}
|
||||||
|
{...props}
|
||||||
|
className={classes}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseOver={this.onMouseOver}
|
||||||
|
onMouseLeave={this.onMouseLeave}
|
||||||
|
>
|
||||||
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
||||||
|
{ tooltip }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -547,7 +547,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
|
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
|
||||||
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
|
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
|
||||||
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
|
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
|
||||||
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1 && unfilteredFavourite < 1) {
|
if (unfilteredRooms.length < 1 && unfilteredHistorical.length < 1 && unfilteredFavourite.length < 1) {
|
||||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||||
<div>{ _t("Use the + to make a new room or explore existing ones below") }</div>
|
<div>{ _t("Use the + to make a new room or explore existing ones below") }</div>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
|
|
@ -14,8 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import PropTypes from 'prop-types';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
|
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
import { IJoinRuleEventContent, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
@ -27,91 +32,102 @@ import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import InviteReason from "../elements/InviteReason";
|
import InviteReason from "../elements/InviteReason";
|
||||||
|
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
|
||||||
const MemberEventHtmlReasonField = "io.element.html_reason";
|
const MemberEventHtmlReasonField = "io.element.html_reason";
|
||||||
|
|
||||||
const MessageCase = Object.freeze({
|
enum MessageCase {
|
||||||
NotLoggedIn: "NotLoggedIn",
|
NotLoggedIn = "NotLoggedIn",
|
||||||
Joining: "Joining",
|
Joining = "Joining",
|
||||||
Loading: "Loading",
|
Loading = "Loading",
|
||||||
Rejecting: "Rejecting",
|
Rejecting = "Rejecting",
|
||||||
Kicked: "Kicked",
|
Kicked = "Kicked",
|
||||||
Banned: "Banned",
|
Banned = "Banned",
|
||||||
OtherThreePIDError: "OtherThreePIDError",
|
OtherThreePIDError = "OtherThreePIDError",
|
||||||
InvitedEmailNotFoundInAccount: "InvitedEmailNotFoundInAccount",
|
InvitedEmailNotFoundInAccount = "InvitedEmailNotFoundInAccount",
|
||||||
InvitedEmailNoIdentityServer: "InvitedEmailNoIdentityServer",
|
InvitedEmailNoIdentityServer = "InvitedEmailNoIdentityServer",
|
||||||
InvitedEmailMismatch: "InvitedEmailMismatch",
|
InvitedEmailMismatch = "InvitedEmailMismatch",
|
||||||
Invite: "Invite",
|
Invite = "Invite",
|
||||||
ViewingRoom: "ViewingRoom",
|
ViewingRoom = "ViewingRoom",
|
||||||
RoomNotFound: "RoomNotFound",
|
RoomNotFound = "RoomNotFound",
|
||||||
OtherError: "OtherError",
|
OtherError = "OtherError",
|
||||||
});
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.RoomPreviewBar")
|
interface IProps {
|
||||||
export default class RoomPreviewBar extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onJoinClick: PropTypes.func,
|
|
||||||
onRejectClick: PropTypes.func,
|
|
||||||
onRejectAndIgnoreClick: PropTypes.func,
|
|
||||||
onForgetClick: PropTypes.func,
|
|
||||||
// if inviterName is specified, the preview bar will shown an invite to the room.
|
// if inviterName is specified, the preview bar will shown an invite to the room.
|
||||||
// You should also specify onRejectClick if specifiying inviterName
|
// You should also specify onRejectClick if specifying inviterName
|
||||||
inviterName: PropTypes.string,
|
inviterName?: string;
|
||||||
|
|
||||||
// If invited by 3rd party invite, the email address the invite was sent to
|
// If invited by 3rd party invite, the email address the invite was sent to
|
||||||
invitedEmail: PropTypes.string,
|
invitedEmail?: string;
|
||||||
|
|
||||||
// For third party invites, information passed about the room out-of-band
|
// For third party invites, information passed about the room out-of-band
|
||||||
oobData: PropTypes.object,
|
oobData?: IOOBData;
|
||||||
|
|
||||||
// For third party invites, a URL for a 3pid invite signing service
|
// For third party invites, a URL for a 3pid invite signing service
|
||||||
signUrl: PropTypes.string,
|
signUrl?: string;
|
||||||
|
|
||||||
// A standard client/server API error object. If supplied, indicates that the
|
// A standard client/server API error object. If supplied, indicates that the
|
||||||
// caller was unable to fetch details about the room for the given reason.
|
// caller was unable to fetch details about the room for the given reason.
|
||||||
error: PropTypes.object,
|
error?: MatrixError;
|
||||||
|
|
||||||
canPreview: PropTypes.bool,
|
canPreview?: boolean;
|
||||||
previewLoading: PropTypes.bool,
|
previewLoading?: boolean;
|
||||||
room: PropTypes.object,
|
room?: Room;
|
||||||
|
|
||||||
// When a spinner is present, a spinnerState can be specified to indicate the
|
loading?: boolean;
|
||||||
// purpose of the spinner.
|
joining?: boolean;
|
||||||
spinner: PropTypes.bool,
|
rejecting?: boolean;
|
||||||
spinnerState: PropTypes.oneOf(["joining"]),
|
|
||||||
loading: PropTypes.bool,
|
|
||||||
joining: PropTypes.bool,
|
|
||||||
rejecting: PropTypes.bool,
|
|
||||||
// The alias that was used to access this room, if appropriate
|
// The alias that was used to access this room, if appropriate
|
||||||
// If given, this will be how the room is referred to (eg.
|
// If given, this will be how the room is referred to (eg.
|
||||||
// in error messages).
|
// in error messages).
|
||||||
roomAlias: PropTypes.string,
|
roomAlias?: string;
|
||||||
};
|
|
||||||
|
|
||||||
|
onJoinClick?(): void;
|
||||||
|
onRejectClick?(): void;
|
||||||
|
onRejectAndIgnoreClick?(): void;
|
||||||
|
onForgetClick?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
busy: boolean;
|
||||||
|
accountEmails?: string[];
|
||||||
|
invitedEmailMxid?: string;
|
||||||
|
threePidFetchError?: MatrixError;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.rooms.RoomPreviewBar")
|
||||||
|
export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onJoinClick() {},
|
onJoinClick() {},
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
busy: false,
|
busy: false,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._checkInvitedEmail();
|
this.checkInvitedEmail();
|
||||||
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this._onCommunityUpdate);
|
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (this.props.invitedEmail !== prevProps.invitedEmail || this.props.inviterName !== prevProps.inviterName) {
|
if (this.props.invitedEmail !== prevProps.invitedEmail || this.props.inviterName !== prevProps.inviterName) {
|
||||||
this._checkInvitedEmail();
|
this.checkInvitedEmail();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this._onCommunityUpdate);
|
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _checkInvitedEmail() {
|
private async checkInvitedEmail() {
|
||||||
// If this is an invite and we've been told what email address was
|
// If this is an invite and we've been told what email address was
|
||||||
// invited, fetch the user's account emails and discovery bindings so we
|
// invited, fetch the user's account emails and discovery bindings so we
|
||||||
// can check them against the email that was invited.
|
// can check them against the email that was invited.
|
||||||
|
@ -121,8 +137,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
// Gather the account 3PIDs
|
// Gather the account 3PIDs
|
||||||
const account3pids = await MatrixClientPeg.get().getThreePids();
|
const account3pids = await MatrixClientPeg.get().getThreePids();
|
||||||
this.setState({
|
this.setState({
|
||||||
accountEmails: account3pids.threepids
|
accountEmails: account3pids.threepids.filter(b => b.medium === 'email').map(b => b.address),
|
||||||
.filter(b => b.medium === 'email').map(b => b.address),
|
|
||||||
});
|
});
|
||||||
// If we have an IS connected, use that to lookup the email and
|
// If we have an IS connected, use that to lookup the email and
|
||||||
// check the bound MXID.
|
// check the bound MXID.
|
||||||
|
@ -146,21 +161,21 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCommunityUpdate = (roomId) => {
|
private onCommunityUpdate = (roomId: string): void => {
|
||||||
if (this.props.room && this.props.room.roomId !== roomId) {
|
if (this.props.room && this.props.room.roomId !== roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.forceUpdate(); // we have nothing to update
|
this.forceUpdate(); // we have nothing to update
|
||||||
};
|
};
|
||||||
|
|
||||||
_getMessageCase() {
|
private getMessageCase(): MessageCase {
|
||||||
const isGuest = MatrixClientPeg.get().isGuest();
|
const isGuest = MatrixClientPeg.get().isGuest();
|
||||||
|
|
||||||
if (isGuest) {
|
if (isGuest) {
|
||||||
return MessageCase.NotLoggedIn;
|
return MessageCase.NotLoggedIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
const myMember = this._getMyMember();
|
const myMember = this.getMyMember();
|
||||||
|
|
||||||
if (myMember) {
|
if (myMember) {
|
||||||
if (myMember.isKicked()) {
|
if (myMember.isKicked()) {
|
||||||
|
@ -195,7 +210,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
}
|
}
|
||||||
return MessageCase.Invite;
|
return MessageCase.Invite;
|
||||||
} else if (this.props.error) {
|
} else if (this.props.error) {
|
||||||
if (this.props.error.errcode == 'M_NOT_FOUND') {
|
if ((this.props.error as MatrixError).errcode == 'M_NOT_FOUND') {
|
||||||
return MessageCase.RoomNotFound;
|
return MessageCase.RoomNotFound;
|
||||||
} else {
|
} else {
|
||||||
return MessageCase.OtherError;
|
return MessageCase.OtherError;
|
||||||
|
@ -205,8 +220,8 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getKickOrBanInfo() {
|
private getKickOrBanInfo(): { memberName?: string, reason?: string } {
|
||||||
const myMember = this._getMyMember();
|
const myMember = this.getMyMember();
|
||||||
if (!myMember) {
|
if (!myMember) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -219,24 +234,19 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
return { memberName, reason };
|
return { memberName, reason };
|
||||||
}
|
}
|
||||||
|
|
||||||
_joinRule() {
|
private joinRule(): JoinRule {
|
||||||
const room = this.props.room;
|
return this.props.room?.currentState
|
||||||
if (room) {
|
.getStateEvents(EventType.RoomJoinRules, "")?.getContent<IJoinRuleEventContent>().join_rule;
|
||||||
const joinRules = room.currentState.getStateEvents('m.room.join_rules', '');
|
|
||||||
if (joinRules) {
|
|
||||||
return joinRules.getContent().join_rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_communityProfile() {
|
private communityProfile(): { displayName?: string, avatarMxc?: string } {
|
||||||
if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
|
if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
|
||||||
return { displayName: null, avatarMxc: null };
|
return { displayName: null, avatarMxc: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
_roomName(atStart = false) {
|
private roomName(atStart = false): string {
|
||||||
let name = this.props.room ? this.props.room.name : this.props.roomAlias;
|
let name = this.props.room ? this.props.room.name : this.props.roomAlias;
|
||||||
const profile = this._communityProfile();
|
const profile = this.communityProfile();
|
||||||
if (profile.displayName) name = profile.displayName;
|
if (profile.displayName) name = profile.displayName;
|
||||||
if (name) {
|
if (name) {
|
||||||
return name;
|
return name;
|
||||||
|
@ -247,14 +257,11 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getMyMember() {
|
private getMyMember(): RoomMember {
|
||||||
return (
|
return this.props.room?.getMember(MatrixClientPeg.get().getUserId());
|
||||||
this.props.room &&
|
|
||||||
this.props.room.getMember(MatrixClientPeg.get().getUserId())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_getInviteMember() {
|
private getInviteMember(): RoomMember {
|
||||||
const { room } = this.props;
|
const { room } = this.props;
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return;
|
return;
|
||||||
|
@ -268,8 +275,8 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
return room.currentState.getMember(inviterUserId);
|
return room.currentState.getMember(inviterUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_isDMInvite() {
|
private isDMInvite(): boolean {
|
||||||
const myMember = this._getMyMember();
|
const myMember = this.getMyMember();
|
||||||
if (!myMember) {
|
if (!myMember) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -278,7 +285,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
return memberContent.membership === "invite" && memberContent.is_direct;
|
return memberContent.membership === "invite" && memberContent.is_direct;
|
||||||
}
|
}
|
||||||
|
|
||||||
_makeScreenAfterLogin() {
|
private makeScreenAfterLogin(): { screen: string, params: Record<string, any> } {
|
||||||
return {
|
return {
|
||||||
screen: 'room',
|
screen: 'room',
|
||||||
params: {
|
params: {
|
||||||
|
@ -291,18 +298,16 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoginClick = () => {
|
private onLoginClick = () => {
|
||||||
dis.dispatch({ action: 'start_login', screenAfterLogin: this._makeScreenAfterLogin() });
|
dis.dispatch({ action: 'start_login', screenAfterLogin: this.makeScreenAfterLogin() });
|
||||||
};
|
};
|
||||||
|
|
||||||
onRegisterClick = () => {
|
private onRegisterClick = () => {
|
||||||
dis.dispatch({ action: 'start_registration', screenAfterLogin: this._makeScreenAfterLogin() });
|
dis.dispatch({ action: 'start_registration', screenAfterLogin: this.makeScreenAfterLogin() });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
|
|
||||||
let showSpinner = false;
|
let showSpinner = false;
|
||||||
let title;
|
let title;
|
||||||
|
@ -315,10 +320,10 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
let footer;
|
let footer;
|
||||||
const extraComponents = [];
|
const extraComponents = [];
|
||||||
|
|
||||||
const messageCase = this._getMessageCase();
|
const messageCase = this.getMessageCase();
|
||||||
switch (messageCase) {
|
switch (messageCase) {
|
||||||
case MessageCase.Joining: {
|
case MessageCase.Joining: {
|
||||||
title = _t("Joining room …");
|
title = this.props.oobData?.roomType === RoomType.Space ? _t("Joining space …") : _t("Joining room …");
|
||||||
showSpinner = true;
|
showSpinner = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -349,12 +354,12 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.Kicked: {
|
case MessageCase.Kicked: {
|
||||||
const { memberName, reason } = this._getKickOrBanInfo();
|
const { memberName, reason } = this.getKickOrBanInfo();
|
||||||
title = _t("You were kicked from %(roomName)s by %(memberName)s",
|
title = _t("You were kicked from %(roomName)s by %(memberName)s",
|
||||||
{ memberName, roomName: this._roomName() });
|
{ memberName, roomName: this.roomName() });
|
||||||
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
||||||
|
|
||||||
if (this._joinRule() === "invite") {
|
if (this.joinRule() === "invite") {
|
||||||
primaryActionLabel = _t("Forget this room");
|
primaryActionLabel = _t("Forget this room");
|
||||||
primaryActionHandler = this.props.onForgetClick;
|
primaryActionHandler = this.props.onForgetClick;
|
||||||
} else {
|
} else {
|
||||||
|
@ -366,9 +371,9 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.Banned: {
|
case MessageCase.Banned: {
|
||||||
const { memberName, reason } = this._getKickOrBanInfo();
|
const { memberName, reason } = this.getKickOrBanInfo();
|
||||||
title = _t("You were banned from %(roomName)s by %(memberName)s",
|
title = _t("You were banned from %(roomName)s by %(memberName)s",
|
||||||
{ memberName, roomName: this._roomName() });
|
{ memberName, roomName: this.roomName() });
|
||||||
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
||||||
primaryActionLabel = _t("Forget this room");
|
primaryActionLabel = _t("Forget this room");
|
||||||
primaryActionHandler = this.props.onForgetClick;
|
primaryActionHandler = this.props.onForgetClick;
|
||||||
|
@ -376,8 +381,8 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
}
|
}
|
||||||
case MessageCase.OtherThreePIDError: {
|
case MessageCase.OtherThreePIDError: {
|
||||||
title = _t("Something went wrong with your invite to %(roomName)s",
|
title = _t("Something went wrong with your invite to %(roomName)s",
|
||||||
{ roomName: this._roomName() });
|
{ roomName: this.roomName() });
|
||||||
const joinRule = this._joinRule();
|
const joinRule = this.joinRule();
|
||||||
const errCodeMessage = _t(
|
const errCodeMessage = _t(
|
||||||
"An error (%(errcode)s) was returned while trying to validate your " +
|
"An error (%(errcode)s) was returned while trying to validate your " +
|
||||||
"invite. You could try to pass this information on to a room admin.",
|
"invite. You could try to pass this information on to a room admin.",
|
||||||
|
@ -410,7 +415,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
"This invite to %(roomName)s was sent to %(email)s which is not " +
|
"This invite to %(roomName)s was sent to %(email)s which is not " +
|
||||||
"associated with your account",
|
"associated with your account",
|
||||||
{
|
{
|
||||||
roomName: this._roomName(),
|
roomName: this.roomName(),
|
||||||
email: this.props.invitedEmail,
|
email: this.props.invitedEmail,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -427,7 +432,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
title = _t(
|
title = _t(
|
||||||
"This invite to %(roomName)s was sent to %(email)s",
|
"This invite to %(roomName)s was sent to %(email)s",
|
||||||
{
|
{
|
||||||
roomName: this._roomName(),
|
roomName: this.roomName(),
|
||||||
email: this.props.invitedEmail,
|
email: this.props.invitedEmail,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -443,7 +448,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
title = _t(
|
title = _t(
|
||||||
"This invite to %(roomName)s was sent to %(email)s",
|
"This invite to %(roomName)s was sent to %(email)s",
|
||||||
{
|
{
|
||||||
roomName: this._roomName(),
|
roomName: this.roomName(),
|
||||||
email: this.props.invitedEmail,
|
email: this.props.invitedEmail,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -458,11 +463,11 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
case MessageCase.Invite: {
|
case MessageCase.Invite: {
|
||||||
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
|
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
|
||||||
const oobData = Object.assign({}, this.props.oobData, {
|
const oobData = Object.assign({}, this.props.oobData, {
|
||||||
avatarUrl: this._communityProfile().avatarMxc,
|
avatarUrl: this.communityProfile().avatarMxc,
|
||||||
});
|
});
|
||||||
const avatar = <RoomAvatar room={this.props.room} oobData={oobData} />;
|
const avatar = <RoomAvatar room={this.props.room} oobData={oobData} />;
|
||||||
|
|
||||||
const inviteMember = this._getInviteMember();
|
const inviteMember = this.getInviteMember();
|
||||||
let inviterElement;
|
let inviterElement;
|
||||||
if (inviteMember) {
|
if (inviteMember) {
|
||||||
inviterElement = <span>
|
inviterElement = <span>
|
||||||
|
@ -474,7 +479,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{ this.props.inviterName }</span>);
|
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{ this.props.inviterName }</span>);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDM = this._isDMInvite();
|
const isDM = this.isDMInvite();
|
||||||
if (isDM) {
|
if (isDM) {
|
||||||
title = _t("Do you want to chat with %(user)s?",
|
title = _t("Do you want to chat with %(user)s?",
|
||||||
{ user: inviteMember.name });
|
{ user: inviteMember.name });
|
||||||
|
@ -485,7 +490,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
primaryActionLabel = _t("Start chatting");
|
primaryActionLabel = _t("Start chatting");
|
||||||
} else {
|
} else {
|
||||||
title = _t("Do you want to join %(roomName)s?",
|
title = _t("Do you want to join %(roomName)s?",
|
||||||
{ roomName: this._roomName() });
|
{ roomName: this.roomName() });
|
||||||
subTitle = [
|
subTitle = [
|
||||||
avatar,
|
avatar,
|
||||||
_t("<userName/> invited you", {}, { userName: () => inviterElement }),
|
_t("<userName/> invited you", {}, { userName: () => inviterElement }),
|
||||||
|
@ -519,22 +524,22 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
case MessageCase.ViewingRoom: {
|
case MessageCase.ViewingRoom: {
|
||||||
if (this.props.canPreview) {
|
if (this.props.canPreview) {
|
||||||
title = _t("You're previewing %(roomName)s. Want to join it?",
|
title = _t("You're previewing %(roomName)s. Want to join it?",
|
||||||
{ roomName: this._roomName() });
|
{ roomName: this.roomName() });
|
||||||
} else {
|
} else {
|
||||||
title = _t("%(roomName)s can't be previewed. Do you want to join it?",
|
title = _t("%(roomName)s can't be previewed. Do you want to join it?",
|
||||||
{ roomName: this._roomName(true) });
|
{ roomName: this.roomName(true) });
|
||||||
}
|
}
|
||||||
primaryActionLabel = _t("Join the discussion");
|
primaryActionLabel = _t("Join the discussion");
|
||||||
primaryActionHandler = this.props.onJoinClick;
|
primaryActionHandler = this.props.onJoinClick;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.RoomNotFound: {
|
case MessageCase.RoomNotFound: {
|
||||||
title = _t("%(roomName)s does not exist.", { roomName: this._roomName(true) });
|
title = _t("%(roomName)s does not exist.", { roomName: this.roomName(true) });
|
||||||
subTitle = _t("This room doesn't exist. Are you sure you're at the right place?");
|
subTitle = _t("This room doesn't exist. Are you sure you're at the right place?");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.OtherError: {
|
case MessageCase.OtherError: {
|
||||||
title = _t("%(roomName)s is not accessible at this time.", { roomName: this._roomName(true) });
|
title = _t("%(roomName)s is not accessible at this time.", { roomName: this.roomName(true) });
|
||||||
subTitle = [
|
subTitle = [
|
||||||
_t("Try again later, or ask a room admin to check if you have access."),
|
_t("Try again later, or ask a room admin to check if you have access."),
|
||||||
_t(
|
_t(
|
|
@ -670,6 +670,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
onClick={this.onBadgeClick}
|
onClick={this.onBadgeClick}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
|
showUnsentTooltip={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { createRef } from "react";
|
import React, { createRef } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||||
|
@ -51,8 +50,6 @@ import IconizedContextMenu, {
|
||||||
} from "../context_menus/IconizedContextMenu";
|
} from "../context_menus/IconizedContextMenu";
|
||||||
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
|
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { getUnsentMessages } from "../../structures/RoomStatusBar";
|
|
||||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -68,7 +65,6 @@ interface IState {
|
||||||
notificationsMenuPosition: PartialDOMRect;
|
notificationsMenuPosition: PartialDOMRect;
|
||||||
generalMenuPosition: PartialDOMRect;
|
generalMenuPosition: PartialDOMRect;
|
||||||
messagePreview?: string;
|
messagePreview?: string;
|
||||||
hasUnsentEvents: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`;
|
const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`;
|
||||||
|
@ -95,7 +91,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
||||||
notificationsMenuPosition: null,
|
notificationsMenuPosition: null,
|
||||||
generalMenuPosition: null,
|
generalMenuPosition: null,
|
||||||
hasUnsentEvents: this.countUnsentEvents() > 0,
|
|
||||||
|
|
||||||
// generatePreview() will return nothing if the user has previews disabled
|
// generatePreview() will return nothing if the user has previews disabled
|
||||||
messagePreview: "",
|
messagePreview: "",
|
||||||
|
@ -106,11 +101,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||||
}
|
}
|
||||||
|
|
||||||
private countUnsentEvents(): number {
|
private onRoomNameUpdate = (room: Room) => {
|
||||||
return getUnsentMessages(this.props.room).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRoomNameUpdate = (room) => {
|
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -118,11 +109,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
this.forceUpdate(); // notification state changed - update
|
this.forceUpdate(); // notification state changed - update
|
||||||
};
|
};
|
||||||
|
|
||||||
private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
|
|
||||||
if (room?.roomId !== this.props.room.roomId) return;
|
|
||||||
this.setState({ hasUnsentEvents: this.countUnsentEvents() > 0 });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRoomPropertyUpdate = (property: CachedRoomKey) => {
|
private onRoomPropertyUpdate = (property: CachedRoomKey) => {
|
||||||
if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate();
|
if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate();
|
||||||
// else ignore - not important for this tile
|
// else ignore - not important for this tile
|
||||||
|
@ -178,12 +164,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
);
|
);
|
||||||
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||||
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||||
this.roomProps.on("Room.name", this.onRoomNameUpdate);
|
this.props.room?.on("Room.name", this.onRoomNameUpdate);
|
||||||
CommunityPrototypeStore.instance.on(
|
CommunityPrototypeStore.instance.on(
|
||||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||||
this.onCommunityUpdate,
|
this.onCommunityUpdate,
|
||||||
);
|
);
|
||||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -208,7 +193,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||||
this.onCommunityUpdate,
|
this.onCommunityUpdate,
|
||||||
);
|
);
|
||||||
MatrixClientPeg.get()?.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
private onAction = (payload: ActionPayload) => {
|
||||||
|
@ -587,20 +571,8 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
let badge: React.ReactNode;
|
let badge: React.ReactNode;
|
||||||
if (!this.props.isMinimized) {
|
if (!this.props.isMinimized && this.notificationState) {
|
||||||
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
|
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
|
||||||
if (this.state.hasUnsentEvents) {
|
|
||||||
// hardcode the badge to a danger state when there's unsent messages
|
|
||||||
badge = (
|
|
||||||
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
|
|
||||||
<NotificationBadge
|
|
||||||
notification={StaticNotificationState.RED_EXCLAMATION}
|
|
||||||
forceCount={false}
|
|
||||||
roomId={this.props.room.roomId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (this.notificationState) {
|
|
||||||
badge = (
|
badge = (
|
||||||
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
|
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
|
||||||
<NotificationBadge
|
<NotificationBadge
|
||||||
|
@ -611,7 +583,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let messagePreview = null;
|
let messagePreview = null;
|
||||||
if (this.showMessagePreview && this.state.messagePreview) {
|
if (this.showMessagePreview && this.state.messagePreview) {
|
||||||
|
|
|
@ -31,8 +31,8 @@ import {
|
||||||
textSerialize,
|
textSerialize,
|
||||||
unescapeMessage,
|
unescapeMessage,
|
||||||
} from '../../../editor/serialize';
|
} from '../../../editor/serialize';
|
||||||
|
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||||
import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
|
import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
|
||||||
import BasicMessageComposer from "./BasicMessageComposer";
|
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import { findEditableEvent } from '../../../utils/EventUtils';
|
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||||
import SendHistoryManager from "../../../SendHistoryManager";
|
import SendHistoryManager from "../../../SendHistoryManager";
|
||||||
|
@ -347,15 +347,24 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendMessage(): Promise<void> {
|
public async sendMessage(): Promise<void> {
|
||||||
if (this.model.isEmpty) {
|
const model = this.model;
|
||||||
|
|
||||||
|
if (model.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace emoticon at the end of the message
|
||||||
|
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||||
|
const caret = this.editorRef.current?.getCaret();
|
||||||
|
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||||
|
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||||
|
}
|
||||||
|
|
||||||
const replyToEvent = this.props.replyToEvent;
|
const replyToEvent = this.props.replyToEvent;
|
||||||
let shouldSend = true;
|
let shouldSend = true;
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
if (!containsEmote(this.model) && this.isSlashCommand()) {
|
if (!containsEmote(model) && this.isSlashCommand()) {
|
||||||
const [cmd, args, commandText] = this.getSlashCommand();
|
const [cmd, args, commandText] = this.getSlashCommand();
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
if (cmd.category === CommandCategories.messages) {
|
if (cmd.category === CommandCategories.messages) {
|
||||||
|
@ -400,7 +409,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isQuickReaction(this.model)) {
|
if (isQuickReaction(model)) {
|
||||||
shouldSend = false;
|
shouldSend = false;
|
||||||
this.sendQuickReaction();
|
this.sendQuickReaction();
|
||||||
}
|
}
|
||||||
|
@ -410,7 +419,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
const { roomId } = this.props.room;
|
const { roomId } = this.props.room;
|
||||||
if (!content) {
|
if (!content) {
|
||||||
content = createMessageContent(
|
content = createMessageContent(
|
||||||
this.model,
|
model,
|
||||||
replyToEvent,
|
replyToEvent,
|
||||||
this.props.replyInThread,
|
this.props.replyInThread,
|
||||||
this.props.permalinkCreator,
|
this.props.permalinkCreator,
|
||||||
|
@ -446,9 +455,9 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendHistoryManager.save(this.model, replyToEvent);
|
this.sendHistoryManager.save(model, replyToEvent);
|
||||||
// clear composer
|
// clear composer
|
||||||
this.model.reset([]);
|
model.reset([]);
|
||||||
this.editorRef.current?.clearUndoHistory();
|
this.editorRef.current?.clearUndoHistory();
|
||||||
this.editorRef.current?.focus();
|
this.editorRef.current?.focus();
|
||||||
this.clearStoredEditorState();
|
this.clearStoredEditorState();
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
|
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
|
||||||
|
import { IApp } from "../../../stores/WidgetStore";
|
||||||
|
|
||||||
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
|
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
|
||||||
// We sit in a context menu, so this should be given to the context menu.
|
// We sit in a context menu, so this should be given to the context menu.
|
||||||
|
@ -256,12 +257,16 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack");
|
stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack");
|
||||||
|
|
||||||
// FIXME: could this use the same code as other apps?
|
// FIXME: could this use the same code as other apps?
|
||||||
const stickerApp = {
|
const stickerApp: IApp = {
|
||||||
id: stickerpickerWidget.id,
|
id: stickerpickerWidget.id,
|
||||||
url: stickerpickerWidget.content.url,
|
url: stickerpickerWidget.content.url,
|
||||||
name: stickerpickerWidget.content.name,
|
name: stickerpickerWidget.content.name,
|
||||||
type: stickerpickerWidget.content.type,
|
type: stickerpickerWidget.content.type,
|
||||||
data: stickerpickerWidget.content.data,
|
data: stickerpickerWidget.content.data,
|
||||||
|
roomId: stickerpickerWidget.content.roomId,
|
||||||
|
eventId: stickerpickerWidget.content.eventId,
|
||||||
|
avatar_url: stickerpickerWidget.content.avatar_url,
|
||||||
|
creatorUserId: stickerpickerWidget.content.creatorUserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
stickersContent = (
|
stickersContent = (
|
||||||
|
@ -287,9 +292,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
onEditClick={this.launchManageIntegrations}
|
onEditClick={this.launchManageIntegrations}
|
||||||
onDeleteClick={this.removeStickerpickerWidgets}
|
onDeleteClick={this.removeStickerpickerWidgets}
|
||||||
showTitle={false}
|
showTitle={false}
|
||||||
showCancel={false}
|
|
||||||
showPopout={false}
|
showPopout={false}
|
||||||
onMinimiseClick={this.onHideStickersClick}
|
|
||||||
handleMinimisePointerEvents={true}
|
handleMinimisePointerEvents={true}
|
||||||
userWidget={true}
|
userWidget={true}
|
||||||
/>
|
/>
|
||||||
|
@ -345,16 +348,6 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Trigger hiding of the sticker picker overlay
|
|
||||||
* @param {Event} ev Event that triggered the function call
|
|
||||||
*/
|
|
||||||
private onHideStickersClick = (ev: React.MouseEvent): void => {
|
|
||||||
if (this.props.showStickers) {
|
|
||||||
this.props.setShowStickers(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the window is resized
|
* Called when the window is resized
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
|
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
|
||||||
import RoomUpgradeWarningDialog from "../dialogs/RoomUpgradeWarningDialog";
|
import RoomUpgradeWarningDialog from "../dialogs/RoomUpgradeWarningDialog";
|
||||||
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
import { upgradeRoom } from "../../../utils/RoomUpgrade";
|
import { upgradeRoom } from "../../../utils/RoomUpgrade";
|
||||||
import { arrayHasDiff } from "../../../utils/arrays";
|
import { arrayHasDiff } from "../../../utils/arrays";
|
||||||
import { useLocalEcho } from "../../../hooks/useLocalEcho";
|
import { useLocalEcho } from "../../../hooks/useLocalEcho";
|
||||||
|
@ -207,13 +208,37 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
|
||||||
} else if (preferredRestrictionVersion) {
|
} else if (preferredRestrictionVersion) {
|
||||||
// Block this action on a room upgrade otherwise it'd make their room unjoinable
|
// Block this action on a room upgrade otherwise it'd make their room unjoinable
|
||||||
const targetVersion = preferredRestrictionVersion;
|
const targetVersion = preferredRestrictionVersion;
|
||||||
Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
|
|
||||||
|
const modal = Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
targetVersion,
|
targetVersion,
|
||||||
description: _t("This upgrade will allow members of selected spaces " +
|
description: _t("This upgrade will allow members of selected spaces " +
|
||||||
"access to this room without an invite."),
|
"access to this room without an invite."),
|
||||||
onFinished: async (resp) => {
|
});
|
||||||
|
|
||||||
|
const [resp] = await modal.finished;
|
||||||
if (!resp?.continue) return;
|
if (!resp?.continue) return;
|
||||||
|
|
||||||
|
const userId = cli.getUserId();
|
||||||
|
const unableToUpdateSomeParents = Array.from(SpaceStore.instance.getKnownParents(room.roomId))
|
||||||
|
.some(roomId => !cli.getRoom(roomId)?.currentState.maySendStateEvent(EventType.SpaceChild, userId));
|
||||||
|
if (unableToUpdateSomeParents) {
|
||||||
|
const modal = Modal.createTrackedDialog<[boolean]>('Parent relink warning', '', QuestionDialog, {
|
||||||
|
title: _t("Before you upgrade"),
|
||||||
|
description: (
|
||||||
|
<div>{ _t("This room is in some spaces you’re not an admin of. " +
|
||||||
|
"In those spaces, the old room will still be shown, " +
|
||||||
|
"but people will be prompted to join the new one.") }</div>
|
||||||
|
),
|
||||||
|
hasCancelButton: true,
|
||||||
|
button: _t("Upgrade anyway"),
|
||||||
|
danger: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [shouldUpgrade] = await modal.finished;
|
||||||
|
if (!shouldUpgrade) return;
|
||||||
|
}
|
||||||
|
|
||||||
const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true);
|
const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true);
|
||||||
closeSettingsFn();
|
closeSettingsFn();
|
||||||
// switch to the new room in the background
|
// switch to the new room in the background
|
||||||
|
@ -226,8 +251,7 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
|
||||||
action: "open_room_settings",
|
action: "open_room_settings",
|
||||||
initial_tab_id: ROOM_SECURITY_TAB,
|
initial_tab_id: ROOM_SECURITY_TAB,
|
||||||
});
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -137,7 +137,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onPowerLevelsChanged = (inputValue: string, powerLevelKey: string) => {
|
private onPowerLevelsChanged = (value: number, powerLevelKey: string) => {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
|
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
|
||||||
|
@ -148,8 +148,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||||
|
|
||||||
const eventsLevelPrefix = "event_levels_";
|
const eventsLevelPrefix = "event_levels_";
|
||||||
|
|
||||||
const value = parseInt(inputValue);
|
|
||||||
|
|
||||||
if (powerLevelKey.startsWith(eventsLevelPrefix)) {
|
if (powerLevelKey.startsWith(eventsLevelPrefix)) {
|
||||||
// deep copy "events" object, Object.assign itself won't deep copy
|
// deep copy "events" object, Object.assign itself won't deep copy
|
||||||
plContent["events"] = Object.assign({}, plContent["events"] || {});
|
plContent["events"] = Object.assign({}, plContent["events"] || {});
|
||||||
|
@ -181,7 +179,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onUserPowerLevelChanged = (value: string, powerLevelKey: string) => {
|
private onUserPowerLevelChanged = (value: number, powerLevelKey: string) => {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
|
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
|
||||||
|
|
|
@ -28,7 +28,6 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent"
|
||||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||||
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
|
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import SpaceStore from "../../../../../stores/SpaceStore";
|
|
||||||
import GroupAvatar from "../../../avatars/GroupAvatar";
|
import GroupAvatar from "../../../avatars/GroupAvatar";
|
||||||
import dis from "../../../../../dispatcher/dispatcher";
|
import dis from "../../../../../dispatcher/dispatcher";
|
||||||
import GroupActions from "../../../../../actions/GroupActions";
|
import GroupActions from "../../../../../actions/GroupActions";
|
||||||
|
@ -145,7 +144,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
];
|
];
|
||||||
|
|
||||||
static COMMUNITIES_SETTINGS = [
|
static COMMUNITIES_SETTINGS = [
|
||||||
// TODO: part of delabsing move the toggle here - https://github.com/vector-im/element-web/issues/18088
|
"showCommunitiesInsteadOfSpaces",
|
||||||
];
|
];
|
||||||
|
|
||||||
static KEYBINDINGS_SETTINGS = [
|
static KEYBINDINGS_SETTINGS = [
|
||||||
|
@ -286,9 +285,17 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
|
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderGroup(settingIds: string[]): React.ReactNodeArray {
|
private renderGroup(
|
||||||
return settingIds.filter(SettingsStore.isEnabled).map(i => {
|
settingIds: string[],
|
||||||
return <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />;
|
level = SettingLevel.ACCOUNT,
|
||||||
|
includeDisabled = false,
|
||||||
|
): React.ReactNodeArray {
|
||||||
|
if (!includeDisabled) {
|
||||||
|
settingIds = settingIds.filter(SettingsStore.isEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
return settingIds.map(i => {
|
||||||
|
return <SettingsFlag key={i} name={i} level={level} />;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,10 +341,10 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
|
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ SpaceStore.spacesEnabled && <div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
|
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
|
||||||
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS) }
|
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS, SettingLevel.ACCOUNT, true) }
|
||||||
</div> }
|
</div>
|
||||||
|
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Communities") }</span>
|
<span className="mx_SettingsTab_subheading">{ _t("Communities") }</span>
|
||||||
|
@ -349,7 +356,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
<p>{ _t("If a community isn't shown you may not have permission to convert it.") }</p>
|
<p>{ _t("If a community isn't shown you may not have permission to convert it.") }</p>
|
||||||
<CommunityMigrator onFinished={this.props.closeSettingsFn} />
|
<CommunityMigrator onFinished={this.props.closeSettingsFn} />
|
||||||
</details>
|
</details>
|
||||||
{ this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS) }
|
{ this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS, SettingLevel.DEVICE) }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
|
|
|
@ -97,9 +97,8 @@ const spaceNameValidator = withValidation({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const nameToAlias = (name: string, domain: string): string => {
|
const nameToLocalpart = (name: string): string => {
|
||||||
const localpart = name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, "");
|
return name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, "");
|
||||||
return `#${localpart}:${domain}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// XXX: Temporary for the Spaces release only
|
// XXX: Temporary for the Spaces release only
|
||||||
|
@ -118,9 +117,7 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
|
||||||
"Your feedback will help inform the next versions."),
|
"Your feedback will help inform the next versions."),
|
||||||
rageshakeLabel: "spaces-feedback",
|
rageshakeLabel: "spaces-feedback",
|
||||||
rageshakeData: Object.fromEntries([
|
rageshakeData: Object.fromEntries([
|
||||||
"feature_spaces.all_rooms",
|
"Spaces.allRoomsInHome",
|
||||||
"feature_spaces.space_member_dms",
|
|
||||||
"feature_spaces.space_dm_badges",
|
|
||||||
].map(k => [k, SettingsStore.getValue(k)])),
|
].map(k => [k, SettingsStore.getValue(k)])),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -176,8 +173,9 @@ export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
|
||||||
value={name}
|
value={name}
|
||||||
onChange={ev => {
|
onChange={ev => {
|
||||||
const newName = ev.target.value;
|
const newName = ev.target.value;
|
||||||
if (!alias || alias === nameToAlias(name, domain)) {
|
if (!alias || alias === `#${nameToLocalpart(name)}:${domain}`) {
|
||||||
setAlias(nameToAlias(newName, domain));
|
setAlias(`#${nameToLocalpart(newName)}:${domain}`);
|
||||||
|
aliasFieldRef.current?.validate({ allowEmpty: true });
|
||||||
}
|
}
|
||||||
setName(newName);
|
setName(newName);
|
||||||
}}
|
}}
|
||||||
|
@ -194,7 +192,7 @@ export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
|
||||||
onChange={setAlias}
|
onChange={setAlias}
|
||||||
domain={domain}
|
domain={domain}
|
||||||
value={alias}
|
value={alias}
|
||||||
placeholder={name ? nameToAlias(name, domain) : _t("e.g. my-space")}
|
placeholder={name ? nameToLocalpart(name) : _t("e.g. my-space")}
|
||||||
label={_t("Address")}
|
label={_t("Address")}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
@ -217,6 +215,7 @@ export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const SpaceCreateMenu = ({ onFinished }) => {
|
const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
const [visibility, setVisibility] = useState<Visibility>(null);
|
const [visibility, setVisibility] = useState<Visibility>(null);
|
||||||
const [busy, setBusy] = useState<boolean>(false);
|
const [busy, setBusy] = useState<boolean>(false);
|
||||||
|
|
||||||
|
@ -233,14 +232,18 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
|
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
// require & validate the space name field
|
// require & validate the space name field
|
||||||
if (!await spaceNameField.current.validate({ allowEmpty: false })) {
|
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
||||||
spaceNameField.current.focus();
|
spaceNameField.current.focus();
|
||||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// validate the space name alias field but do not require it
|
|
||||||
if (visibility === Visibility.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
|
// validate the space alias field but do not require it
|
||||||
|
const aliasLocalpart = alias.substring(1, alias.length - cli.getDomain().length - 1);
|
||||||
|
if (visibility === Visibility.Public && aliasLocalpart &&
|
||||||
|
(await spaceAliasField.current.validate({ allowEmpty: true })) === false
|
||||||
|
) {
|
||||||
spaceAliasField.current.focus();
|
spaceAliasField.current.focus();
|
||||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
|
@ -248,7 +251,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createSpace(name, visibility === Visibility.Public, alias, topic, avatar);
|
await createSpace(
|
||||||
|
name,
|
||||||
|
visibility === Visibility.Public,
|
||||||
|
aliasLocalpart ? alias : undefined,
|
||||||
|
topic,
|
||||||
|
avatar,
|
||||||
|
);
|
||||||
|
|
||||||
onFinished();
|
onFinished();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -290,13 +299,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{ _t("You can also create a Space from a <a>community</a>.", {}, {
|
{ _t("You can also make Spaces from <a>communities</a>.", {}, {
|
||||||
a: sub => <AccessibleButton kind="link" onClick={onCreateSpaceFromCommunityClick}>
|
a: sub => <AccessibleButton kind="link" onClick={onCreateSpaceFromCommunityClick}>
|
||||||
{ sub }
|
{ sub }
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
}) }
|
}) }
|
||||||
<br />
|
<br />
|
||||||
{ _t("To join an existing space you'll need an invite.") }
|
{ _t("To join a space you'll need an invite.") }
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SpaceFeedbackPrompt onClick={onFinished} />
|
<SpaceFeedbackPrompt onClick={onFinished} />
|
||||||
|
|
|
@ -151,12 +151,19 @@ const CreateSpaceButton = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const onNewClick = menuDisplayed ? closeMenu : () => {
|
const onNewClick = menuDisplayed ? closeMenu : () => {
|
||||||
|
// persist that the user has interacted with this, use it to dismiss the beta dot
|
||||||
|
localStorage.setItem("mx_seenSpaces", "1");
|
||||||
if (!isPanelCollapsed) setPanelCollapsed(true);
|
if (!isPanelCollapsed) setPanelCollapsed(true);
|
||||||
openMenu();
|
openMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let betaDot: JSX.Element;
|
||||||
|
if (!localStorage.getItem("mx_seenSpaces") && !SpaceStore.instance.spacePanelSpaces.length) {
|
||||||
|
betaDot = <div className="mx_BetaDot" />;
|
||||||
|
}
|
||||||
|
|
||||||
return <li
|
return <li
|
||||||
className={classNames("mx_SpaceItem", {
|
className={classNames("mx_SpaceItem mx_SpaceItem_new", {
|
||||||
"collapsed": isPanelCollapsed,
|
"collapsed": isPanelCollapsed,
|
||||||
})}
|
})}
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
|
@ -169,6 +176,7 @@ const CreateSpaceButton = ({
|
||||||
onClick={onNewClick}
|
onClick={onNewClick}
|
||||||
isNarrow={isPanelCollapsed}
|
isNarrow={isPanelCollapsed}
|
||||||
/>
|
/>
|
||||||
|
{ betaDot }
|
||||||
|
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
</li>;
|
</li>;
|
||||||
|
|
|
@ -93,6 +93,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
|
||||||
notification={notificationState}
|
notification={notificationState}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
showUnsentTooltip={true}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,6 +214,8 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
this.setState({
|
this.setState({
|
||||||
primaryFeed: primary,
|
primaryFeed: primary,
|
||||||
secondaryFeeds: secondary,
|
secondaryFeeds: secondary,
|
||||||
|
micMuted: this.props.call.isMicrophoneMuted(),
|
||||||
|
vidMuted: this.props.call.isLocalVideoMuted(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -258,18 +260,14 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
return { primary, secondary };
|
return { primary, secondary };
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMicMuteClick = (): void => {
|
private onMicMuteClick = async (): Promise<void> => {
|
||||||
const newVal = !this.state.micMuted;
|
const newVal = !this.state.micMuted;
|
||||||
|
this.setState({ micMuted: await this.props.call.setMicrophoneMuted(newVal) });
|
||||||
this.props.call.setMicrophoneMuted(newVal);
|
|
||||||
this.setState({ micMuted: newVal });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onVidMuteClick = (): void => {
|
private onVidMuteClick = async (): Promise<void> => {
|
||||||
const newVal = !this.state.vidMuted;
|
const newVal = !this.state.vidMuted;
|
||||||
|
this.setState({ vidMuted: await this.props.call.setLocalVideoMuted(newVal) });
|
||||||
this.props.call.setLocalVideoMuted(newVal);
|
|
||||||
this.setState({ vidMuted: newVal });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onScreenshareClick = async (): Promise<void> => {
|
private onScreenshareClick = async (): Promise<void> => {
|
||||||
|
@ -277,9 +275,13 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
if (this.state.screensharing) {
|
if (this.state.screensharing) {
|
||||||
isScreensharing = await this.props.call.setScreensharingEnabled(false);
|
isScreensharing = await this.props.call.setScreensharingEnabled(false);
|
||||||
} else {
|
} else {
|
||||||
|
if (window.electron?.getDesktopCapturerSources) {
|
||||||
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||||
const [source] = await finished;
|
const [source] = await finished;
|
||||||
isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
|
isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
|
||||||
|
} else {
|
||||||
|
isScreensharing = await this.props.call.setScreensharingEnabled(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
|
@ -32,13 +32,20 @@ export default class Range {
|
||||||
this._end = bIsLarger ? positionB : positionA;
|
this._end = bIsLarger ? positionB : positionA;
|
||||||
}
|
}
|
||||||
|
|
||||||
public moveStart(delta: number): void {
|
public moveStartForwards(delta: number): void {
|
||||||
this._start = this._start.forwardsWhile(this.model, () => {
|
this._start = this._start.forwardsWhile(this.model, () => {
|
||||||
delta -= 1;
|
delta -= 1;
|
||||||
return delta >= 0;
|
return delta >= 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public moveEndBackwards(delta: number): void {
|
||||||
|
this._end = this._end.backwardsWhile(this.model, () => {
|
||||||
|
delta -= 1;
|
||||||
|
return delta >= 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public trim(): void {
|
public trim(): void {
|
||||||
this._start = this._start.forwardsWhile(this.model, whitespacePredicate);
|
this._start = this._start.forwardsWhile(this.model, whitespacePredicate);
|
||||||
this._end = this._end.backwardsWhile(this.model, whitespacePredicate);
|
this._end = this._end.backwardsWhile(this.model, whitespacePredicate);
|
||||||
|
|
|
@ -20,7 +20,11 @@ import type { EventEmitter } from "events";
|
||||||
type Handler = (...args: any[]) => void;
|
type Handler = (...args: any[]) => void;
|
||||||
|
|
||||||
// Hook to wrap event emitter on and removeListener in hook lifecycle
|
// Hook to wrap event emitter on and removeListener in hook lifecycle
|
||||||
export const useEventEmitter = (emitter: EventEmitter, eventName: string | symbol, handler: Handler) => {
|
export const useEventEmitter = (
|
||||||
|
emitter: EventEmitter | undefined,
|
||||||
|
eventName: string | symbol,
|
||||||
|
handler: Handler,
|
||||||
|
) => {
|
||||||
// Create a ref that stores handler
|
// Create a ref that stores handler
|
||||||
const savedHandler = useRef(handler);
|
const savedHandler = useRef(handler);
|
||||||
|
|
||||||
|
@ -51,7 +55,11 @@ export const useEventEmitter = (emitter: EventEmitter, eventName: string | symbo
|
||||||
|
|
||||||
type Mapper<T> = (...args: any[]) => T;
|
type Mapper<T> = (...args: any[]) => T;
|
||||||
|
|
||||||
export const useEventEmitterState = <T>(emitter: EventEmitter, eventName: string | symbol, fn: Mapper<T>): T => {
|
export const useEventEmitterState = <T>(
|
||||||
|
emitter: EventEmitter | undefined,
|
||||||
|
eventName: string | symbol,
|
||||||
|
fn: Mapper<T>,
|
||||||
|
): T => {
|
||||||
const [value, setValue] = useState<T>(fn());
|
const [value, setValue] = useState<T>(fn());
|
||||||
const handler = useCallback((...args: any[]) => {
|
const handler = useCallback((...args: any[]) => {
|
||||||
setValue(fn(...args));
|
setValue(fn(...args));
|
||||||
|
|
|
@ -25,7 +25,7 @@ const defaultMapper: Mapper<RoomState> = (roomState: RoomState) => roomState;
|
||||||
|
|
||||||
// Hook to simplify watching Matrix Room state
|
// Hook to simplify watching Matrix Room state
|
||||||
export const useRoomState = <T extends any = RoomState>(
|
export const useRoomState = <T extends any = RoomState>(
|
||||||
room: Room,
|
room?: Room,
|
||||||
mapper: Mapper<T> = defaultMapper as Mapper<T>,
|
mapper: Mapper<T> = defaultMapper as Mapper<T>,
|
||||||
): T => {
|
): T => {
|
||||||
const [value, setValue] = useState<T>(room ? mapper(room.currentState) : undefined);
|
const [value, setValue] = useState<T>(room ? mapper(room.currentState) : undefined);
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue