mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +03:00 
			
		
		
		
	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",
 | 
			
		||||
        "@khanacademy/simple-markdown": "0.8.6",
 | 
			
		||||
        "@matrix-org/olm": "3.2.14",
 | 
			
		||||
        "@tanstack/react-virtual": "3.0.0-beta.54",
 | 
			
		||||
        "@tippyjs/react": "4.2.6",
 | 
			
		||||
        "@vanilla-extract/css": "1.9.3",
 | 
			
		||||
        "@vanilla-extract/recipes": "0.3.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +38,7 @@
 | 
			
		|||
        "linkify-html": "4.0.2",
 | 
			
		||||
        "linkifyjs": "4.0.2",
 | 
			
		||||
        "matrix-js-sdk": "24.1.0",
 | 
			
		||||
        "millify": "6.1.0",
 | 
			
		||||
        "prop-types": "15.8.1",
 | 
			
		||||
        "react": "17.0.2",
 | 
			
		||||
        "react-autosize-textarea": "7.1.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -1106,6 +1108,30 @@
 | 
			
		|||
      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
 | 
			
		||||
      "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": {
 | 
			
		||||
      "version": "4.2.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -1669,7 +1695,6 @@
 | 
			
		|||
      "version": "5.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -2058,6 +2083,19 @@
 | 
			
		|||
      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
 | 
			
		||||
      "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": {
 | 
			
		||||
      "version": "1.9.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -3349,6 +3387,14 @@
 | 
			
		|||
        "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": {
 | 
			
		||||
      "version": "1.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -3771,6 +3817,14 @@
 | 
			
		|||
        "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": {
 | 
			
		||||
      "version": "4.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -4286,6 +4340,17 @@
 | 
			
		|||
        "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": {
 | 
			
		||||
      "version": "1.4.4",
 | 
			
		||||
      "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"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "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": {
 | 
			
		||||
      "version": "0.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -5256,6 +5329,24 @@
 | 
			
		|||
        "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": {
 | 
			
		||||
      "version": "4.0.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -5307,7 +5398,6 @@
 | 
			
		|||
      "version": "6.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "ansi-regex": "^5.0.1"
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			@ -6166,12 +6256,66 @@
 | 
			
		|||
        "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": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
 | 
			
		||||
      "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": {
 | 
			
		||||
      "version": "4.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -6186,6 +6330,31 @@
 | 
			
		|||
        "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": {
 | 
			
		||||
      "version": "0.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,7 @@
 | 
			
		|||
    "@fontsource/roboto": "4.5.8",
 | 
			
		||||
    "@khanacademy/simple-markdown": "0.8.6",
 | 
			
		||||
    "@matrix-org/olm": "3.2.14",
 | 
			
		||||
    "@tanstack/react-virtual": "3.0.0-beta.54",
 | 
			
		||||
    "@tippyjs/react": "4.2.6",
 | 
			
		||||
    "@vanilla-extract/css": "1.9.3",
 | 
			
		||||
    "@vanilla-extract/recipes": "0.3.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +48,7 @@
 | 
			
		|||
    "linkify-html": "4.0.2",
 | 
			
		||||
    "linkifyjs": "4.0.2",
 | 
			
		||||
    "matrix-js-sdk": "24.1.0",
 | 
			
		||||
    "millify": "6.1.0",
 | 
			
		||||
    "prop-types": "15.8.1",
 | 
			
		||||
    "react": "17.0.2",
 | 
			
		||||
    "react-autosize-textarea": "7.1.0",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,12 +60,13 @@ export function EmoticonAutocomplete({
 | 
			
		|||
    );
 | 
			
		||||
  }, [imagePacks]);
 | 
			
		||||
 | 
			
		||||
  const [result, search] = useAsyncSearch(searchList, getEmoticonStr, SEARCH_OPTIONS);
 | 
			
		||||
  const [result, search, resetSearch] = useAsyncSearch(searchList, getEmoticonStr, SEARCH_OPTIONS);
 | 
			
		||||
  const autoCompleteEmoticon = result ? result.items : recentEmoji;
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    search(query.text);
 | 
			
		||||
  }, [query.text, search]);
 | 
			
		||||
    if (query.text) search(query.text);
 | 
			
		||||
    else resetSearch();
 | 
			
		||||
  }, [query.text, search, resetSearch]);
 | 
			
		||||
 | 
			
		||||
  const handleAutocomplete: EmoticonCompleteHandler = (key, shortcode) => {
 | 
			
		||||
    const emoticonEl = createEmoticonElement(key, shortcode);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -81,7 +81,7 @@ export function RoomMentionAutocomplete({
 | 
			
		|||
    return [...spaces, ...rooms, ...directs].sort(roomIdByActivity);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const [result, search] = useAsyncSearch(
 | 
			
		||||
  const [result, search, resetSearch] = useAsyncSearch(
 | 
			
		||||
    allRoomId,
 | 
			
		||||
    useCallback(
 | 
			
		||||
      (rId) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -99,8 +99,9 @@ export function RoomMentionAutocomplete({
 | 
			
		|||
  const autoCompleteRoomIds = result ? result.items : allRoomId.slice(0, 20);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    search(query.text);
 | 
			
		||||
  }, [query.text, search]);
 | 
			
		||||
    if (query.text) search(query.text);
 | 
			
		||||
    else resetSearch();
 | 
			
		||||
  }, [query.text, search, resetSearch]);
 | 
			
		||||
 | 
			
		||||
  const handleAutocomplete: MentionAutoCompleteHandler = (roomAliasOrId, name) => {
 | 
			
		||||
    const mentionEl = createMentionElement(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -94,12 +94,13 @@ export function UserMentionAutocomplete({
 | 
			
		|||
  const roomAliasOrId = room?.getCanonicalAlias() || 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);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    search(query.text);
 | 
			
		||||
  }, [query.text, search]);
 | 
			
		||||
    if (query.text) search(query.text);
 | 
			
		||||
    else resetSearch();
 | 
			
		||||
  }, [query.text, search, resetSearch]);
 | 
			
		||||
 | 
			
		||||
  const handleAutocomplete: MentionAutoCompleteHandler = (uId, name) => {
 | 
			
		||||
    const mentionEl = createMentionElement(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -647,15 +647,20 @@ export function EmojiBoard({
 | 
			
		|||
    return list;
 | 
			
		||||
  }, [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(
 | 
			
		||||
    useCallback(
 | 
			
		||||
      (evt) => {
 | 
			
		||||
        const term = evt.target.value;
 | 
			
		||||
        search(term);
 | 
			
		||||
        if (term) search(term);
 | 
			
		||||
        else resetSearch();
 | 
			
		||||
      },
 | 
			
		||||
      [search]
 | 
			
		||||
      [search, resetSearch]
 | 
			
		||||
    ),
 | 
			
		||||
    { wait: 200 }
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,11 +25,13 @@ export type UseAsyncSearchResult<TSearchItem extends object | string | number> =
 | 
			
		|||
  items: TSearchItem[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type SearchResetHandler = () => void;
 | 
			
		||||
 | 
			
		||||
export const useAsyncSearch = <TSearchItem extends object | string | number>(
 | 
			
		||||
  list: TSearchItem[],
 | 
			
		||||
  getItemStr: SearchItemStrGetter<TSearchItem>,
 | 
			
		||||
  options?: UseAsyncSearchOptions
 | 
			
		||||
): [UseAsyncSearchResult<TSearchItem> | undefined, AsyncSearchHandler] => {
 | 
			
		||||
): [UseAsyncSearchResult<TSearchItem> | undefined, AsyncSearchHandler, SearchResetHandler] => {
 | 
			
		||||
  const [result, setResult] = useState<UseAsyncSearchResult<TSearchItem>>();
 | 
			
		||||
 | 
			
		||||
  const [searchCallback, terminateSearch] = useMemo(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -51,7 +53,7 @@ export const useAsyncSearch = <TSearchItem extends object | string | number>(
 | 
			
		|||
    const handleResult: ResultHandler<TSearchItem> = (results, query) =>
 | 
			
		||||
      setResult({
 | 
			
		||||
        query,
 | 
			
		||||
        items: results,
 | 
			
		||||
        items: [...results],
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    return AsyncSearch(list, handleMatch, handleResult, options);
 | 
			
		||||
| 
						 | 
				
			
			@ -60,15 +62,16 @@ export const useAsyncSearch = <TSearchItem extends object | string | number>(
 | 
			
		|||
  const searchHandler: AsyncSearchHandler = useCallback(
 | 
			
		||||
    (query) => {
 | 
			
		||||
      const normalizedQuery = normalize(query, options?.normalizeOptions);
 | 
			
		||||
      if (!normalizedQuery) {
 | 
			
		||||
        setResult(undefined);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      searchCallback(normalizedQuery);
 | 
			
		||||
    },
 | 
			
		||||
    [searchCallback, options?.normalizeOptions]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const resetHandler: SearchResetHandler = useCallback(() => {
 | 
			
		||||
    terminateSearch();
 | 
			
		||||
    setResult(undefined);
 | 
			
		||||
  }, [terminateSearch]);
 | 
			
		||||
 | 
			
		||||
  useEffect(
 | 
			
		||||
    () => () => {
 | 
			
		||||
      // terminate any ongoing search request on unmount.
 | 
			
		||||
| 
						 | 
				
			
			@ -77,5 +80,5 @@ export const useAsyncSearch = <TSearchItem extends object | string | number>(
 | 
			
		|||
    [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);
 | 
			
		||||
 | 
			
		||||
export const useResizeObserver = (
 | 
			
		||||
  element: Element | null,
 | 
			
		||||
  onResizeCallback: OnResizeCallback
 | 
			
		||||
  onResizeCallback: OnResizeCallback,
 | 
			
		||||
  observeElement?: Element | null | (() => Element | null)
 | 
			
		||||
): ResizeObserver => {
 | 
			
		||||
  const resizeObserver = useMemo(() => new ResizeObserver(onResizeCallback), [onResizeCallback]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const element = typeof observeElement === 'function' ? observeElement() : observeElement;
 | 
			
		||||
    if (element) resizeObserver.observe(element);
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (element) resizeObserver.unobserve(element);
 | 
			
		||||
    };
 | 
			
		||||
  }, [resizeObserver, element]);
 | 
			
		||||
  }, [resizeObserver, observeElement]);
 | 
			
		||||
 | 
			
		||||
  return resizeObserver;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,23 +1,25 @@
 | 
			
		|||
import { MatrixClient, MatrixEvent, RoomMember, RoomMemberEvent } from 'matrix-js-sdk';
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { useAlive } from './useAlive';
 | 
			
		||||
 | 
			
		||||
export const useRoomMembers = (mx: MatrixClient, roomId: string): RoomMember[] => {
 | 
			
		||||
  const [members, setMembers] = useState<RoomMember[]>([]);
 | 
			
		||||
  const alive = useAlive();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const room = mx.getRoom(roomId);
 | 
			
		||||
    let loadingMembers = true;
 | 
			
		||||
    let disposed = false;
 | 
			
		||||
 | 
			
		||||
    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());
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (room) {
 | 
			
		||||
      updateMemberList();
 | 
			
		||||
      setMembers(room.getMembers());
 | 
			
		||||
      room.loadMembersIfNeeded().then(() => {
 | 
			
		||||
        if (!alive) return;
 | 
			
		||||
        loadingMembers = false;
 | 
			
		||||
        if (disposed) return;
 | 
			
		||||
        updateMemberList();
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -25,10 +27,11 @@ export const useRoomMembers = (mx: MatrixClient, roomId: string): RoomMember[] =
 | 
			
		|||
    mx.on(RoomMemberEvent.Membership, updateMemberList);
 | 
			
		||||
    mx.on(RoomMemberEvent.PowerLevel, updateMemberList);
 | 
			
		||||
    return () => {
 | 
			
		||||
      disposed = true;
 | 
			
		||||
      mx.removeListener(RoomMemberEvent.Membership, updateMemberList);
 | 
			
		||||
      mx.removeListener(RoomMemberEvent.PowerLevel, updateMemberList);
 | 
			
		||||
    };
 | 
			
		||||
  }, [mx, roomId, alive]);
 | 
			
		||||
  }, [mx, roomId]);
 | 
			
		||||
 | 
			
		||||
  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 './Room.scss';
 | 
			
		||||
import { Line } from 'folds';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import settings from '../../../client/state/settings';
 | 
			
		||||
import RoomTimeline from '../../../client/state/RoomTimeline';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import { openNavigation } from '../../../client/action/navigation';
 | 
			
		||||
| 
						 | 
				
			
			@ -11,7 +11,10 @@ import { openNavigation } from '../../../client/action/navigation';
 | 
			
		|||
import Welcome from '../welcome/Welcome';
 | 
			
		||||
import RoomView from './RoomView';
 | 
			
		||||
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() {
 | 
			
		||||
  const [roomInfo, setRoomInfo] = useState({
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +22,8 @@ function Room() {
 | 
			
		|||
    roomTimeline: null,
 | 
			
		||||
    eventId: null,
 | 
			
		||||
  });
 | 
			
		||||
  const [isDrawer, setIsDrawer] = useState(settings.isPeopleDrawer);
 | 
			
		||||
  const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
 | 
			
		||||
  const [screenSize] = useScreenSize();
 | 
			
		||||
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -49,14 +53,6 @@ function Room() {
 | 
			
		|||
    };
 | 
			
		||||
  }, [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;
 | 
			
		||||
  if (roomTimeline === null) {
 | 
			
		||||
    setTimeout(() => openNavigation());
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +65,13 @@ function Room() {
 | 
			
		|||
        <RoomSettings roomId={roomTimeline.roomId} />
 | 
			
		||||
        <RoomView room={room} roomTimeline={roomTimeline} eventId={eventId} />
 | 
			
		||||
      </div>
 | 
			
		||||
      {isDrawer && <PeopleDrawer roomId={roomTimeline.roomId} />}
 | 
			
		||||
 | 
			
		||||
      {screenSize === ScreenSize.Desktop && isDrawer && (
 | 
			
		||||
        <>
 | 
			
		||||
          <Line variant="Background" direction="Vertical" size="300" />
 | 
			
		||||
          <MembersDrawer room={room} />
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -97,7 +97,7 @@ import { MessageReply } from '../../molecules/message/Message';
 | 
			
		|||
import colorMXID from '../../../util/colorMXID';
 | 
			
		||||
import { parseReplyBody, parseReplyFormattedBody } from '../../utils/room';
 | 
			
		||||
import { sanitizeText } from '../../utils/sanitize';
 | 
			
		||||
import { getResizeObserverEntry, useResizeObserver } from '../../hooks/useResizeObserver';
 | 
			
		||||
import { useScreenSize } from '../../hooks/useScreenSize';
 | 
			
		||||
 | 
			
		||||
interface RoomInputProps {
 | 
			
		||||
  roomViewRef: RefObject<HTMLElement>;
 | 
			
		||||
| 
						 | 
				
			
			@ -161,15 +161,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
 | 
			
		|||
    const handlePaste = useFilePasteHandler(handleFiles);
 | 
			
		||||
    const dropZoneVisible = useFileDropZone(roomViewRef, handleFiles);
 | 
			
		||||
 | 
			
		||||
    const [mobile, setMobile] = useState(document.body.clientWidth < 500);
 | 
			
		||||
    useResizeObserver(
 | 
			
		||||
      document.body,
 | 
			
		||||
      useCallback((entries) => {
 | 
			
		||||
        const bodyEntry = getResizeObserverEntry(document.body, entries);
 | 
			
		||||
        if (bodyEntry && bodyEntry.contentRect.width < 500) setMobile(true);
 | 
			
		||||
        else setMobile(false);
 | 
			
		||||
      }, [])
 | 
			
		||||
    );
 | 
			
		||||
    const [, screenWidth] = useScreenSize();
 | 
			
		||||
    const hideStickerBtn = screenWidth < 500;
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      Transforms.insertFragment(editor, msgDraft);
 | 
			
		||||
| 
						 | 
				
			
			@ -515,7 +508,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
 | 
			
		|||
                  >
 | 
			
		||||
                    {(anchorRef) => (
 | 
			
		||||
                      <>
 | 
			
		||||
                        {!mobile && (
 | 
			
		||||
                        {!hideStickerBtn && (
 | 
			
		||||
                          <IconButton
 | 
			
		||||
                            aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker}
 | 
			
		||||
                            onClick={() => setEmojiBoardTab(EmojiBoardTab.Sticker)}
 | 
			
		||||
| 
						 | 
				
			
			@ -532,7 +525,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
 | 
			
		|||
                        <IconButton
 | 
			
		||||
                          ref={anchorRef}
 | 
			
		||||
                          aria-pressed={
 | 
			
		||||
                            mobile ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
 | 
			
		||||
                            hideStickerBtn ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
 | 
			
		||||
                          }
 | 
			
		||||
                          onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)}
 | 
			
		||||
                          variant="SurfaceVariant"
 | 
			
		||||
| 
						 | 
				
			
			@ -542,7 +535,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
 | 
			
		|||
                          <Icon
 | 
			
		||||
                            src={Icons.Smile}
 | 
			
		||||
                            filled={
 | 
			
		||||
                              mobile ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
 | 
			
		||||
                              hideStickerBtn
 | 
			
		||||
                                ? !!emojiBoardTab
 | 
			
		||||
                                : emojiBoardTab === EmojiBoardTab.Emoji
 | 
			
		||||
                            }
 | 
			
		||||
                          />
 | 
			
		||||
                        </IconButton>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -486,7 +486,6 @@ function RoomViewContent({ roomInputRef, eventId, roomTimeline }) {
 | 
			
		|||
  }, [newEvent]);
 | 
			
		||||
 | 
			
		||||
  useResizeObserver(
 | 
			
		||||
    roomInputRef.current,
 | 
			
		||||
    useCallback((entries) => {
 | 
			
		||||
      if (!roomInputRef.current) return;
 | 
			
		||||
      const editorBaseEntry = getResizeObserverEntry(roomInputRef.current, entries);
 | 
			
		||||
| 
						 | 
				
			
			@ -497,7 +496,8 @@ function RoomViewContent({ roomInputRef, eventId, roomTimeline }) {
 | 
			
		|||
      if (timelineScroll.bottom < 40 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
 | 
			
		||||
        timelineScroll.scrollToBottom();
 | 
			
		||||
      }
 | 
			
		||||
    }, [roomInputRef])
 | 
			
		||||
    }, [roomInputRef]),
 | 
			
		||||
    useCallback(() => roomInputRef.current, [roomInputRef]),
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
  const listenKeyboard = useCallback((event) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,8 +8,11 @@ import { blurOnBubbling } from '../../atoms/button/script';
 | 
			
		|||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import { toggleRoomSettings, openReusableContextMenu, openNavigation } from '../../../client/action/navigation';
 | 
			
		||||
import { togglePeopleDrawer } from '../../../client/action/settings';
 | 
			
		||||
import {
 | 
			
		||||
  toggleRoomSettings,
 | 
			
		||||
  openReusableContextMenu,
 | 
			
		||||
  openNavigation,
 | 
			
		||||
} from '../../../client/action/navigation';
 | 
			
		||||
import colorMXID from '../../../util/colorMXID';
 | 
			
		||||
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 { useForceUpdate } from '../../hooks/useForceUpdate';
 | 
			
		||||
import { useSetSetting } from '../../state/hooks/settings';
 | 
			
		||||
import { settingsAtom } from '../../state/settings';
 | 
			
		||||
 | 
			
		||||
function RoomViewHeader({ roomId }) {
 | 
			
		||||
  const [, forceUpdate] = useForceUpdate();
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const isDM = initMatrix.roomList.directs.has(roomId);
 | 
			
		||||
  const room = mx.getRoom(roomId);
 | 
			
		||||
  const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
 | 
			
		||||
  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 roomHeaderBtnRef = useRef(null);
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const settingsToggle = (isVisibile) => {
 | 
			
		||||
      const rawIcon = roomHeaderBtnRef.current.lastElementChild;
 | 
			
		||||
      rawIcon.style.transform = isVisibile
 | 
			
		||||
        ? 'rotateX(180deg)'
 | 
			
		||||
        : 'rotateX(0deg)';
 | 
			
		||||
      rawIcon.style.transform = isVisibile ? 'rotateX(180deg)' : 'rotateX(0deg)';
 | 
			
		||||
    };
 | 
			
		||||
    navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle);
 | 
			
		||||
    return () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -66,11 +72,9 @@ function RoomViewHeader({ roomId }) {
 | 
			
		|||
  }, [roomId]);
 | 
			
		||||
 | 
			
		||||
  const openRoomOptions = (e) => {
 | 
			
		||||
    openReusableContextMenu(
 | 
			
		||||
      'bottom',
 | 
			
		||||
      getEventCords(e, '.ic-btn'),
 | 
			
		||||
      (closeMenu) => <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />,
 | 
			
		||||
    );
 | 
			
		||||
    openReusableContextMenu('bottom', getEventCords(e, '.ic-btn'), (closeMenu) => (
 | 
			
		||||
      <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />
 | 
			
		||||
    ));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
| 
						 | 
				
			
			@ -90,18 +94,34 @@ function RoomViewHeader({ roomId }) {
 | 
			
		|||
      >
 | 
			
		||||
        <Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
 | 
			
		||||
        <TitleWrapper>
 | 
			
		||||
          <Text variant="h2" weight="medium" primary>{twemojify(roomName)}</Text>
 | 
			
		||||
          <Text variant="h2" weight="medium" primary>
 | 
			
		||||
            {twemojify(roomName)}
 | 
			
		||||
          </Text>
 | 
			
		||||
        </TitleWrapper>
 | 
			
		||||
        <RawIcon src={ChevronBottomIC} />
 | 
			
		||||
      </button>
 | 
			
		||||
      {mx.isRoomEncrypted(roomId) === false && <IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />}
 | 
			
		||||
      <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} />
 | 
			
		||||
      {mx.isRoomEncrypted(roomId) === false && (
 | 
			
		||||
        <IconButton
 | 
			
		||||
          onClick={() => toggleRoomSettings(tabText.SEARCH)}
 | 
			
		||||
          tooltip="Search"
 | 
			
		||||
          src={SearchIC}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      <IconButton
 | 
			
		||||
        onClick={openRoomOptions}
 | 
			
		||||
        tooltip="Options"
 | 
			
		||||
        src={VerticalMenuIC}
 | 
			
		||||
        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>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,9 +10,9 @@ export const useSetSetting = <K extends keyof Settings>(
 | 
			
		|||
) => {
 | 
			
		||||
  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) };
 | 
			
		||||
        s[key] = value;
 | 
			
		||||
        s[key] = typeof value === 'function' ? value(s[key]) : value;
 | 
			
		||||
        set(settingsAtom, s);
 | 
			
		||||
      }),
 | 
			
		||||
    [settingsAtom, key]
 | 
			
		||||
| 
						 | 
				
			
			@ -24,11 +24,10 @@ export const useSetSetting = <K extends keyof Settings>(
 | 
			
		|||
export const useSetting = <K extends keyof Settings>(
 | 
			
		||||
  settingsAtom: WritableAtom<Settings, Settings>,
 | 
			
		||||
  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 setting = useAtomValue(selectAtom(settingsAtom, selector));
 | 
			
		||||
 | 
			
		||||
  const setter = useSetSetting(settingsAtom, key);
 | 
			
		||||
 | 
			
		||||
  return [setting, setter];
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue