Improve Members Right Panel (#1286)
* fix room members hook * fix resize observer hook * add intersection observer hook * install react-virtual lib * improve right panel - WIP * add filters for members * fix bug in async search * categories members and add search * show spinner on room member fetch * make invite member btn clickable * so no member text * add line between room view and member drawer * fix imports * add screen size hook * fix set setting hook * make member drawer responsive * extract power level tags hook * fix room members hook * fix use async search api * produce search result on filter change
This commit is contained in:
parent
da32d0d9e7
commit
c07905c360
19 changed files with 984 additions and 79 deletions
173
package-lock.json
generated
173
package-lock.json
generated
|
@ -13,6 +13,7 @@
|
||||||
"@fontsource/roboto": "4.5.8",
|
"@fontsource/roboto": "4.5.8",
|
||||||
"@khanacademy/simple-markdown": "0.8.6",
|
"@khanacademy/simple-markdown": "0.8.6",
|
||||||
"@matrix-org/olm": "3.2.14",
|
"@matrix-org/olm": "3.2.14",
|
||||||
|
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||||
"@tippyjs/react": "4.2.6",
|
"@tippyjs/react": "4.2.6",
|
||||||
"@vanilla-extract/css": "1.9.3",
|
"@vanilla-extract/css": "1.9.3",
|
||||||
"@vanilla-extract/recipes": "0.3.0",
|
"@vanilla-extract/recipes": "0.3.0",
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
"linkify-html": "4.0.2",
|
"linkify-html": "4.0.2",
|
||||||
"linkifyjs": "4.0.2",
|
"linkifyjs": "4.0.2",
|
||||||
"matrix-js-sdk": "24.1.0",
|
"matrix-js-sdk": "24.1.0",
|
||||||
|
"millify": "6.1.0",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-autosize-textarea": "7.1.0",
|
"react-autosize-textarea": "7.1.0",
|
||||||
|
@ -1106,6 +1108,30 @@
|
||||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/react-virtual": {
|
||||||
|
"version": "3.0.0-beta.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.54.tgz",
|
||||||
|
"integrity": "sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/virtual-core": "3.0.0-beta.54"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/virtual-core": {
|
||||||
|
"version": "3.0.0-beta.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.54.tgz",
|
||||||
|
"integrity": "sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tippyjs/react": {
|
"node_modules/@tippyjs/react": {
|
||||||
"version": "4.2.6",
|
"version": "4.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz",
|
||||||
|
@ -1669,7 +1695,6 @@
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
|
@ -2058,6 +2083,19 @@
|
||||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
||||||
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
|
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.1",
|
||||||
|
"wrap-ansi": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
|
@ -3349,6 +3387,14 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
|
||||||
|
@ -3771,6 +3817,14 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-glob": {
|
"node_modules/is-glob": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
|
@ -4286,6 +4340,17 @@
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/millify": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/millify/-/millify-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-H/E3J6t+DQs/F2YgfDhxUVZz/dF8JXPPKTLHL/yHCcLZLtCXJDUaqvhJXQwqOVBvbyNn4T0WjLpIHd7PAw7fBA==",
|
||||||
|
"dependencies": {
|
||||||
|
"yargs": "^17.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"millify": "bin/millify"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mini-svg-data-uri": {
|
"node_modules/mini-svg-data-uri": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||||
|
@ -4965,6 +5030,14 @@
|
||||||
"url": "https://github.com/sponsors/mysticatea"
|
"url": "https://github.com/sponsors/mysticatea"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/require-like": {
|
"node_modules/require-like": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz",
|
||||||
|
@ -5256,6 +5329,24 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width/node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||||
|
},
|
||||||
"node_modules/string.prototype.matchall": {
|
"node_modules/string.prototype.matchall": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz",
|
||||||
|
@ -5307,7 +5398,6 @@
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
},
|
},
|
||||||
|
@ -6166,12 +6256,66 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
|
},
|
||||||
"node_modules/wrappy": {
|
"node_modules/wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
@ -6186,6 +6330,31 @@
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "17.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^8.0.1",
|
||||||
|
"escalade": "^3.1.1",
|
||||||
|
"get-caller-file": "^2.0.5",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"string-width": "^4.2.3",
|
||||||
|
"y18n": "^5.0.5",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "21.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"@fontsource/roboto": "4.5.8",
|
"@fontsource/roboto": "4.5.8",
|
||||||
"@khanacademy/simple-markdown": "0.8.6",
|
"@khanacademy/simple-markdown": "0.8.6",
|
||||||
"@matrix-org/olm": "3.2.14",
|
"@matrix-org/olm": "3.2.14",
|
||||||
|
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||||
"@tippyjs/react": "4.2.6",
|
"@tippyjs/react": "4.2.6",
|
||||||
"@vanilla-extract/css": "1.9.3",
|
"@vanilla-extract/css": "1.9.3",
|
||||||
"@vanilla-extract/recipes": "0.3.0",
|
"@vanilla-extract/recipes": "0.3.0",
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
"linkify-html": "4.0.2",
|
"linkify-html": "4.0.2",
|
||||||
"linkifyjs": "4.0.2",
|
"linkifyjs": "4.0.2",
|
||||||
"matrix-js-sdk": "24.1.0",
|
"matrix-js-sdk": "24.1.0",
|
||||||
|
"millify": "6.1.0",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-autosize-textarea": "7.1.0",
|
"react-autosize-textarea": "7.1.0",
|
||||||
|
|
|
@ -60,12 +60,13 @@ export function EmoticonAutocomplete({
|
||||||
);
|
);
|
||||||
}, [imagePacks]);
|
}, [imagePacks]);
|
||||||
|
|
||||||
const [result, search] = useAsyncSearch(searchList, getEmoticonStr, SEARCH_OPTIONS);
|
const [result, search, resetSearch] = useAsyncSearch(searchList, getEmoticonStr, SEARCH_OPTIONS);
|
||||||
const autoCompleteEmoticon = result ? result.items : recentEmoji;
|
const autoCompleteEmoticon = result ? result.items : recentEmoji;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
search(query.text);
|
if (query.text) search(query.text);
|
||||||
}, [query.text, search]);
|
else resetSearch();
|
||||||
|
}, [query.text, search, resetSearch]);
|
||||||
|
|
||||||
const handleAutocomplete: EmoticonCompleteHandler = (key, shortcode) => {
|
const handleAutocomplete: EmoticonCompleteHandler = (key, shortcode) => {
|
||||||
const emoticonEl = createEmoticonElement(key, shortcode);
|
const emoticonEl = createEmoticonElement(key, shortcode);
|
||||||
|
|
|
@ -81,7 +81,7 @@ export function RoomMentionAutocomplete({
|
||||||
return [...spaces, ...rooms, ...directs].sort(roomIdByActivity);
|
return [...spaces, ...rooms, ...directs].sort(roomIdByActivity);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [result, search] = useAsyncSearch(
|
const [result, search, resetSearch] = useAsyncSearch(
|
||||||
allRoomId,
|
allRoomId,
|
||||||
useCallback(
|
useCallback(
|
||||||
(rId) => {
|
(rId) => {
|
||||||
|
@ -99,8 +99,9 @@ export function RoomMentionAutocomplete({
|
||||||
const autoCompleteRoomIds = result ? result.items : allRoomId.slice(0, 20);
|
const autoCompleteRoomIds = result ? result.items : allRoomId.slice(0, 20);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
search(query.text);
|
if (query.text) search(query.text);
|
||||||
}, [query.text, search]);
|
else resetSearch();
|
||||||
|
}, [query.text, search, resetSearch]);
|
||||||
|
|
||||||
const handleAutocomplete: MentionAutoCompleteHandler = (roomAliasOrId, name) => {
|
const handleAutocomplete: MentionAutoCompleteHandler = (roomAliasOrId, name) => {
|
||||||
const mentionEl = createMentionElement(
|
const mentionEl = createMentionElement(
|
||||||
|
|
|
@ -94,12 +94,13 @@ export function UserMentionAutocomplete({
|
||||||
const roomAliasOrId = room?.getCanonicalAlias() || roomId;
|
const roomAliasOrId = room?.getCanonicalAlias() || roomId;
|
||||||
const members = useRoomMembers(mx, roomId);
|
const members = useRoomMembers(mx, roomId);
|
||||||
|
|
||||||
const [result, search] = useAsyncSearch(members, getRoomMemberStr, SEARCH_OPTIONS);
|
const [result, search, resetSearch] = useAsyncSearch(members, getRoomMemberStr, SEARCH_OPTIONS);
|
||||||
const autoCompleteMembers = result ? result.items : members.slice(0, 20);
|
const autoCompleteMembers = result ? result.items : members.slice(0, 20);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
search(query.text);
|
if (query.text) search(query.text);
|
||||||
}, [query.text, search]);
|
else resetSearch();
|
||||||
|
}, [query.text, search, resetSearch]);
|
||||||
|
|
||||||
const handleAutocomplete: MentionAutoCompleteHandler = (uId, name) => {
|
const handleAutocomplete: MentionAutoCompleteHandler = (uId, name) => {
|
||||||
const mentionEl = createMentionElement(
|
const mentionEl = createMentionElement(
|
||||||
|
|
|
@ -647,15 +647,20 @@ export function EmojiBoard({
|
||||||
return list;
|
return list;
|
||||||
}, [emojiTab, usage, imagePacks]);
|
}, [emojiTab, usage, imagePacks]);
|
||||||
|
|
||||||
const [result, search] = useAsyncSearch(searchList, getSearchListItemStr, SEARCH_OPTIONS);
|
const [result, search, resetSearch] = useAsyncSearch(
|
||||||
|
searchList,
|
||||||
|
getSearchListItemStr,
|
||||||
|
SEARCH_OPTIONS
|
||||||
|
);
|
||||||
|
|
||||||
const handleOnChange: ChangeEventHandler<HTMLInputElement> = useDebounce(
|
const handleOnChange: ChangeEventHandler<HTMLInputElement> = useDebounce(
|
||||||
useCallback(
|
useCallback(
|
||||||
(evt) => {
|
(evt) => {
|
||||||
const term = evt.target.value;
|
const term = evt.target.value;
|
||||||
search(term);
|
if (term) search(term);
|
||||||
|
else resetSearch();
|
||||||
},
|
},
|
||||||
[search]
|
[search, resetSearch]
|
||||||
),
|
),
|
||||||
{ wait: 200 }
|
{ wait: 200 }
|
||||||
);
|
);
|
||||||
|
|
|
@ -25,11 +25,13 @@ export type UseAsyncSearchResult<TSearchItem extends object | string | number> =
|
||||||
items: TSearchItem[];
|
items: TSearchItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SearchResetHandler = () => void;
|
||||||
|
|
||||||
export const useAsyncSearch = <TSearchItem extends object | string | number>(
|
export const useAsyncSearch = <TSearchItem extends object | string | number>(
|
||||||
list: TSearchItem[],
|
list: TSearchItem[],
|
||||||
getItemStr: SearchItemStrGetter<TSearchItem>,
|
getItemStr: SearchItemStrGetter<TSearchItem>,
|
||||||
options?: UseAsyncSearchOptions
|
options?: UseAsyncSearchOptions
|
||||||
): [UseAsyncSearchResult<TSearchItem> | undefined, AsyncSearchHandler] => {
|
): [UseAsyncSearchResult<TSearchItem> | undefined, AsyncSearchHandler, SearchResetHandler] => {
|
||||||
const [result, setResult] = useState<UseAsyncSearchResult<TSearchItem>>();
|
const [result, setResult] = useState<UseAsyncSearchResult<TSearchItem>>();
|
||||||
|
|
||||||
const [searchCallback, terminateSearch] = useMemo(() => {
|
const [searchCallback, terminateSearch] = useMemo(() => {
|
||||||
|
@ -51,7 +53,7 @@ export const useAsyncSearch = <TSearchItem extends object | string | number>(
|
||||||
const handleResult: ResultHandler<TSearchItem> = (results, query) =>
|
const handleResult: ResultHandler<TSearchItem> = (results, query) =>
|
||||||
setResult({
|
setResult({
|
||||||
query,
|
query,
|
||||||
items: results,
|
items: [...results],
|
||||||
});
|
});
|
||||||
|
|
||||||
return AsyncSearch(list, handleMatch, handleResult, options);
|
return AsyncSearch(list, handleMatch, handleResult, options);
|
||||||
|
@ -60,15 +62,16 @@ export const useAsyncSearch = <TSearchItem extends object | string | number>(
|
||||||
const searchHandler: AsyncSearchHandler = useCallback(
|
const searchHandler: AsyncSearchHandler = useCallback(
|
||||||
(query) => {
|
(query) => {
|
||||||
const normalizedQuery = normalize(query, options?.normalizeOptions);
|
const normalizedQuery = normalize(query, options?.normalizeOptions);
|
||||||
if (!normalizedQuery) {
|
|
||||||
setResult(undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
searchCallback(normalizedQuery);
|
searchCallback(normalizedQuery);
|
||||||
},
|
},
|
||||||
[searchCallback, options?.normalizeOptions]
|
[searchCallback, options?.normalizeOptions]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const resetHandler: SearchResetHandler = useCallback(() => {
|
||||||
|
terminateSearch();
|
||||||
|
setResult(undefined);
|
||||||
|
}, [terminateSearch]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
// terminate any ongoing search request on unmount.
|
// terminate any ongoing search request on unmount.
|
||||||
|
@ -77,5 +80,5 @@ export const useAsyncSearch = <TSearchItem extends object | string | number>(
|
||||||
[terminateSearch]
|
[terminateSearch]
|
||||||
);
|
);
|
||||||
|
|
||||||
return [result, searchHandler];
|
return [result, searchHandler, resetHandler];
|
||||||
};
|
};
|
||||||
|
|
37
src/app/hooks/useIntersectionObserver.ts
Normal file
37
src/app/hooks/useIntersectionObserver.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export type OnIntersectionCallback = (entries: IntersectionObserverEntry[]) => void;
|
||||||
|
|
||||||
|
export type IntersectionObserverOpts = {
|
||||||
|
root?: Element | Document | null;
|
||||||
|
rootMargin?: string;
|
||||||
|
threshold?: number | number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIntersectionObserverEntry = (
|
||||||
|
target: Element | Document,
|
||||||
|
entries: IntersectionObserverEntry[]
|
||||||
|
): IntersectionObserverEntry | undefined => entries.find((entry) => entry.target === target);
|
||||||
|
|
||||||
|
export const useIntersectionObserver = (
|
||||||
|
onIntersectionCallback: OnIntersectionCallback,
|
||||||
|
opts?: IntersectionObserverOpts | (() => IntersectionObserverOpts),
|
||||||
|
observeElement?: Element | null | (() => Element | null)
|
||||||
|
): IntersectionObserver | undefined => {
|
||||||
|
const [intersectionObserver, setIntersectionObserver] = useState<IntersectionObserver>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initOpts = typeof opts === 'function' ? opts() : opts;
|
||||||
|
setIntersectionObserver(new IntersectionObserver(onIntersectionCallback, initOpts));
|
||||||
|
}, [onIntersectionCallback, opts]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = typeof observeElement === 'function' ? observeElement() : observeElement;
|
||||||
|
if (element) intersectionObserver?.observe(element);
|
||||||
|
return () => {
|
||||||
|
if (element) intersectionObserver?.unobserve(element);
|
||||||
|
};
|
||||||
|
}, [intersectionObserver, observeElement]);
|
||||||
|
|
||||||
|
return intersectionObserver;
|
||||||
|
};
|
38
src/app/hooks/usePowerLevelTags.ts
Normal file
38
src/app/hooks/usePowerLevelTags.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
export type PowerLevelTag = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
export const usePowerLevelTags = () => {
|
||||||
|
const powerLevelTags = useMemo(
|
||||||
|
() => ({
|
||||||
|
9000: {
|
||||||
|
name: 'Goku',
|
||||||
|
},
|
||||||
|
101: {
|
||||||
|
name: 'Founder',
|
||||||
|
},
|
||||||
|
100: {
|
||||||
|
name: 'Admin',
|
||||||
|
},
|
||||||
|
50: {
|
||||||
|
name: 'Moderator',
|
||||||
|
},
|
||||||
|
0: {
|
||||||
|
name: 'Default',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(powerLevel: number): PowerLevelTag => {
|
||||||
|
if (powerLevel >= 9000) return powerLevelTags[9000];
|
||||||
|
if (powerLevel >= 101) return powerLevelTags[101];
|
||||||
|
if (powerLevel === 100) return powerLevelTags[100];
|
||||||
|
if (powerLevel >= 50) return powerLevelTags[50];
|
||||||
|
return powerLevelTags[0];
|
||||||
|
},
|
||||||
|
[powerLevelTags]
|
||||||
|
);
|
||||||
|
};
|
|
@ -8,17 +8,18 @@ export const getResizeObserverEntry = (
|
||||||
): ResizeObserverEntry | undefined => entries.find((entry) => entry.target === target);
|
): ResizeObserverEntry | undefined => entries.find((entry) => entry.target === target);
|
||||||
|
|
||||||
export const useResizeObserver = (
|
export const useResizeObserver = (
|
||||||
element: Element | null,
|
onResizeCallback: OnResizeCallback,
|
||||||
onResizeCallback: OnResizeCallback
|
observeElement?: Element | null | (() => Element | null)
|
||||||
): ResizeObserver => {
|
): ResizeObserver => {
|
||||||
const resizeObserver = useMemo(() => new ResizeObserver(onResizeCallback), [onResizeCallback]);
|
const resizeObserver = useMemo(() => new ResizeObserver(onResizeCallback), [onResizeCallback]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const element = typeof observeElement === 'function' ? observeElement() : observeElement;
|
||||||
if (element) resizeObserver.observe(element);
|
if (element) resizeObserver.observe(element);
|
||||||
return () => {
|
return () => {
|
||||||
if (element) resizeObserver.unobserve(element);
|
if (element) resizeObserver.unobserve(element);
|
||||||
};
|
};
|
||||||
}, [resizeObserver, element]);
|
}, [resizeObserver, observeElement]);
|
||||||
|
|
||||||
return resizeObserver;
|
return resizeObserver;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,23 +1,25 @@
|
||||||
import { MatrixClient, MatrixEvent, RoomMember, RoomMemberEvent } from 'matrix-js-sdk';
|
import { MatrixClient, MatrixEvent, RoomMember, RoomMemberEvent } from 'matrix-js-sdk';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useAlive } from './useAlive';
|
|
||||||
|
|
||||||
export const useRoomMembers = (mx: MatrixClient, roomId: string): RoomMember[] => {
|
export const useRoomMembers = (mx: MatrixClient, roomId: string): RoomMember[] => {
|
||||||
const [members, setMembers] = useState<RoomMember[]>([]);
|
const [members, setMembers] = useState<RoomMember[]>([]);
|
||||||
const alive = useAlive();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
|
let loadingMembers = true;
|
||||||
|
let disposed = false;
|
||||||
|
|
||||||
const updateMemberList = (event?: MatrixEvent) => {
|
const updateMemberList = (event?: MatrixEvent) => {
|
||||||
if (!room || !alive || (event && event.getRoomId() !== roomId)) return;
|
if (!room || disposed || (event && event.getRoomId() !== roomId)) return;
|
||||||
|
if (loadingMembers) return;
|
||||||
setMembers(room.getMembers());
|
setMembers(room.getMembers());
|
||||||
};
|
};
|
||||||
|
|
||||||
if (room) {
|
if (room) {
|
||||||
updateMemberList();
|
setMembers(room.getMembers());
|
||||||
room.loadMembersIfNeeded().then(() => {
|
room.loadMembersIfNeeded().then(() => {
|
||||||
if (!alive) return;
|
loadingMembers = false;
|
||||||
|
if (disposed) return;
|
||||||
updateMemberList();
|
updateMemberList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -25,10 +27,11 @@ export const useRoomMembers = (mx: MatrixClient, roomId: string): RoomMember[] =
|
||||||
mx.on(RoomMemberEvent.Membership, updateMemberList);
|
mx.on(RoomMemberEvent.Membership, updateMemberList);
|
||||||
mx.on(RoomMemberEvent.PowerLevel, updateMemberList);
|
mx.on(RoomMemberEvent.PowerLevel, updateMemberList);
|
||||||
return () => {
|
return () => {
|
||||||
|
disposed = true;
|
||||||
mx.removeListener(RoomMemberEvent.Membership, updateMemberList);
|
mx.removeListener(RoomMemberEvent.Membership, updateMemberList);
|
||||||
mx.removeListener(RoomMemberEvent.PowerLevel, updateMemberList);
|
mx.removeListener(RoomMemberEvent.PowerLevel, updateMemberList);
|
||||||
};
|
};
|
||||||
}, [mx, roomId, alive]);
|
}, [mx, roomId]);
|
||||||
|
|
||||||
return members;
|
return members;
|
||||||
};
|
};
|
||||||
|
|
36
src/app/hooks/useScreenSize.ts
Normal file
36
src/app/hooks/useScreenSize.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { getResizeObserverEntry, useResizeObserver } from './useResizeObserver';
|
||||||
|
|
||||||
|
export const TABLET_BREAKPOINT = 1124;
|
||||||
|
export const MOBILE_BREAKPOINT = 750;
|
||||||
|
|
||||||
|
export enum ScreenSize {
|
||||||
|
Desktop = 'Desktop',
|
||||||
|
Tablet = 'Tablet',
|
||||||
|
Mobile = 'Mobile',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getScreenSize = (width: number): ScreenSize => {
|
||||||
|
if (width > TABLET_BREAKPOINT) return ScreenSize.Desktop;
|
||||||
|
if (width > MOBILE_BREAKPOINT) return ScreenSize.Tablet;
|
||||||
|
return ScreenSize.Mobile;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useScreenSize = (): [ScreenSize, number] => {
|
||||||
|
const [size, setSize] = useState<[ScreenSize, number]>([
|
||||||
|
getScreenSize(document.body.clientWidth),
|
||||||
|
document.body.clientWidth,
|
||||||
|
]);
|
||||||
|
useResizeObserver(
|
||||||
|
useCallback((entries) => {
|
||||||
|
const bodyEntry = getResizeObserverEntry(document.body, entries);
|
||||||
|
if (bodyEntry) {
|
||||||
|
const bWidth = bodyEntry.contentRect.width;
|
||||||
|
setSize([getScreenSize(bWidth), bWidth]);
|
||||||
|
}
|
||||||
|
}, []),
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
|
||||||
|
return size;
|
||||||
|
};
|
64
src/app/organisms/room/MembersDrawer.css.ts
Normal file
64
src/app/organisms/room/MembersDrawer.css.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { keyframes, style } from '@vanilla-extract/css';
|
||||||
|
import { color, config, toRem } from 'folds';
|
||||||
|
|
||||||
|
export const MembersDrawer = style({
|
||||||
|
width: toRem(266),
|
||||||
|
backgroundColor: color.Background.Container,
|
||||||
|
color: color.Background.OnContainer,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MembersDrawerHeader = style({
|
||||||
|
flexShrink: 0,
|
||||||
|
padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
|
||||||
|
borderBottomWidth: config.borderWidth.B300,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MemberDrawerContentBase = style({
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MemberDrawerContent = style({
|
||||||
|
padding: `${config.space.S300} 0`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ScrollBtnAnime = keyframes({
|
||||||
|
'0%': {
|
||||||
|
transform: `translate(-50%, -100%) scale(0)`,
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: `translate(-50%, 0) scale(1)`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DrawerScrollTop = style({
|
||||||
|
position: 'absolute',
|
||||||
|
top: config.space.S200,
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
zIndex: 1,
|
||||||
|
animation: `${ScrollBtnAnime} 100ms`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DrawerGroup = style({
|
||||||
|
padding: `0 ${config.space.S100} 0 ${config.space.S300}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MembersGroup = style({
|
||||||
|
paddingLeft: config.space.S200,
|
||||||
|
});
|
||||||
|
export const MembersGroupLabel = style({
|
||||||
|
padding: config.space.S200,
|
||||||
|
selectors: {
|
||||||
|
'&:not(:first-child)': {
|
||||||
|
paddingTop: config.space.S500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DrawerVirtualItem = style({
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
});
|
528
src/app/organisms/room/MembersDrawer.tsx
Normal file
528
src/app/organisms/room/MembersDrawer.tsx
Normal file
|
@ -0,0 +1,528 @@
|
||||||
|
import React, {
|
||||||
|
ChangeEventHandler,
|
||||||
|
MouseEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
Box,
|
||||||
|
Chip,
|
||||||
|
ContainerColor,
|
||||||
|
Header,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Icons,
|
||||||
|
Input,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
PopOut,
|
||||||
|
Scroll,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
TooltipProvider,
|
||||||
|
config,
|
||||||
|
} from 'folds';
|
||||||
|
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||||
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import millify from 'millify';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { openInviteUser, openProfileViewer } from '../../../client/action/navigation';
|
||||||
|
import * as css from './MembersDrawer.css';
|
||||||
|
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
import {
|
||||||
|
getIntersectionObserverEntry,
|
||||||
|
useIntersectionObserver,
|
||||||
|
} from '../../hooks/useIntersectionObserver';
|
||||||
|
import { Membership } from '../../../types/matrix/room';
|
||||||
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
|
import { UseAsyncSearchOptions, useAsyncSearch } from '../../hooks/useAsyncSearch';
|
||||||
|
import { useDebounce } from '../../hooks/useDebounce';
|
||||||
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
import { usePowerLevelTags, PowerLevelTag } from '../../hooks/usePowerLevelTags';
|
||||||
|
|
||||||
|
export const MembershipFilters = {
|
||||||
|
filterJoined: (m: RoomMember) => m.membership === Membership.Join,
|
||||||
|
filterInvited: (m: RoomMember) => m.membership === Membership.Invite,
|
||||||
|
filterLeaved: (m: RoomMember) =>
|
||||||
|
m.membership === Membership.Leave &&
|
||||||
|
m.events.member?.getStateKey() === m.events.member?.getSender(),
|
||||||
|
filterKicked: (m: RoomMember) =>
|
||||||
|
m.membership === Membership.Leave &&
|
||||||
|
m.events.member?.getStateKey() !== m.events.member?.getSender(),
|
||||||
|
filterBanned: (m: RoomMember) => m.membership === Membership.Ban,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MembershipFilterFn = (m: RoomMember) => boolean;
|
||||||
|
|
||||||
|
export type MembershipFilter = {
|
||||||
|
name: string;
|
||||||
|
filterFn: MembershipFilterFn;
|
||||||
|
color: ContainerColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useMembershipFilterMenu = (): MembershipFilter[] =>
|
||||||
|
useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
name: 'Joined',
|
||||||
|
filterFn: MembershipFilters.filterJoined,
|
||||||
|
color: 'Surface',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Invited',
|
||||||
|
filterFn: MembershipFilters.filterInvited,
|
||||||
|
color: 'Success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Left',
|
||||||
|
filterFn: MembershipFilters.filterLeaved,
|
||||||
|
color: 'Secondary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Kicked',
|
||||||
|
filterFn: MembershipFilters.filterKicked,
|
||||||
|
color: 'Warning',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Banned',
|
||||||
|
filterFn: MembershipFilters.filterBanned,
|
||||||
|
color: 'Critical',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SortFilters = {
|
||||||
|
filterAscending: (a: RoomMember, b: RoomMember) =>
|
||||||
|
a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1,
|
||||||
|
filterDescending: (a: RoomMember, b: RoomMember) =>
|
||||||
|
a.name.toLowerCase() > b.name.toLowerCase() ? -1 : 1,
|
||||||
|
filterNewestFirst: (a: RoomMember, b: RoomMember) =>
|
||||||
|
(b.events.member?.getTs() ?? 0) - (a.events.member?.getTs() ?? 0),
|
||||||
|
filterOldest: (a: RoomMember, b: RoomMember) =>
|
||||||
|
(a.events.member?.getTs() ?? 0) - (b.events.member?.getTs() ?? 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SortFilterFn = (a: RoomMember, b: RoomMember) => number;
|
||||||
|
|
||||||
|
export type SortFilter = {
|
||||||
|
name: string;
|
||||||
|
filterFn: SortFilterFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSortFilterMenu = (): SortFilter[] =>
|
||||||
|
useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
name: 'A to Z',
|
||||||
|
filterFn: SortFilters.filterAscending,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Z to A',
|
||||||
|
filterFn: SortFilters.filterDescending,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Newest First',
|
||||||
|
filterFn: SortFilters.filterNewestFirst,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Oldest First',
|
||||||
|
filterFn: SortFilters.filterOldest,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
export type MembersFilterOptions = {
|
||||||
|
membershipFilter: MembershipFilter;
|
||||||
|
sortFilter: SortFilter;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
|
||||||
|
limit: 100,
|
||||||
|
matchOptions: {
|
||||||
|
contain: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const getMemberItemStr = (m: RoomMember) => [m.name, m.userId];
|
||||||
|
|
||||||
|
type MembersDrawerProps = {
|
||||||
|
room: Room;
|
||||||
|
};
|
||||||
|
export function MembersDrawer({ room }: MembersDrawerProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
|
||||||
|
const members = useRoomMembers(mx, room.roomId);
|
||||||
|
const getPowerLevelTag = usePowerLevelTags();
|
||||||
|
const fetchingMembers = members.length < room.getJoinedMemberCount();
|
||||||
|
|
||||||
|
const membershipFilterMenu = useMembershipFilterMenu();
|
||||||
|
const sortFilterMenu = useSortFilterMenu();
|
||||||
|
const [filter, setFilter] = useState<MembersFilterOptions>({
|
||||||
|
membershipFilter: membershipFilterMenu[0],
|
||||||
|
sortFilter: sortFilterMenu[0],
|
||||||
|
});
|
||||||
|
const [onTop, setOnTop] = useState(true);
|
||||||
|
|
||||||
|
const filteredMembers = useMemo(
|
||||||
|
() =>
|
||||||
|
members
|
||||||
|
.filter(filter.membershipFilter.filterFn)
|
||||||
|
.sort(filter.sortFilter.filterFn)
|
||||||
|
.sort((a, b) => b.powerLevel - a.powerLevel),
|
||||||
|
[members, filter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [result, search, resetSearch] = useAsyncSearch(
|
||||||
|
filteredMembers,
|
||||||
|
getMemberItemStr,
|
||||||
|
SEARCH_OPTIONS
|
||||||
|
);
|
||||||
|
if (!result && searchInputRef.current?.value) search(searchInputRef.current.value);
|
||||||
|
|
||||||
|
const processMembers = result ? result.items : filteredMembers;
|
||||||
|
|
||||||
|
const PLTagOrRoomMember = useMemo(() => {
|
||||||
|
let prevTag: PowerLevelTag | undefined;
|
||||||
|
const tagOrMember: Array<PowerLevelTag | RoomMember> = [];
|
||||||
|
processMembers.forEach((m) => {
|
||||||
|
const plTag = getPowerLevelTag(m.powerLevel);
|
||||||
|
if (plTag !== prevTag) {
|
||||||
|
prevTag = plTag;
|
||||||
|
tagOrMember.push(plTag);
|
||||||
|
}
|
||||||
|
tagOrMember.push(m);
|
||||||
|
});
|
||||||
|
return tagOrMember;
|
||||||
|
}, [processMembers, getPowerLevelTag]);
|
||||||
|
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
count: PLTagOrRoomMember.length,
|
||||||
|
getScrollElement: () => scrollRef.current,
|
||||||
|
estimateSize: () => 40,
|
||||||
|
overscan: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
useIntersectionObserver(
|
||||||
|
useCallback((intersectionEntries) => {
|
||||||
|
if (!scrollTopAnchorRef.current) return;
|
||||||
|
const entry = getIntersectionObserverEntry(scrollTopAnchorRef.current, intersectionEntries);
|
||||||
|
if (entry) setOnTop(entry.isIntersecting);
|
||||||
|
}, []),
|
||||||
|
useCallback(() => ({ root: scrollRef.current }), []),
|
||||||
|
useCallback(() => scrollTopAnchorRef.current, [])
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearchChange: ChangeEventHandler<HTMLInputElement> = useDebounce(
|
||||||
|
useCallback(
|
||||||
|
(evt) => {
|
||||||
|
if (evt.target.value) search(evt.target.value);
|
||||||
|
else resetSearch();
|
||||||
|
},
|
||||||
|
[search, resetSearch]
|
||||||
|
),
|
||||||
|
{ wait: 200 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMemberClick: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
|
const btn = evt.currentTarget as HTMLButtonElement;
|
||||||
|
const userId = btn.getAttribute('data-user-id');
|
||||||
|
openProfileViewer(userId, room.roomId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className={css.MembersDrawer} direction="Column">
|
||||||
|
<Header className={css.MembersDrawerHeader} variant="Background" size="600">
|
||||||
|
<Box grow="Yes" alignItems="Center" gap="200">
|
||||||
|
<Box grow="Yes" alignItems="Center" gap="200">
|
||||||
|
<Text size="H5" truncate>
|
||||||
|
{`${millify(room.getJoinedMemberCount(), { precision: 1 })} Members`}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box shrink="No" alignItems="Center">
|
||||||
|
<TooltipProvider
|
||||||
|
position="Bottom"
|
||||||
|
align="End"
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text>Invite Member</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(triggerRef) => (
|
||||||
|
<IconButton
|
||||||
|
ref={triggerRef}
|
||||||
|
variant="Background"
|
||||||
|
onClick={() => openInviteUser(room.roomId)}
|
||||||
|
>
|
||||||
|
<Icon src={Icons.UserPlus} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Header>
|
||||||
|
<Box className={css.MemberDrawerContentBase} grow="Yes">
|
||||||
|
<Scroll ref={scrollRef} variant="Background" size="300" visibility="Hover">
|
||||||
|
<Box className={css.MemberDrawerContent} direction="Column" gap="400">
|
||||||
|
<Box className={css.DrawerGroup} direction="Column" gap="100">
|
||||||
|
<Text size="L400">Filter</Text>
|
||||||
|
<Box alignItems="Center" gap="100" wrap="Wrap">
|
||||||
|
<UseStateProvider initial={false}>
|
||||||
|
{(open, setOpen) => (
|
||||||
|
<PopOut
|
||||||
|
open={open}
|
||||||
|
position="Bottom"
|
||||||
|
align="Start"
|
||||||
|
content={
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: () => setOpen(false),
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu style={{ padding: config.space.S100 }}>
|
||||||
|
{membershipFilterMenu.map((menuItem) => (
|
||||||
|
<MenuItem
|
||||||
|
key={menuItem.name}
|
||||||
|
variant={
|
||||||
|
menuItem.name === filter.membershipFilter.name
|
||||||
|
? menuItem.color
|
||||||
|
: 'Surface'
|
||||||
|
}
|
||||||
|
radii="300"
|
||||||
|
onClick={() => {
|
||||||
|
setFilter((f) => ({ ...f, membershipFilter: menuItem }));
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>{menuItem.name}</Text>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</FocusTrap>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<Chip
|
||||||
|
ref={anchorRef}
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
variant={filter.membershipFilter.color}
|
||||||
|
radii="400"
|
||||||
|
outlined
|
||||||
|
after={<Icon src={Icons.ChevronBottom} size="50" />}
|
||||||
|
>
|
||||||
|
<Text size="T200">{filter.membershipFilter.name}</Text>
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</PopOut>
|
||||||
|
)}
|
||||||
|
</UseStateProvider>
|
||||||
|
<UseStateProvider initial={false}>
|
||||||
|
{(open, setOpen) => (
|
||||||
|
<PopOut
|
||||||
|
open={open}
|
||||||
|
position="Bottom"
|
||||||
|
align="Start"
|
||||||
|
content={
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: () => setOpen(false),
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu style={{ padding: config.space.S100 }}>
|
||||||
|
{sortFilterMenu.map((menuItem) => (
|
||||||
|
<MenuItem
|
||||||
|
key={menuItem.name}
|
||||||
|
variant="Surface"
|
||||||
|
aria-pressed={menuItem.name === filter.sortFilter.name}
|
||||||
|
radii="300"
|
||||||
|
onClick={() => {
|
||||||
|
setFilter((f) => ({ ...f, sortFilter: menuItem }));
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>{menuItem.name}</Text>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</FocusTrap>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<Chip
|
||||||
|
ref={anchorRef}
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
variant="Surface"
|
||||||
|
radii="400"
|
||||||
|
outlined
|
||||||
|
after={<Icon src={Icons.ChevronBottom} size="50" />}
|
||||||
|
>
|
||||||
|
<Text size="T200">{`Order: ${filter.sortFilter.name}`}</Text>
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</PopOut>
|
||||||
|
)}
|
||||||
|
</UseStateProvider>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box ref={scrollTopAnchorRef} className={css.DrawerGroup} direction="Column" gap="100">
|
||||||
|
<Text size="L400">Search</Text>
|
||||||
|
<Input
|
||||||
|
ref={searchInputRef}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
style={{ paddingRight: config.space.S200 }}
|
||||||
|
placeholder="Type name..."
|
||||||
|
variant="Surface"
|
||||||
|
size="400"
|
||||||
|
outlined
|
||||||
|
radii="400"
|
||||||
|
before={<Icon size="50" src={Icons.Search} />}
|
||||||
|
after={
|
||||||
|
result && (
|
||||||
|
<Chip
|
||||||
|
variant={result.items.length > 0 ? 'Success' : 'Critical'}
|
||||||
|
size="400"
|
||||||
|
radii="Pill"
|
||||||
|
onClick={() => {
|
||||||
|
if (searchInputRef.current) searchInputRef.current.value = '';
|
||||||
|
resetSearch();
|
||||||
|
}}
|
||||||
|
after={<Icon size="50" src={Icons.Cross} />}
|
||||||
|
>
|
||||||
|
<Text size="B300">{`${result.items.length || 'No'} ${
|
||||||
|
result.items.length === 1 ? 'Result' : 'Results'
|
||||||
|
}`}</Text>
|
||||||
|
</Chip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{!onTop && (
|
||||||
|
<Box className={css.DrawerScrollTop}>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => virtualizer.scrollToOffset(0)}
|
||||||
|
variant="Surface"
|
||||||
|
radii="Pill"
|
||||||
|
outlined
|
||||||
|
size="300"
|
||||||
|
aria-label="Scroll to Top"
|
||||||
|
>
|
||||||
|
<Icon src={Icons.ChevronTop} size="300" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!fetchingMembers && !result && processMembers.length === 0 && (
|
||||||
|
<Text style={{ padding: config.space.S300 }} align="Center">
|
||||||
|
{`No "${filter.membershipFilter.name}" Members`}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box className={css.MembersGroup} direction="Column" gap="100">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
height: virtualizer.getTotalSize(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{virtualizer.getVirtualItems().map((vItem) => {
|
||||||
|
const tagOrMember = PLTagOrRoomMember[vItem.index];
|
||||||
|
if (!('userId' in tagOrMember)) {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
transform: `translateY(${vItem.start}px)`,
|
||||||
|
}}
|
||||||
|
data-index={vItem.index}
|
||||||
|
ref={virtualizer.measureElement}
|
||||||
|
key={`${room.roomId}-${vItem.index}`}
|
||||||
|
className={classNames(css.MembersGroupLabel, css.DrawerVirtualItem)}
|
||||||
|
size="O400"
|
||||||
|
>
|
||||||
|
{tagOrMember.name}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = tagOrMember;
|
||||||
|
const avatarUrl = member.getAvatarUrl(
|
||||||
|
mx.baseUrl,
|
||||||
|
100,
|
||||||
|
100,
|
||||||
|
'crop',
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
style={{
|
||||||
|
padding: config.space.S200,
|
||||||
|
transform: `translateY(${vItem.start}px)`,
|
||||||
|
}}
|
||||||
|
data-index={vItem.index}
|
||||||
|
data-user-id={member.userId}
|
||||||
|
ref={virtualizer.measureElement}
|
||||||
|
key={`${room.roomId}-${member.userId}`}
|
||||||
|
className={css.DrawerVirtualItem}
|
||||||
|
variant="Background"
|
||||||
|
radii="400"
|
||||||
|
onClick={handleMemberClick}
|
||||||
|
before={
|
||||||
|
<Avatar size="200">
|
||||||
|
{avatarUrl ? (
|
||||||
|
<AvatarImage src={avatarUrl} />
|
||||||
|
) : (
|
||||||
|
<AvatarFallback
|
||||||
|
style={{
|
||||||
|
background: colorMXID(member.userId),
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="T200">{member.name[0]}</Text>
|
||||||
|
</AvatarFallback>
|
||||||
|
)}
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="T400" truncate>
|
||||||
|
{member.name}
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{fetchingMembers && (
|
||||||
|
<Box justifyContent="Center">
|
||||||
|
<Spinner />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Scroll>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import './Room.scss';
|
import './Room.scss';
|
||||||
|
import { Line } from 'folds';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import settings from '../../../client/state/settings';
|
|
||||||
import RoomTimeline from '../../../client/state/RoomTimeline';
|
import RoomTimeline from '../../../client/state/RoomTimeline';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { openNavigation } from '../../../client/action/navigation';
|
import { openNavigation } from '../../../client/action/navigation';
|
||||||
|
@ -11,7 +11,10 @@ import { openNavigation } from '../../../client/action/navigation';
|
||||||
import Welcome from '../welcome/Welcome';
|
import Welcome from '../welcome/Welcome';
|
||||||
import RoomView from './RoomView';
|
import RoomView from './RoomView';
|
||||||
import RoomSettings from './RoomSettings';
|
import RoomSettings from './RoomSettings';
|
||||||
import PeopleDrawer from './PeopleDrawer';
|
import { MembersDrawer } from './MembersDrawer';
|
||||||
|
import { ScreenSize, useScreenSize } from '../../hooks/useScreenSize';
|
||||||
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
|
import { settingsAtom } from '../../state/settings';
|
||||||
|
|
||||||
function Room() {
|
function Room() {
|
||||||
const [roomInfo, setRoomInfo] = useState({
|
const [roomInfo, setRoomInfo] = useState({
|
||||||
|
@ -19,7 +22,8 @@ function Room() {
|
||||||
roomTimeline: null,
|
roomTimeline: null,
|
||||||
eventId: null,
|
eventId: null,
|
||||||
});
|
});
|
||||||
const [isDrawer, setIsDrawer] = useState(settings.isPeopleDrawer);
|
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
||||||
|
const [screenSize] = useScreenSize();
|
||||||
|
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
|
|
||||||
|
@ -49,14 +53,6 @@ function Room() {
|
||||||
};
|
};
|
||||||
}, [roomInfo, mx]);
|
}, [roomInfo, mx]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleDrawerToggling = (visiblity) => setIsDrawer(visiblity);
|
|
||||||
settings.on(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
|
|
||||||
return () => {
|
|
||||||
settings.removeListener(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { room, roomTimeline, eventId } = roomInfo;
|
const { room, roomTimeline, eventId } = roomInfo;
|
||||||
if (roomTimeline === null) {
|
if (roomTimeline === null) {
|
||||||
setTimeout(() => openNavigation());
|
setTimeout(() => openNavigation());
|
||||||
|
@ -69,7 +65,13 @@ function Room() {
|
||||||
<RoomSettings roomId={roomTimeline.roomId} />
|
<RoomSettings roomId={roomTimeline.roomId} />
|
||||||
<RoomView room={room} roomTimeline={roomTimeline} eventId={eventId} />
|
<RoomView room={room} roomTimeline={roomTimeline} eventId={eventId} />
|
||||||
</div>
|
</div>
|
||||||
{isDrawer && <PeopleDrawer roomId={roomTimeline.roomId} />}
|
|
||||||
|
{screenSize === ScreenSize.Desktop && isDrawer && (
|
||||||
|
<>
|
||||||
|
<Line variant="Background" direction="Vertical" size="300" />
|
||||||
|
<MembersDrawer room={room} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,7 +97,7 @@ import { MessageReply } from '../../molecules/message/Message';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
import { parseReplyBody, parseReplyFormattedBody } from '../../utils/room';
|
import { parseReplyBody, parseReplyFormattedBody } from '../../utils/room';
|
||||||
import { sanitizeText } from '../../utils/sanitize';
|
import { sanitizeText } from '../../utils/sanitize';
|
||||||
import { getResizeObserverEntry, useResizeObserver } from '../../hooks/useResizeObserver';
|
import { useScreenSize } from '../../hooks/useScreenSize';
|
||||||
|
|
||||||
interface RoomInputProps {
|
interface RoomInputProps {
|
||||||
roomViewRef: RefObject<HTMLElement>;
|
roomViewRef: RefObject<HTMLElement>;
|
||||||
|
@ -161,15 +161,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
const handlePaste = useFilePasteHandler(handleFiles);
|
const handlePaste = useFilePasteHandler(handleFiles);
|
||||||
const dropZoneVisible = useFileDropZone(roomViewRef, handleFiles);
|
const dropZoneVisible = useFileDropZone(roomViewRef, handleFiles);
|
||||||
|
|
||||||
const [mobile, setMobile] = useState(document.body.clientWidth < 500);
|
const [, screenWidth] = useScreenSize();
|
||||||
useResizeObserver(
|
const hideStickerBtn = screenWidth < 500;
|
||||||
document.body,
|
|
||||||
useCallback((entries) => {
|
|
||||||
const bodyEntry = getResizeObserverEntry(document.body, entries);
|
|
||||||
if (bodyEntry && bodyEntry.contentRect.width < 500) setMobile(true);
|
|
||||||
else setMobile(false);
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Transforms.insertFragment(editor, msgDraft);
|
Transforms.insertFragment(editor, msgDraft);
|
||||||
|
@ -515,7 +508,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
>
|
>
|
||||||
{(anchorRef) => (
|
{(anchorRef) => (
|
||||||
<>
|
<>
|
||||||
{!mobile && (
|
{!hideStickerBtn && (
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker}
|
aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker}
|
||||||
onClick={() => setEmojiBoardTab(EmojiBoardTab.Sticker)}
|
onClick={() => setEmojiBoardTab(EmojiBoardTab.Sticker)}
|
||||||
|
@ -532,7 +525,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
<IconButton
|
<IconButton
|
||||||
ref={anchorRef}
|
ref={anchorRef}
|
||||||
aria-pressed={
|
aria-pressed={
|
||||||
mobile ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
|
hideStickerBtn ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
|
||||||
}
|
}
|
||||||
onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)}
|
onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)}
|
||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
|
@ -542,7 +535,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
<Icon
|
<Icon
|
||||||
src={Icons.Smile}
|
src={Icons.Smile}
|
||||||
filled={
|
filled={
|
||||||
mobile ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
|
hideStickerBtn
|
||||||
|
? !!emojiBoardTab
|
||||||
|
: emojiBoardTab === EmojiBoardTab.Emoji
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
|
@ -486,7 +486,6 @@ function RoomViewContent({ roomInputRef, eventId, roomTimeline }) {
|
||||||
}, [newEvent]);
|
}, [newEvent]);
|
||||||
|
|
||||||
useResizeObserver(
|
useResizeObserver(
|
||||||
roomInputRef.current,
|
|
||||||
useCallback((entries) => {
|
useCallback((entries) => {
|
||||||
if (!roomInputRef.current) return;
|
if (!roomInputRef.current) return;
|
||||||
const editorBaseEntry = getResizeObserverEntry(roomInputRef.current, entries);
|
const editorBaseEntry = getResizeObserverEntry(roomInputRef.current, entries);
|
||||||
|
@ -497,7 +496,8 @@ function RoomViewContent({ roomInputRef, eventId, roomTimeline }) {
|
||||||
if (timelineScroll.bottom < 40 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
|
if (timelineScroll.bottom < 40 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
|
||||||
timelineScroll.scrollToBottom();
|
timelineScroll.scrollToBottom();
|
||||||
}
|
}
|
||||||
}, [roomInputRef])
|
}, [roomInputRef]),
|
||||||
|
useCallback(() => roomInputRef.current, [roomInputRef]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const listenKeyboard = useCallback((event) => {
|
const listenKeyboard = useCallback((event) => {
|
||||||
|
|
|
@ -8,8 +8,11 @@ import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { toggleRoomSettings, openReusableContextMenu, openNavigation } from '../../../client/action/navigation';
|
import {
|
||||||
import { togglePeopleDrawer } from '../../../client/action/settings';
|
toggleRoomSettings,
|
||||||
|
openReusableContextMenu,
|
||||||
|
openNavigation,
|
||||||
|
} from '../../../client/action/navigation';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
import { getEventCords } from '../../../util/common';
|
import { getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
|
@ -28,23 +31,26 @@ import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg
|
||||||
import BackArrowIC from '../../../../public/res/ic/outlined/chevron-left.svg';
|
import BackArrowIC from '../../../../public/res/ic/outlined/chevron-left.svg';
|
||||||
|
|
||||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
|
import { useSetSetting } from '../../state/hooks/settings';
|
||||||
|
import { settingsAtom } from '../../state/settings';
|
||||||
|
|
||||||
function RoomViewHeader({ roomId }) {
|
function RoomViewHeader({ roomId }) {
|
||||||
const [, forceUpdate] = useForceUpdate();
|
const [, forceUpdate] = useForceUpdate();
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const isDM = initMatrix.roomList.directs.has(roomId);
|
const isDM = initMatrix.roomList.directs.has(roomId);
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
|
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
||||||
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
||||||
avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc;
|
avatarSrc = isDM
|
||||||
|
? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop')
|
||||||
|
: avatarSrc;
|
||||||
const roomName = room.name;
|
const roomName = room.name;
|
||||||
|
|
||||||
const roomHeaderBtnRef = useRef(null);
|
const roomHeaderBtnRef = useRef(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const settingsToggle = (isVisibile) => {
|
const settingsToggle = (isVisibile) => {
|
||||||
const rawIcon = roomHeaderBtnRef.current.lastElementChild;
|
const rawIcon = roomHeaderBtnRef.current.lastElementChild;
|
||||||
rawIcon.style.transform = isVisibile
|
rawIcon.style.transform = isVisibile ? 'rotateX(180deg)' : 'rotateX(0deg)';
|
||||||
? 'rotateX(180deg)'
|
|
||||||
: 'rotateX(0deg)';
|
|
||||||
};
|
};
|
||||||
navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle);
|
navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle);
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -66,11 +72,9 @@ function RoomViewHeader({ roomId }) {
|
||||||
}, [roomId]);
|
}, [roomId]);
|
||||||
|
|
||||||
const openRoomOptions = (e) => {
|
const openRoomOptions = (e) => {
|
||||||
openReusableContextMenu(
|
openReusableContextMenu('bottom', getEventCords(e, '.ic-btn'), (closeMenu) => (
|
||||||
'bottom',
|
<RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />
|
||||||
getEventCords(e, '.ic-btn'),
|
));
|
||||||
(closeMenu) => <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -90,18 +94,34 @@ function RoomViewHeader({ roomId }) {
|
||||||
>
|
>
|
||||||
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
|
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
<Text variant="h2" weight="medium" primary>{twemojify(roomName)}</Text>
|
<Text variant="h2" weight="medium" primary>
|
||||||
|
{twemojify(roomName)}
|
||||||
|
</Text>
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
<RawIcon src={ChevronBottomIC} />
|
<RawIcon src={ChevronBottomIC} />
|
||||||
</button>
|
</button>
|
||||||
{mx.isRoomEncrypted(roomId) === false && <IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />}
|
{mx.isRoomEncrypted(roomId) === false && (
|
||||||
<IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
|
|
||||||
<IconButton className="room-header__members-btn" onClick={() => toggleRoomSettings(tabText.MEMBERS)} tooltip="Members" src={UserIC} />
|
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={openRoomOptions}
|
onClick={() => toggleRoomSettings(tabText.SEARCH)}
|
||||||
tooltip="Options"
|
tooltip="Search"
|
||||||
src={VerticalMenuIC}
|
src={SearchIC}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
className="room-header__drawer-btn"
|
||||||
|
onClick={() => {
|
||||||
|
setPeopleDrawer((t) => !t);
|
||||||
|
}}
|
||||||
|
tooltip="People"
|
||||||
|
src={UserIC}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
className="room-header__members-btn"
|
||||||
|
onClick={() => toggleRoomSettings(tabText.MEMBERS)}
|
||||||
|
tooltip="Members"
|
||||||
|
src={UserIC}
|
||||||
|
/>
|
||||||
|
<IconButton onClick={openRoomOptions} tooltip="Options" src={VerticalMenuIC} />
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,9 @@ export const useSetSetting = <K extends keyof Settings>(
|
||||||
) => {
|
) => {
|
||||||
const setterAtom = useMemo(
|
const setterAtom = useMemo(
|
||||||
() =>
|
() =>
|
||||||
atom<null, Settings[K]>(null, (get, set, value) => {
|
atom<null, Settings[K] | ((s: Settings[K]) => Settings[K])>(null, (get, set, value) => {
|
||||||
const s = { ...get(settingsAtom) };
|
const s = { ...get(settingsAtom) };
|
||||||
s[key] = value;
|
s[key] = typeof value === 'function' ? value(s[key]) : value;
|
||||||
set(settingsAtom, s);
|
set(settingsAtom, s);
|
||||||
}),
|
}),
|
||||||
[settingsAtom, key]
|
[settingsAtom, key]
|
||||||
|
@ -24,11 +24,10 @@ export const useSetSetting = <K extends keyof Settings>(
|
||||||
export const useSetting = <K extends keyof Settings>(
|
export const useSetting = <K extends keyof Settings>(
|
||||||
settingsAtom: WritableAtom<Settings, Settings>,
|
settingsAtom: WritableAtom<Settings, Settings>,
|
||||||
key: K
|
key: K
|
||||||
): [Settings[K], SetAtom<Settings[K], void>] => {
|
): [Settings[K], SetAtom<Settings[K] | ((s: Settings[K]) => Settings[K]), void>] => {
|
||||||
const selector = useMemo(() => (s: Settings) => s[key], [key]);
|
const selector = useMemo(() => (s: Settings) => s[key], [key]);
|
||||||
const setting = useAtomValue(selectAtom(settingsAtom, selector));
|
const setting = useAtomValue(selectAtom(settingsAtom, selector));
|
||||||
|
|
||||||
const setter = useSetSetting(settingsAtom, key);
|
const setter = useSetSetting(settingsAtom, key);
|
||||||
|
|
||||||
return [setting, setter];
|
return [setting, setter];
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue