mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +03:00 
			
		
		
		
	(chore) remove outdated code (#1765)
* optimize room typing members hook * remove unused code - WIP * remove old code from initMatrix * remove twemojify function * remove old sanitize util * delete old markdown util * delete Math atom component * uninstall unused dependencies * remove old notification system * decrypt message in inbox notification center and fix refresh in background * improve notification --------- Co-authored-by: Krishan <33421343+kfiven@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									60e022035f
								
							
						
					
					
						commit
						4f09e6bbb5
					
				
					 147 changed files with 1164 additions and 15330 deletions
				
			
		| 
						 | 
				
			
			@ -90,12 +90,6 @@
 | 
			
		|||
      window.global ||= window;
 | 
			
		||||
    </script>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
    <audio id="notificationSound">
 | 
			
		||||
      <source src="./public/sound/notification.ogg" type="audio/ogg" />
 | 
			
		||||
    </audio>
 | 
			
		||||
    <audio id="inviteSound">
 | 
			
		||||
      <source src="./public/sound/invite.ogg" type="audio/ogg" />
 | 
			
		||||
    </audio>
 | 
			
		||||
    <script type="module" src="./src/index.tsx"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										188
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										188
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -13,7 +13,6 @@
 | 
			
		|||
        "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
 | 
			
		||||
        "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
 | 
			
		||||
        "@fontsource/inter": "4.5.14",
 | 
			
		||||
        "@khanacademy/simple-markdown": "0.8.6",
 | 
			
		||||
        "@matrix-org/olm": "3.2.14",
 | 
			
		||||
        "@tanstack/react-query": "5.24.1",
 | 
			
		||||
        "@tanstack/react-query-devtools": "5.24.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -41,8 +40,6 @@
 | 
			
		|||
        "immer": "9.0.16",
 | 
			
		||||
        "is-hotkey": "0.2.0",
 | 
			
		||||
        "jotai": "2.6.0",
 | 
			
		||||
        "katex": "0.16.10",
 | 
			
		||||
        "linkify-html": "4.0.2",
 | 
			
		||||
        "linkify-react": "4.1.1",
 | 
			
		||||
        "linkifyjs": "4.0.2",
 | 
			
		||||
        "matrix-js-sdk": "29.1.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -54,8 +51,6 @@
 | 
			
		|||
        "react-aria": "3.29.1",
 | 
			
		||||
        "react-autosize-textarea": "7.1.0",
 | 
			
		||||
        "react-blurhash": "0.2.0",
 | 
			
		||||
        "react-dnd": "16.0.1",
 | 
			
		||||
        "react-dnd-html5-backend": "16.0.1",
 | 
			
		||||
        "react-dom": "18.2.0",
 | 
			
		||||
        "react-error-boundary": "4.0.10",
 | 
			
		||||
        "react-google-recaptcha": "2.1.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -67,7 +62,6 @@
 | 
			
		|||
        "slate-history": "0.93.0",
 | 
			
		||||
        "slate-react": "0.98.4",
 | 
			
		||||
        "tippy.js": "6.3.7",
 | 
			
		||||
        "twemoji": "14.0.2",
 | 
			
		||||
        "ua-parser-js": "1.0.35"
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
| 
						 | 
				
			
			@ -1109,18 +1103,6 @@
 | 
			
		|||
      "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
 | 
			
		||||
      "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@khanacademy/simple-markdown": {
 | 
			
		||||
      "version": "0.8.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@khanacademy/simple-markdown/-/simple-markdown-0.8.6.tgz",
 | 
			
		||||
      "integrity": "sha512-mAUlR9lchzfqunR89pFvNI51jQKsMpJeWYsYWw0DQcUXczn/T/V6510utgvm7X0N3zN87j1SvuKk8cMbl9IAFw==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/react": ">=16.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "react": "16.14.0",
 | 
			
		||||
        "react-dom": "16.14.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mapbox/node-pre-gyp": {
 | 
			
		||||
      "version": "1.0.11",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -1942,21 +1924,6 @@
 | 
			
		|||
        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@react-dnd/asap": {
 | 
			
		||||
      "version": "5.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@react-dnd/invariant": {
 | 
			
		||||
      "version": "4.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@react-dnd/shallowequal": {
 | 
			
		||||
      "version": "4.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@react-stately/calendar": {
 | 
			
		||||
      "version": "3.4.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.4.1.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -3307,12 +3274,14 @@
 | 
			
		|||
    "node_modules/@types/prop-types": {
 | 
			
		||||
      "version": "15.7.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
 | 
			
		||||
      "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
 | 
			
		||||
      "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/react": {
 | 
			
		||||
      "version": "18.2.39",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
 | 
			
		||||
      "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/prop-types": "*",
 | 
			
		||||
        "@types/scheduler": "*",
 | 
			
		||||
| 
						 | 
				
			
			@ -3354,7 +3323,8 @@
 | 
			
		|||
    "node_modules/@types/scheduler": {
 | 
			
		||||
      "version": "0.16.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
 | 
			
		||||
      "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
 | 
			
		||||
      "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/semver": {
 | 
			
		||||
      "version": "7.3.13",
 | 
			
		||||
| 
						 | 
				
			
			@ -4419,14 +4389,6 @@
 | 
			
		|||
        "color-support": "bin.js"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/commander": {
 | 
			
		||||
      "version": "8.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/compute-scroll-into-view": {
 | 
			
		||||
      "version": "1.0.20",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -4723,16 +4685,6 @@
 | 
			
		|||
        "url": "https://github.com/sponsors/wooorm"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/dnd-core": {
 | 
			
		||||
      "version": "16.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@react-dnd/asap": "^5.0.1",
 | 
			
		||||
        "@react-dnd/invariant": "^4.0.1",
 | 
			
		||||
        "redux": "^4.2.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/doctrine": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -5559,7 +5511,8 @@
 | 
			
		|||
    "node_modules/fast-deep-equal": {
 | 
			
		||||
      "version": "3.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
 | 
			
		||||
      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
 | 
			
		||||
      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/fast-glob": {
 | 
			
		||||
      "version": "3.2.12",
 | 
			
		||||
| 
						 | 
				
			
			@ -5796,27 +5749,6 @@
 | 
			
		|||
        "react": ">=16.8.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/fs-extra": {
 | 
			
		||||
      "version": "8.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "graceful-fs": "^4.2.0",
 | 
			
		||||
        "jsonfile": "^4.0.0",
 | 
			
		||||
        "universalify": "^0.1.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6 <7 || >=8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/fs-extra/node_modules/jsonfile": {
 | 
			
		||||
      "version": "4.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
 | 
			
		||||
      "optionalDependencies": {
 | 
			
		||||
        "graceful-fs": "^4.1.6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/fs-minipass": {
 | 
			
		||||
      "version": "2.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -6060,7 +5992,8 @@
 | 
			
		|||
    "node_modules/graceful-fs": {
 | 
			
		||||
      "version": "4.2.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
 | 
			
		||||
      "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
 | 
			
		||||
      "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/grapheme-splitter": {
 | 
			
		||||
      "version": "1.0.4",
 | 
			
		||||
| 
						 | 
				
			
			@ -6749,17 +6682,6 @@
 | 
			
		|||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/jsonfile": {
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "universalify": "^0.1.2"
 | 
			
		||||
      },
 | 
			
		||||
      "optionalDependencies": {
 | 
			
		||||
        "graceful-fs": "^4.1.6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/jsx-ast-utils": {
 | 
			
		||||
      "version": "3.3.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -6778,21 +6700,6 @@
 | 
			
		|||
      "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/katex": {
 | 
			
		||||
      "version": "0.16.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
 | 
			
		||||
      "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
 | 
			
		||||
      "funding": [
 | 
			
		||||
        "https://opencollective.com/katex",
 | 
			
		||||
        "https://github.com/sponsors/katex"
 | 
			
		||||
      ],
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "commander": "^8.3.0"
 | 
			
		||||
      },
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "katex": "cli.js"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/language-subtag-registry": {
 | 
			
		||||
      "version": "0.3.22",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -6854,14 +6761,6 @@
 | 
			
		|||
        "node": ">= 4.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/linkify-html": {
 | 
			
		||||
      "version": "4.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/linkify-html/-/linkify-html-4.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-YcN3tsyutK2Y/uSuoG0zne8FQdoqzrAgNU5ko0DWE7M2oQ3ms4z/202f2W4TvRm9uxKdrsWAullfynANLaVMqw==",
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "linkifyjs": "^4.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/linkify-react": {
 | 
			
		||||
      "version": "4.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.1.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -7766,43 +7665,6 @@
 | 
			
		|||
        "react": ">=15"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/react-dnd": {
 | 
			
		||||
      "version": "16.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@react-dnd/invariant": "^4.0.1",
 | 
			
		||||
        "@react-dnd/shallowequal": "^4.0.1",
 | 
			
		||||
        "dnd-core": "^16.0.1",
 | 
			
		||||
        "fast-deep-equal": "^3.1.3",
 | 
			
		||||
        "hoist-non-react-statics": "^3.3.2"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@types/hoist-non-react-statics": ">= 3.3.1",
 | 
			
		||||
        "@types/node": ">= 12",
 | 
			
		||||
        "@types/react": ">= 16",
 | 
			
		||||
        "react": ">= 16.14"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependenciesMeta": {
 | 
			
		||||
        "@types/hoist-non-react-statics": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        },
 | 
			
		||||
        "@types/node": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        },
 | 
			
		||||
        "@types/react": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/react-dnd-html5-backend": {
 | 
			
		||||
      "version": "16.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "dnd-core": "^16.0.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/react-dom": {
 | 
			
		||||
      "version": "18.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -7950,14 +7812,6 @@
 | 
			
		|||
        "node": ">=8.10.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/redux": {
 | 
			
		||||
      "version": "4.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.9.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/regexp.prototype.flags": {
 | 
			
		||||
      "version": "1.5.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -8711,22 +8565,6 @@
 | 
			
		|||
        "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/twemoji": {
 | 
			
		||||
      "version": "14.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/twemoji/-/twemoji-14.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-BzOoXIe1QVdmsUmZ54xbEH+8AgtOKUiG53zO5vVP2iUu6h5u9lN15NcuS6te4OY96qx0H7JK9vjjl9WQbkTRuA==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "fs-extra": "^8.0.1",
 | 
			
		||||
        "jsonfile": "^5.0.0",
 | 
			
		||||
        "twemoji-parser": "14.0.0",
 | 
			
		||||
        "universalify": "^0.1.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/twemoji-parser": {
 | 
			
		||||
      "version": "14.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-14.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/type-check": {
 | 
			
		||||
      "version": "0.4.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -8875,14 +8713,6 @@
 | 
			
		|||
      "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz",
 | 
			
		||||
      "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/universalify": {
 | 
			
		||||
      "version": "0.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 4.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/update-browserslist-db": {
 | 
			
		||||
      "version": "1.0.13",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,7 +24,6 @@
 | 
			
		|||
    "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
 | 
			
		||||
    "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
 | 
			
		||||
    "@fontsource/inter": "4.5.14",
 | 
			
		||||
    "@khanacademy/simple-markdown": "0.8.6",
 | 
			
		||||
    "@matrix-org/olm": "3.2.14",
 | 
			
		||||
    "@tanstack/react-query": "5.24.1",
 | 
			
		||||
    "@tanstack/react-query-devtools": "5.24.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -52,8 +51,6 @@
 | 
			
		|||
    "immer": "9.0.16",
 | 
			
		||||
    "is-hotkey": "0.2.0",
 | 
			
		||||
    "jotai": "2.6.0",
 | 
			
		||||
    "katex": "0.16.10",
 | 
			
		||||
    "linkify-html": "4.0.2",
 | 
			
		||||
    "linkify-react": "4.1.1",
 | 
			
		||||
    "linkifyjs": "4.0.2",
 | 
			
		||||
    "matrix-js-sdk": "29.1.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -65,8 +62,6 @@
 | 
			
		|||
    "react-aria": "3.29.1",
 | 
			
		||||
    "react-autosize-textarea": "7.1.0",
 | 
			
		||||
    "react-blurhash": "0.2.0",
 | 
			
		||||
    "react-dnd": "16.0.1",
 | 
			
		||||
    "react-dnd-html5-backend": "16.0.1",
 | 
			
		||||
    "react-dom": "18.2.0",
 | 
			
		||||
    "react-error-boundary": "4.0.10",
 | 
			
		||||
    "react-google-recaptcha": "2.1.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -78,7 +73,6 @@
 | 
			
		|||
    "slate-history": "0.93.0",
 | 
			
		||||
    "slate-react": "0.98.4",
 | 
			
		||||
    "tippy.js": "6.3.7",
 | 
			
		||||
    "twemoji": "14.0.2",
 | 
			
		||||
    "ua-parser-js": "1.0.35"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,17 +2,13 @@ import React from 'react';
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import './Avatar.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import Text from '../text/Text';
 | 
			
		||||
import RawIcon from '../system-icons/RawIcon';
 | 
			
		||||
 | 
			
		||||
import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg';
 | 
			
		||||
import { avatarInitials } from '../../../util/common';
 | 
			
		||||
 | 
			
		||||
const Avatar = React.forwardRef(({
 | 
			
		||||
  text, bgColor, iconSrc, iconColor, imageSrc, size,
 | 
			
		||||
}, ref) => {
 | 
			
		||||
const Avatar = React.forwardRef(({ text, bgColor, iconSrc, iconColor, imageSrc, size }, ref) => {
 | 
			
		||||
  let textSize = 's1';
 | 
			
		||||
  if (size === 'large') textSize = 'h1';
 | 
			
		||||
  if (size === 'small') textSize = 'b1';
 | 
			
		||||
| 
						 | 
				
			
			@ -20,34 +16,34 @@ const Avatar = React.forwardRef(({
 | 
			
		|||
 | 
			
		||||
  return (
 | 
			
		||||
    <div ref={ref} className={`avatar-container avatar-container__${size} noselect`}>
 | 
			
		||||
      {
 | 
			
		||||
        imageSrc !== null
 | 
			
		||||
          ? (
 | 
			
		||||
            <img
 | 
			
		||||
              draggable="false"
 | 
			
		||||
              src={imageSrc}
 | 
			
		||||
              onLoad={(e) => { e.target.style.backgroundColor = 'transparent'; }}
 | 
			
		||||
              onError={(e) => { e.target.src = ImageBrokenSVG; }}
 | 
			
		||||
              alt=""
 | 
			
		||||
            />
 | 
			
		||||
          )
 | 
			
		||||
          : (
 | 
			
		||||
            <span
 | 
			
		||||
              style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
 | 
			
		||||
              className={`avatar__border${iconSrc !== null ? '--active' : ''}`}
 | 
			
		||||
            >
 | 
			
		||||
              {
 | 
			
		||||
                iconSrc !== null
 | 
			
		||||
                  ? <RawIcon size={size} src={iconSrc} color={iconColor} />
 | 
			
		||||
                  : text !== null && (
 | 
			
		||||
                    <Text variant={textSize} primary>
 | 
			
		||||
                      {twemojify(avatarInitials(text))}
 | 
			
		||||
                    </Text>
 | 
			
		||||
                  )
 | 
			
		||||
              }
 | 
			
		||||
            </span>
 | 
			
		||||
          )
 | 
			
		||||
      }
 | 
			
		||||
      {imageSrc !== null ? (
 | 
			
		||||
        <img
 | 
			
		||||
          draggable="false"
 | 
			
		||||
          src={imageSrc}
 | 
			
		||||
          onLoad={(e) => {
 | 
			
		||||
            e.target.style.backgroundColor = 'transparent';
 | 
			
		||||
          }}
 | 
			
		||||
          onError={(e) => {
 | 
			
		||||
            e.target.src = ImageBrokenSVG;
 | 
			
		||||
          }}
 | 
			
		||||
          alt=""
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <span
 | 
			
		||||
          style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
 | 
			
		||||
          className={`avatar__border${iconSrc !== null ? '--active' : ''}`}
 | 
			
		||||
        >
 | 
			
		||||
          {iconSrc !== null ? (
 | 
			
		||||
            <RawIcon size={size} src={iconSrc} color={iconColor} />
 | 
			
		||||
          ) : (
 | 
			
		||||
            text !== null && (
 | 
			
		||||
              <Text variant={textSize} primary>
 | 
			
		||||
                {avatarInitials(text)}
 | 
			
		||||
              </Text>
 | 
			
		||||
            )
 | 
			
		||||
          )}
 | 
			
		||||
        </span>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,33 +0,0 @@
 | 
			
		|||
import React, { useEffect, useRef } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './Math.scss';
 | 
			
		||||
 | 
			
		||||
import katex from 'katex';
 | 
			
		||||
import 'katex/dist/katex.min.css';
 | 
			
		||||
 | 
			
		||||
import 'katex/dist/contrib/copy-tex';
 | 
			
		||||
 | 
			
		||||
const Math = React.memo(({
 | 
			
		||||
  content, throwOnError, errorColor, displayMode,
 | 
			
		||||
}) => {
 | 
			
		||||
  const ref = useRef(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    katex.render(content, ref.current, { throwOnError, errorColor, displayMode });
 | 
			
		||||
  }, [content, throwOnError, errorColor, displayMode]);
 | 
			
		||||
 | 
			
		||||
  return <span ref={ref} />;
 | 
			
		||||
});
 | 
			
		||||
Math.defaultProps = {
 | 
			
		||||
  throwOnError: null,
 | 
			
		||||
  errorColor: null,
 | 
			
		||||
  displayMode: null,
 | 
			
		||||
};
 | 
			
		||||
Math.propTypes = {
 | 
			
		||||
  content: PropTypes.string.isRequired,
 | 
			
		||||
  throwOnError: PropTypes.bool,
 | 
			
		||||
  errorColor: PropTypes.string,
 | 
			
		||||
  displayMode: PropTypes.bool,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Math;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
.katex-display {
 | 
			
		||||
  margin: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ import React, {
 | 
			
		|||
  useRef,
 | 
			
		||||
  useState,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import { useAtom } from 'jotai';
 | 
			
		||||
import { useAtom, useAtomValue } from 'jotai';
 | 
			
		||||
import { isKeyHotkey } from 'is-hotkey';
 | 
			
		||||
import { EventType, IContent, MsgType, Room } from 'matrix-js-sdk';
 | 
			
		||||
import { ReactEditor } from 'slate-react';
 | 
			
		||||
| 
						 | 
				
			
			@ -56,7 +56,6 @@ import {
 | 
			
		|||
} from '../../components/editor';
 | 
			
		||||
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
 | 
			
		||||
import { UseStateProvider } from '../../components/UseStateProvider';
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import { TUploadContent, encryptFile, getImageInfo, getMxIdLocalPart } from '../../utils/matrix';
 | 
			
		||||
import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater';
 | 
			
		||||
import { useFilePicker } from '../../hooks/useFilePicker';
 | 
			
		||||
| 
						 | 
				
			
			@ -95,6 +94,7 @@ import {
 | 
			
		|||
} from './msgContent';
 | 
			
		||||
import colorMXID from '../../../util/colorMXID';
 | 
			
		||||
import {
 | 
			
		||||
  getAllParents,
 | 
			
		||||
  getMemberDisplayName,
 | 
			
		||||
  parseReplyBody,
 | 
			
		||||
  parseReplyFormattedBody,
 | 
			
		||||
| 
						 | 
				
			
			@ -107,6 +107,7 @@ import { Command, SHRUG, useCommands } from '../../hooks/useCommands';
 | 
			
		|||
import { mobileOrTablet } from '../../utils/user-agent';
 | 
			
		||||
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
 | 
			
		||||
import { ReplyLayout } from '../../components/message';
 | 
			
		||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
 | 
			
		||||
 | 
			
		||||
interface RoomInputProps {
 | 
			
		||||
  editor: Editor;
 | 
			
		||||
| 
						 | 
				
			
			@ -121,6 +122,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
 | 
			
		|||
    const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
 | 
			
		||||
    const commands = useCommands(mx, room);
 | 
			
		||||
    const emojiBtnRef = useRef<HTMLButtonElement>(null);
 | 
			
		||||
    const roomToParents = useAtomValue(roomToParentsAtom);
 | 
			
		||||
 | 
			
		||||
    const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
 | 
			
		||||
    const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
 | 
			
		||||
| 
						 | 
				
			
			@ -133,13 +135,13 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
 | 
			
		|||
    const uploadBoardHandlers = useRef<UploadBoardImperativeHandlers>();
 | 
			
		||||
 | 
			
		||||
    const imagePackRooms: Room[] = useMemo(() => {
 | 
			
		||||
      const allParentSpaces = [roomId, ...(initMatrix.roomList?.getAllParentSpaces(roomId) ?? [])];
 | 
			
		||||
      const allParentSpaces = [roomId].concat(Array.from(getAllParents(roomToParents, roomId)));
 | 
			
		||||
      return allParentSpaces.reduce<Room[]>((list, rId) => {
 | 
			
		||||
        const r = mx.getRoom(rId);
 | 
			
		||||
        if (r) list.push(r);
 | 
			
		||||
        return list;
 | 
			
		||||
      }, []);
 | 
			
		||||
    }, [mx, roomId]);
 | 
			
		||||
    }, [mx, roomId, roomToParents]);
 | 
			
		||||
 | 
			
		||||
    const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar');
 | 
			
		||||
    const [autocompleteQuery, setAutocompleteQuery] =
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,7 +28,7 @@ import classNames from 'classnames';
 | 
			
		|||
import { ReactEditor } from 'slate-react';
 | 
			
		||||
import { Editor } from 'slate';
 | 
			
		||||
import to from 'await-to-js';
 | 
			
		||||
import { useSetAtom } from 'jotai';
 | 
			
		||||
import { useAtomValue, useSetAtom } from 'jotai';
 | 
			
		||||
import {
 | 
			
		||||
  Badge,
 | 
			
		||||
  Box,
 | 
			
		||||
| 
						 | 
				
			
			@ -74,6 +74,7 @@ import { getReactCustomHtmlParser } from '../../plugins/react-custom-html-parser
 | 
			
		|||
import {
 | 
			
		||||
  canEditEvent,
 | 
			
		||||
  decryptAllTimelineEvent,
 | 
			
		||||
  getAllParents,
 | 
			
		||||
  getEditedEvent,
 | 
			
		||||
  getEventReactions,
 | 
			
		||||
  getLatestEditableEvt,
 | 
			
		||||
| 
						 | 
				
			
			@ -103,14 +104,15 @@ import { createMentionElement, isEmptyEditor, moveCursor } from '../../component
 | 
			
		|||
import { roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts';
 | 
			
		||||
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
 | 
			
		||||
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import { useKeyDown } from '../../hooks/useKeyDown';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange';
 | 
			
		||||
import { RenderMessageContent } from '../../components/RenderMessageContent';
 | 
			
		||||
import { Image } from '../../components/media';
 | 
			
		||||
import { ImageViewer } from '../../components/image-viewer';
 | 
			
		||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
 | 
			
		||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
 | 
			
		||||
import { useRoomUnread } from '../../state/hooks/unread';
 | 
			
		||||
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
 | 
			
		||||
 | 
			
		||||
const TimelineFloat = as<'div', css.TimelineFloatVariants>(
 | 
			
		||||
  ({ position, className, ...props }, ref) => (
 | 
			
		||||
| 
						 | 
				
			
			@ -444,18 +446,19 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
 | 
			
		|||
  const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel);
 | 
			
		||||
  const [editId, setEditId] = useState<string>();
 | 
			
		||||
  const { navigateRoom, navigateSpace } = useRoomNavigate();
 | 
			
		||||
  const roomToParents = useAtomValue(roomToParentsAtom);
 | 
			
		||||
  const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
 | 
			
		||||
 | 
			
		||||
  const imagePackRooms: Room[] = useMemo(() => {
 | 
			
		||||
    const allParentSpaces = [
 | 
			
		||||
      room.roomId,
 | 
			
		||||
      ...(initMatrix.roomList?.getAllParentSpaces(room.roomId) ?? []),
 | 
			
		||||
    ];
 | 
			
		||||
    const allParentSpaces = [room.roomId].concat(
 | 
			
		||||
      Array.from(getAllParents(roomToParents, room.roomId))
 | 
			
		||||
    );
 | 
			
		||||
    return allParentSpaces.reduce<Room[]>((list, rId) => {
 | 
			
		||||
      const r = mx.getRoom(rId);
 | 
			
		||||
      if (r) list.push(r);
 | 
			
		||||
      return list;
 | 
			
		||||
    }, []);
 | 
			
		||||
  }, [mx, room]);
 | 
			
		||||
  }, [mx, room, roomToParents]);
 | 
			
		||||
 | 
			
		||||
  const [unreadInfo, setUnreadInfo] = useState(() => getRoomUnreadInfo(room, true));
 | 
			
		||||
  const readUptoEventIdRef = useRef<string>();
 | 
			
		||||
| 
						 | 
				
			
			@ -794,15 +797,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
 | 
			
		|||
 | 
			
		||||
  // Remove unreadInfo on mark as read
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleFullRead = (rId: string) => {
 | 
			
		||||
      if (rId !== room.roomId) return;
 | 
			
		||||
    if (!unread) {
 | 
			
		||||
      setUnreadInfo(undefined);
 | 
			
		||||
    };
 | 
			
		||||
    initMatrix.notifications?.on(cons.events.notifications.FULL_READ, handleFullRead);
 | 
			
		||||
    return () => {
 | 
			
		||||
      initMatrix.notifications?.removeListener(cons.events.notifications.FULL_READ, handleFullRead);
 | 
			
		||||
    };
 | 
			
		||||
  }, [room]);
 | 
			
		||||
    }
 | 
			
		||||
  }, [unread]);
 | 
			
		||||
 | 
			
		||||
  // scroll out of view msg editor in view.
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,25 +0,0 @@
 | 
			
		|||
/* eslint-disable import/prefer-default-export */
 | 
			
		||||
import { useState, useEffect } from 'react';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../client/initMatrix';
 | 
			
		||||
import cons from '../../client/state/cons';
 | 
			
		||||
 | 
			
		||||
export function useCategorizedSpaces() {
 | 
			
		||||
  const { accountData } = initMatrix;
 | 
			
		||||
  const [categorizedSpaces, setCategorizedSpaces] = useState([...accountData.categorizedSpaces]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleCategorizedSpaces = () => {
 | 
			
		||||
      setCategorizedSpaces([...accountData.categorizedSpaces]);
 | 
			
		||||
    };
 | 
			
		||||
    accountData.on(cons.events.accountData.CATEGORIZE_SPACE_UPDATED, handleCategorizedSpaces);
 | 
			
		||||
    return () => {
 | 
			
		||||
      accountData.removeListener(
 | 
			
		||||
        cons.events.accountData.CATEGORIZE_SPACE_UPDATED,
 | 
			
		||||
        handleCategorizedSpaces,
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return [categorizedSpaces];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -114,12 +114,12 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
 | 
			
		|||
        description: 'Leave current room.',
 | 
			
		||||
        exe: async (payload) => {
 | 
			
		||||
          if (payload.trim() === '') {
 | 
			
		||||
            roomActions.leave(room.roomId);
 | 
			
		||||
            mx.leave(room.roomId);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          const rawIds = payload.split(' ');
 | 
			
		||||
          const roomIds = rawIds.filter((id) => isRoomId(id));
 | 
			
		||||
          roomIds.map((id) => roomActions.leave(id));
 | 
			
		||||
          roomIds.map((id) => mx.leave(id));
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      [Command.Invite]: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										11
									
								
								src/app/hooks/usePreviousValue.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/app/hooks/usePreviousValue.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
import { useEffect, useRef } from 'react';
 | 
			
		||||
 | 
			
		||||
export const usePreviousValue = <T>(currentValue: T, initialValue: T) => {
 | 
			
		||||
  const valueRef = useRef(initialValue);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    valueRef.current = currentValue;
 | 
			
		||||
  }, [currentValue]);
 | 
			
		||||
 | 
			
		||||
  return valueRef.current;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +1,26 @@
 | 
			
		|||
import { useAtomValue } from 'jotai';
 | 
			
		||||
import { useMemo } from 'react';
 | 
			
		||||
import { roomIdToTypingMembersAtom, selectRoomTypingMembersAtom } from '../state/typingMembers';
 | 
			
		||||
import { selectAtom } from 'jotai/utils';
 | 
			
		||||
import { useCallback } from 'react';
 | 
			
		||||
import {
 | 
			
		||||
  IRoomIdToTypingMembers,
 | 
			
		||||
  TypingReceipt,
 | 
			
		||||
  roomIdToTypingMembersAtom,
 | 
			
		||||
} from '../state/typingMembers';
 | 
			
		||||
 | 
			
		||||
const typingReceiptEqual = (a: TypingReceipt, b: TypingReceipt): boolean =>
 | 
			
		||||
  a.userId === b.userId && a.ts === b.ts;
 | 
			
		||||
 | 
			
		||||
const equalTypingMembers = (x: TypingReceipt[], y: TypingReceipt[]): boolean => {
 | 
			
		||||
  if (x.length !== y.length) return false;
 | 
			
		||||
  return x.every((a, i) => typingReceiptEqual(a, y[i]));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useRoomTypingMember = (roomId: string) => {
 | 
			
		||||
  const typing = useAtomValue(
 | 
			
		||||
    useMemo(() => selectRoomTypingMembersAtom(roomId, roomIdToTypingMembersAtom), [roomId])
 | 
			
		||||
  const selector = useCallback(
 | 
			
		||||
    (roomToTyping: IRoomIdToTypingMembers) => roomToTyping.get(roomId) ?? [],
 | 
			
		||||
    [roomId]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const typing = useAtomValue(selectAtom(roomIdToTypingMembersAtom, selector, equalTypingMembers));
 | 
			
		||||
  return typing;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,21 +0,0 @@
 | 
			
		|||
/* eslint-disable import/prefer-default-export */
 | 
			
		||||
import { useState, useEffect } from 'react';
 | 
			
		||||
 | 
			
		||||
import cons from '../../client/state/cons';
 | 
			
		||||
import navigation from '../../client/state/navigation';
 | 
			
		||||
 | 
			
		||||
export function useSelectedSpace() {
 | 
			
		||||
  const [spaceId, setSpaceId] = useState(navigation.selectedSpaceId);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const onSpaceSelected = (roomId) => {
 | 
			
		||||
      setSpaceId(roomId);
 | 
			
		||||
    };
 | 
			
		||||
    navigation.on(cons.events.navigation.SPACE_SELECTED, onSpaceSelected);
 | 
			
		||||
    return () => {
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.SPACE_SELECTED, onSpaceSelected);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return [spaceId];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,21 +0,0 @@
 | 
			
		|||
/* eslint-disable import/prefer-default-export */
 | 
			
		||||
import { useState, useEffect } from 'react';
 | 
			
		||||
 | 
			
		||||
import cons from '../../client/state/cons';
 | 
			
		||||
import navigation from '../../client/state/navigation';
 | 
			
		||||
 | 
			
		||||
export function useSelectedTab() {
 | 
			
		||||
  const [selectedTab, setSelectedTab] = useState(navigation.selectedTab);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const onTabSelected = (tabId) => {
 | 
			
		||||
      setSelectedTab(tabId);
 | 
			
		||||
    };
 | 
			
		||||
    navigation.on(cons.events.navigation.TAB_SELECTED, onTabSelected);
 | 
			
		||||
    return () => {
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.TAB_SELECTED, onTabSelected);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return [selectedTab];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,25 +0,0 @@
 | 
			
		|||
/* eslint-disable import/prefer-default-export */
 | 
			
		||||
import { useState, useEffect } from 'react';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../client/initMatrix';
 | 
			
		||||
import cons from '../../client/state/cons';
 | 
			
		||||
 | 
			
		||||
export function useSpaceShortcut() {
 | 
			
		||||
  const { accountData } = initMatrix;
 | 
			
		||||
  const [spaceShortcut, setSpaceShortcut] = useState([...accountData.spaceShortcut]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const onSpaceShortcutUpdated = () => {
 | 
			
		||||
      setSpaceShortcut([...accountData.spaceShortcut]);
 | 
			
		||||
    };
 | 
			
		||||
    accountData.on(cons.events.accountData.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated);
 | 
			
		||||
    return () => {
 | 
			
		||||
      accountData.removeListener(
 | 
			
		||||
        cons.events.accountData.SPACE_SHORTCUT_UPDATED,
 | 
			
		||||
        onSpaceShortcutUpdated,
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return [spaceShortcut];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,16 +2,21 @@ import React from 'react';
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import './Dialog.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
 | 
			
		||||
import ScrollView from '../../atoms/scroll/ScrollView';
 | 
			
		||||
import RawModal from '../../atoms/modal/RawModal';
 | 
			
		||||
 | 
			
		||||
function Dialog({
 | 
			
		||||
  className, isOpen, title, onAfterOpen, onAfterClose,
 | 
			
		||||
  contentOptions, onRequestClose, closeFromOutside, children,
 | 
			
		||||
  className,
 | 
			
		||||
  isOpen,
 | 
			
		||||
  title,
 | 
			
		||||
  onAfterOpen,
 | 
			
		||||
  onAfterClose,
 | 
			
		||||
  contentOptions,
 | 
			
		||||
  onRequestClose,
 | 
			
		||||
  closeFromOutside,
 | 
			
		||||
  children,
 | 
			
		||||
  invisibleScroll,
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
| 
						 | 
				
			
			@ -28,19 +33,19 @@ function Dialog({
 | 
			
		|||
        <div className="dialog__content">
 | 
			
		||||
          <Header>
 | 
			
		||||
            <TitleWrapper>
 | 
			
		||||
              {
 | 
			
		||||
                typeof title === 'string'
 | 
			
		||||
                  ? <Text variant="h2" weight="medium" primary>{twemojify(title)}</Text>
 | 
			
		||||
                  : title
 | 
			
		||||
              }
 | 
			
		||||
              {typeof title === 'string' ? (
 | 
			
		||||
                <Text variant="h2" weight="medium" primary>
 | 
			
		||||
                  {title}
 | 
			
		||||
                </Text>
 | 
			
		||||
              ) : (
 | 
			
		||||
                title
 | 
			
		||||
              )}
 | 
			
		||||
            </TitleWrapper>
 | 
			
		||||
            {contentOptions}
 | 
			
		||||
          </Header>
 | 
			
		||||
          <div className="dialog__content__wrapper">
 | 
			
		||||
            <ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
 | 
			
		||||
              <div className="dialog__content-container">
 | 
			
		||||
                {children}
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="dialog__content-container">{children}</div>
 | 
			
		||||
            </ScrollView>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,61 +0,0 @@
 | 
			
		|||
/* eslint-disable react/prop-types */
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './FollowingMembers.scss';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import { openReadReceipts } from '../../../client/action/navigation';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import RawIcon from '../../atoms/system-icons/RawIcon';
 | 
			
		||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
 | 
			
		||||
 | 
			
		||||
import { getUsersActionJsx } from '../../organisms/room/common';
 | 
			
		||||
 | 
			
		||||
function FollowingMembers({ roomTimeline }) {
 | 
			
		||||
  const [followingMembers, setFollowingMembers] = useState([]);
 | 
			
		||||
  const { roomId } = roomTimeline;
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const myUserId = mx.getUserId();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const updateFollowingMembers = () => {
 | 
			
		||||
      setFollowingMembers(roomTimeline.getLiveReaders());
 | 
			
		||||
    };
 | 
			
		||||
    const updateOnEvent = (event, room) => {
 | 
			
		||||
      if (room.roomId !== roomId) return;
 | 
			
		||||
      setFollowingMembers(roomTimeline.getLiveReaders());
 | 
			
		||||
    };
 | 
			
		||||
    updateFollowingMembers();
 | 
			
		||||
    roomTimeline.on(cons.events.roomTimeline.LIVE_RECEIPT, updateFollowingMembers);
 | 
			
		||||
    mx.on('Room.timeline', updateOnEvent);
 | 
			
		||||
    return () => {
 | 
			
		||||
      roomTimeline.removeListener(cons.events.roomTimeline.LIVE_RECEIPT, updateFollowingMembers);
 | 
			
		||||
      mx.removeListener('Room.timeline', updateOnEvent);
 | 
			
		||||
    };
 | 
			
		||||
  }, [roomTimeline, roomId]);
 | 
			
		||||
 | 
			
		||||
  const filteredM = followingMembers.filter((userId) => userId !== myUserId);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    filteredM.length !== 0 && (
 | 
			
		||||
      <button
 | 
			
		||||
        className="following-members"
 | 
			
		||||
        onClick={() => openReadReceipts(roomId, followingMembers)}
 | 
			
		||||
        type="button"
 | 
			
		||||
      >
 | 
			
		||||
        <RawIcon size="extra-small" src={TickMarkIC} />
 | 
			
		||||
        <Text variant="b2">
 | 
			
		||||
          {getUsersActionJsx(roomId, filteredM, 'following the conversation.')}
 | 
			
		||||
        </Text>
 | 
			
		||||
      </button>
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FollowingMembers.propTypes = {
 | 
			
		||||
  roomTimeline: PropTypes.shape({}).isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default FollowingMembers;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,31 +0,0 @@
 | 
			
		|||
@use '../../partials/text';
 | 
			
		||||
 | 
			
		||||
.following-members {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  padding: 0 var(--sp-normal);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: flex-end;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  
 | 
			
		||||
  & .ic-raw {
 | 
			
		||||
    min-width: var(--ic-extra-small);
 | 
			
		||||
    opacity: 0.4;
 | 
			
		||||
    margin: 0 var(--sp-extra-tight);
 | 
			
		||||
  }
 | 
			
		||||
  & .text {
 | 
			
		||||
    @extend .cp-txt__ellipsis;
 | 
			
		||||
    color: var(--tc-surface-low);
 | 
			
		||||
    b {
 | 
			
		||||
      color: var(--tc-surface-normal);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:hover,
 | 
			
		||||
  &:focus {
 | 
			
		||||
    background-color: var(--bg-surface-hover);
 | 
			
		||||
  }
 | 
			
		||||
  &:active {
 | 
			
		||||
    background-color: var(--bg-surface-active);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,47 +0,0 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './ImageLightbox.scss';
 | 
			
		||||
import FileSaver from 'file-saver';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import RawModal from '../../atoms/modal/RawModal';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
 | 
			
		||||
import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
 | 
			
		||||
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
 | 
			
		||||
 | 
			
		||||
function ImageLightbox({
 | 
			
		||||
  url, alt, isOpen, onRequestClose,
 | 
			
		||||
}) {
 | 
			
		||||
  const handleDownload = () => {
 | 
			
		||||
    FileSaver.saveAs(url, alt);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <RawModal
 | 
			
		||||
      className="image-lightbox__modal"
 | 
			
		||||
      overlayClassName="image-lightbox__overlay"
 | 
			
		||||
      isOpen={isOpen}
 | 
			
		||||
      onRequestClose={onRequestClose}
 | 
			
		||||
      size="large"
 | 
			
		||||
    >
 | 
			
		||||
      <div className="image-lightbox__header">
 | 
			
		||||
        <Text variant="b2" weight="medium">{alt}</Text>
 | 
			
		||||
        <IconButton onClick={() => window.open(url)} size="small" src={ExternalSVG} />
 | 
			
		||||
        <IconButton onClick={handleDownload} size="small" src={DownloadSVG} />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="image-lightbox__content">
 | 
			
		||||
        <img src={url} alt={alt} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </RawModal>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ImageLightbox.propTypes = {
 | 
			
		||||
  url: PropTypes.string.isRequired,
 | 
			
		||||
  alt: PropTypes.string.isRequired,
 | 
			
		||||
  isOpen: PropTypes.bool.isRequired,
 | 
			
		||||
  onRequestClose: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ImageLightbox;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,50 +0,0 @@
 | 
			
		|||
@use '../../partials/flex';
 | 
			
		||||
@use '../../partials/text';
 | 
			
		||||
 | 
			
		||||
.image-lightbox__modal {
 | 
			
		||||
  box-shadow: none;
 | 
			
		||||
  width: unset;
 | 
			
		||||
  gap: var(--sp-normal);
 | 
			
		||||
 | 
			
		||||
  border-radius: 0;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
 | 
			
		||||
  & .text {
 | 
			
		||||
    color: white;
 | 
			
		||||
  }
 | 
			
		||||
  & .ic-raw {
 | 
			
		||||
    background-color: white;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.image-lightbox__overlay {
 | 
			
		||||
  background-color: var(--bg-overlay-low);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.image-lightbox__header > *,
 | 
			
		||||
.image-lightbox__content > * {
 | 
			
		||||
  pointer-events: all;
 | 
			
		||||
}
 | 
			
		||||
.image-lightbox__header {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
 | 
			
		||||
  & > .text {
 | 
			
		||||
    @extend .cp-fx__item-one;
 | 
			
		||||
    @extend .cp-txt__ellipsis;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.image-lightbox__content {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  max-height: 80vh;
 | 
			
		||||
 | 
			
		||||
  & img {
 | 
			
		||||
    background-color: var(--bg-surface-low);
 | 
			
		||||
    object-fit: contain;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
    max-height: 100%;
 | 
			
		||||
    border-radius: var(--bo-radius);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,366 +0,0 @@
 | 
			
		|||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './Media.scss';
 | 
			
		||||
 | 
			
		||||
import encrypt from 'browser-encrypt-attachment';
 | 
			
		||||
 | 
			
		||||
import { BlurhashCanvas } from 'react-blurhash';
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import Spinner from '../../atoms/spinner/Spinner';
 | 
			
		||||
import ImageLightbox from '../image-lightbox/ImageLightbox';
 | 
			
		||||
 | 
			
		||||
import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
 | 
			
		||||
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
 | 
			
		||||
import PlaySVG from '../../../../public/res/ic/outlined/play.svg';
 | 
			
		||||
 | 
			
		||||
import { getBlobSafeMimeType } from '../../../util/mimetypes';
 | 
			
		||||
 | 
			
		||||
async function getDecryptedBlob(response, type, decryptData) {
 | 
			
		||||
  const arrayBuffer = await response.arrayBuffer();
 | 
			
		||||
  const dataArray = await encrypt.decryptAttachment(arrayBuffer, decryptData);
 | 
			
		||||
  const blob = new Blob([dataArray], { type: getBlobSafeMimeType(type) });
 | 
			
		||||
  return blob;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getUrl(link, type, decryptData) {
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await fetch(link, { method: 'GET' });
 | 
			
		||||
    if (decryptData !== null) {
 | 
			
		||||
      return URL.createObjectURL(await getDecryptedBlob(response, type, decryptData));
 | 
			
		||||
    }
 | 
			
		||||
    const blob = await response.blob();
 | 
			
		||||
    return URL.createObjectURL(blob);
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    return link;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getNativeHeight(width, height, maxWidth = 296) {
 | 
			
		||||
  const scale = maxWidth / width;
 | 
			
		||||
  return scale * height;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function FileHeader({
 | 
			
		||||
  name, link, external,
 | 
			
		||||
  file, type,
 | 
			
		||||
}) {
 | 
			
		||||
  const [url, setUrl] = useState(null);
 | 
			
		||||
 | 
			
		||||
  async function getFile() {
 | 
			
		||||
    const myUrl = await getUrl(link, type, file);
 | 
			
		||||
    setUrl(myUrl);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function handleDownload(e) {
 | 
			
		||||
    if (file !== null && url === null) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      await getFile();
 | 
			
		||||
      e.target.click();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="file-header">
 | 
			
		||||
      <Text className="file-name" variant="b3">{name}</Text>
 | 
			
		||||
      { link !== null && (
 | 
			
		||||
        <>
 | 
			
		||||
          {
 | 
			
		||||
            external && (
 | 
			
		||||
              <IconButton
 | 
			
		||||
                size="extra-small"
 | 
			
		||||
                tooltip="Open in new tab"
 | 
			
		||||
                src={ExternalSVG}
 | 
			
		||||
                onClick={() => window.open(url || link)}
 | 
			
		||||
              />
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          <a href={url || link} download={name} target="_blank" rel="noreferrer">
 | 
			
		||||
            <IconButton
 | 
			
		||||
              size="extra-small"
 | 
			
		||||
              tooltip="Download"
 | 
			
		||||
              src={DownloadSVG}
 | 
			
		||||
              onClick={handleDownload}
 | 
			
		||||
            />
 | 
			
		||||
          </a>
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
FileHeader.defaultProps = {
 | 
			
		||||
  external: false,
 | 
			
		||||
  file: null,
 | 
			
		||||
  link: null,
 | 
			
		||||
};
 | 
			
		||||
FileHeader.propTypes = {
 | 
			
		||||
  name: PropTypes.string.isRequired,
 | 
			
		||||
  link: PropTypes.string,
 | 
			
		||||
  external: PropTypes.bool,
 | 
			
		||||
  file: PropTypes.shape({}),
 | 
			
		||||
  type: PropTypes.string.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function File({
 | 
			
		||||
  name, link, file, type,
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="file-container">
 | 
			
		||||
      <FileHeader name={name} link={link} file={file} type={type} />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
File.defaultProps = {
 | 
			
		||||
  file: null,
 | 
			
		||||
  type: '',
 | 
			
		||||
};
 | 
			
		||||
File.propTypes = {
 | 
			
		||||
  name: PropTypes.string.isRequired,
 | 
			
		||||
  link: PropTypes.string.isRequired,
 | 
			
		||||
  type: PropTypes.string,
 | 
			
		||||
  file: PropTypes.shape({}),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function Image({
 | 
			
		||||
  name, width, height, link, file, type, blurhash,
 | 
			
		||||
}) {
 | 
			
		||||
  const [url, setUrl] = useState(null);
 | 
			
		||||
  const [blur, setBlur] = useState(true);
 | 
			
		||||
  const [lightbox, setLightbox] = useState(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let unmounted = false;
 | 
			
		||||
    async function fetchUrl() {
 | 
			
		||||
      const myUrl = await getUrl(link, type, file);
 | 
			
		||||
      if (unmounted) return;
 | 
			
		||||
      setUrl(myUrl);
 | 
			
		||||
    }
 | 
			
		||||
    fetchUrl();
 | 
			
		||||
    return () => {
 | 
			
		||||
      unmounted = true;
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const toggleLightbox = () => {
 | 
			
		||||
    if (!url) return;
 | 
			
		||||
    setLightbox(!lightbox);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className="file-container">
 | 
			
		||||
        <div
 | 
			
		||||
          style={{ height: width !== null ? getNativeHeight(width, height) : 'unset' }}
 | 
			
		||||
          className="image-container"
 | 
			
		||||
          role="button"
 | 
			
		||||
          tabIndex="0"
 | 
			
		||||
          onClick={toggleLightbox}
 | 
			
		||||
          onKeyDown={toggleLightbox}
 | 
			
		||||
        >
 | 
			
		||||
          { blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
 | 
			
		||||
          { url !== null && (
 | 
			
		||||
            <img
 | 
			
		||||
              style={{ display: blur ? 'none' : 'unset' }}
 | 
			
		||||
              onLoad={() => setBlur(false)}
 | 
			
		||||
              src={url || link}
 | 
			
		||||
              alt={name}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      {url && (
 | 
			
		||||
        <ImageLightbox
 | 
			
		||||
          url={url}
 | 
			
		||||
          alt={name}
 | 
			
		||||
          isOpen={lightbox}
 | 
			
		||||
          onRequestClose={toggleLightbox}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
Image.defaultProps = {
 | 
			
		||||
  file: null,
 | 
			
		||||
  width: null,
 | 
			
		||||
  height: null,
 | 
			
		||||
  type: '',
 | 
			
		||||
  blurhash: '',
 | 
			
		||||
};
 | 
			
		||||
Image.propTypes = {
 | 
			
		||||
  name: PropTypes.string.isRequired,
 | 
			
		||||
  width: PropTypes.number,
 | 
			
		||||
  height: PropTypes.number,
 | 
			
		||||
  link: PropTypes.string.isRequired,
 | 
			
		||||
  file: PropTypes.shape({}),
 | 
			
		||||
  type: PropTypes.string,
 | 
			
		||||
  blurhash: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function Sticker({
 | 
			
		||||
  name, height, width, link, file, type,
 | 
			
		||||
}) {
 | 
			
		||||
  const [url, setUrl] = useState(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let unmounted = false;
 | 
			
		||||
    async function fetchUrl() {
 | 
			
		||||
      const myUrl = await getUrl(link, type, file);
 | 
			
		||||
      if (unmounted) return;
 | 
			
		||||
      setUrl(myUrl);
 | 
			
		||||
    }
 | 
			
		||||
    fetchUrl();
 | 
			
		||||
    return () => {
 | 
			
		||||
      unmounted = true;
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="sticker-container" style={{ height: width !== null ? getNativeHeight(width, height, 128) : 'unset' }}>
 | 
			
		||||
      { url !== null && <img src={url || link} title={name} alt={name} />}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
Sticker.defaultProps = {
 | 
			
		||||
  file: null,
 | 
			
		||||
  type: '',
 | 
			
		||||
  width: null,
 | 
			
		||||
  height: null,
 | 
			
		||||
};
 | 
			
		||||
Sticker.propTypes = {
 | 
			
		||||
  name: PropTypes.string.isRequired,
 | 
			
		||||
  width: PropTypes.number,
 | 
			
		||||
  height: PropTypes.number,
 | 
			
		||||
  link: PropTypes.string.isRequired,
 | 
			
		||||
  file: PropTypes.shape({}),
 | 
			
		||||
  type: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function Audio({
 | 
			
		||||
  name, link, type, file,
 | 
			
		||||
}) {
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
  const [url, setUrl] = useState(null);
 | 
			
		||||
 | 
			
		||||
  async function loadAudio() {
 | 
			
		||||
    const myUrl = await getUrl(link, type, file);
 | 
			
		||||
    setUrl(myUrl);
 | 
			
		||||
    setIsLoading(false);
 | 
			
		||||
  }
 | 
			
		||||
  function handlePlayAudio() {
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
    loadAudio();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="file-container">
 | 
			
		||||
      <FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
 | 
			
		||||
      <div className="audio-container">
 | 
			
		||||
        { url === null && isLoading && <Spinner size="small" /> }
 | 
			
		||||
        { url === null && !isLoading && <IconButton onClick={handlePlayAudio} tooltip="Play audio" src={PlaySVG} />}
 | 
			
		||||
        { url !== null && (
 | 
			
		||||
          /* eslint-disable-next-line jsx-a11y/media-has-caption */
 | 
			
		||||
          <audio autoPlay controls>
 | 
			
		||||
            <source src={url} type={getBlobSafeMimeType(type)} />
 | 
			
		||||
          </audio>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
Audio.defaultProps = {
 | 
			
		||||
  file: null,
 | 
			
		||||
  type: '',
 | 
			
		||||
};
 | 
			
		||||
Audio.propTypes = {
 | 
			
		||||
  name: PropTypes.string.isRequired,
 | 
			
		||||
  link: PropTypes.string.isRequired,
 | 
			
		||||
  type: PropTypes.string,
 | 
			
		||||
  file: PropTypes.shape({}),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function Video({
 | 
			
		||||
  name, link, thumbnail, thumbnailFile, thumbnailType,
 | 
			
		||||
  width, height, file, type, blurhash,
 | 
			
		||||
}) {
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
  const [url, setUrl] = useState(null);
 | 
			
		||||
  const [thumbUrl, setThumbUrl] = useState(null);
 | 
			
		||||
  const [blur, setBlur] = useState(true);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let unmounted = false;
 | 
			
		||||
    async function fetchUrl() {
 | 
			
		||||
      const myThumbUrl = await getUrl(thumbnail, thumbnailType, thumbnailFile);
 | 
			
		||||
      if (unmounted) return;
 | 
			
		||||
      setThumbUrl(myThumbUrl);
 | 
			
		||||
    }
 | 
			
		||||
    if (thumbnail !== null) fetchUrl();
 | 
			
		||||
    return () => {
 | 
			
		||||
      unmounted = true;
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const loadVideo = async () => {
 | 
			
		||||
    const myUrl = await getUrl(link, type, file);
 | 
			
		||||
    setUrl(myUrl);
 | 
			
		||||
    setIsLoading(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handlePlayVideo = () => {
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
    loadVideo();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="file-container">
 | 
			
		||||
      <FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          height: width !== null ? getNativeHeight(width, height) : 'unset',
 | 
			
		||||
        }}
 | 
			
		||||
        className="video-container"
 | 
			
		||||
      >
 | 
			
		||||
        { url === null ? (
 | 
			
		||||
          <>
 | 
			
		||||
            { blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
 | 
			
		||||
            { thumbUrl !== null && (
 | 
			
		||||
              <img style={{ display: blur ? 'none' : 'unset' }} src={thumbUrl} onLoad={() => setBlur(false)} alt={name} />
 | 
			
		||||
            )}
 | 
			
		||||
            {isLoading && <Spinner size="small" />}
 | 
			
		||||
            {!isLoading && <IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />}
 | 
			
		||||
          </>
 | 
			
		||||
        ) : (
 | 
			
		||||
          /* eslint-disable-next-line jsx-a11y/media-has-caption */
 | 
			
		||||
          <video autoPlay controls poster={thumbUrl}>
 | 
			
		||||
            <source src={url} type={getBlobSafeMimeType(type)} />
 | 
			
		||||
          </video>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
Video.defaultProps = {
 | 
			
		||||
  width: null,
 | 
			
		||||
  height: null,
 | 
			
		||||
  file: null,
 | 
			
		||||
  thumbnail: null,
 | 
			
		||||
  thumbnailType: null,
 | 
			
		||||
  thumbnailFile: null,
 | 
			
		||||
  type: '',
 | 
			
		||||
  blurhash: null,
 | 
			
		||||
};
 | 
			
		||||
Video.propTypes = {
 | 
			
		||||
  name: PropTypes.string.isRequired,
 | 
			
		||||
  link: PropTypes.string.isRequired,
 | 
			
		||||
  thumbnail: PropTypes.string,
 | 
			
		||||
  thumbnailFile: PropTypes.shape({}),
 | 
			
		||||
  thumbnailType: PropTypes.string,
 | 
			
		||||
  width: PropTypes.number,
 | 
			
		||||
  height: PropTypes.number,
 | 
			
		||||
  file: PropTypes.shape({}),
 | 
			
		||||
  type: PropTypes.string,
 | 
			
		||||
  blurhash: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  File, Image, Sticker, Audio, Video,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,90 +0,0 @@
 | 
			
		|||
@use '../../partials/text';
 | 
			
		||||
 | 
			
		||||
.file-header {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  padding: var(--sp-ultra-tight) var(--sp-tight);
 | 
			
		||||
  min-height: 42px;
 | 
			
		||||
 | 
			
		||||
  & .file-name {
 | 
			
		||||
    @extend .cp-txt__ellipsis;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    color: var(--tc-surface-low);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & a {
 | 
			
		||||
    line-height: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.file-container {
 | 
			
		||||
  --media-max-width: 296px;
 | 
			
		||||
 | 
			
		||||
  background-color: var(--bg-surface-hover);
 | 
			
		||||
  border-radius: calc(var(--bo-radius) / 2);
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  max-width: var(--media-max-width);
 | 
			
		||||
  white-space: initial;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sticker-container {
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  max-width: 128px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  & img {
 | 
			
		||||
    width: 100% !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.image-container,
 | 
			
		||||
.video-container,
 | 
			
		||||
.audio-container {
 | 
			
		||||
  font-size: 0;
 | 
			
		||||
  line-height: 0;
 | 
			
		||||
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
 | 
			
		||||
  background-position: center;
 | 
			
		||||
  background-repeat: no-repeat;
 | 
			
		||||
  background-size: cover;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.image-container,
 | 
			
		||||
.video-container {
 | 
			
		||||
  & img,
 | 
			
		||||
  & canvas {
 | 
			
		||||
    max-width: unset !important;
 | 
			
		||||
    width: 100% !important;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    border-radius: 0 !important;
 | 
			
		||||
    margin: 0 !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.image-container {
 | 
			
		||||
  max-height: 460px;
 | 
			
		||||
  img {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    object-fit: cover;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.video-container {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  & .ic-btn-surface {
 | 
			
		||||
    background-color: var(--bg-surface-low);
 | 
			
		||||
  }
 | 
			
		||||
  & .ic-btn-surface,
 | 
			
		||||
  & .donut-spinner {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
  }
 | 
			
		||||
  video {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.audio-container {
 | 
			
		||||
  audio {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,853 +0,0 @@
 | 
			
		|||
/* eslint-disable react/prop-types */
 | 
			
		||||
import React, {
 | 
			
		||||
  useState, useEffect, useCallback, useRef,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './Message.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import {
 | 
			
		||||
  getUsername, getUsernameOfRoomMember, parseReply, trimHTMLReply,
 | 
			
		||||
} from '../../../util/matrixUtil';
 | 
			
		||||
import colorMXID from '../../../util/colorMXID';
 | 
			
		||||
import { getEventCords } from '../../../util/common';
 | 
			
		||||
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
 | 
			
		||||
import {
 | 
			
		||||
  openEmojiBoard, openProfileViewer, openReadReceipts, openViewSource, replyTo,
 | 
			
		||||
} from '../../../client/action/navigation';
 | 
			
		||||
import { sanitizeCustomHtml } from '../../../util/sanitize';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import RawIcon from '../../atoms/system-icons/RawIcon';
 | 
			
		||||
import Button from '../../atoms/button/Button';
 | 
			
		||||
import Tooltip from '../../atoms/tooltip/Tooltip';
 | 
			
		||||
import Input from '../../atoms/input/Input';
 | 
			
		||||
import Avatar from '../../atoms/avatar/Avatar';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import Time from '../../atoms/time/Time';
 | 
			
		||||
import ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu';
 | 
			
		||||
import * as Media from '../media/Media';
 | 
			
		||||
 | 
			
		||||
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
 | 
			
		||||
import EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg';
 | 
			
		||||
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
 | 
			
		||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
 | 
			
		||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
 | 
			
		||||
import CmdIC from '../../../../public/res/ic/outlined/cmd.svg';
 | 
			
		||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
 | 
			
		||||
 | 
			
		||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
 | 
			
		||||
import { getBlobSafeMimeType } from '../../../util/mimetypes';
 | 
			
		||||
import { html, plain } from '../../../util/markdown';
 | 
			
		||||
 | 
			
		||||
function PlaceholderMessage() {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="ph-msg">
 | 
			
		||||
      <div className="ph-msg__avatar-container">
 | 
			
		||||
        <div className="ph-msg__avatar" />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="ph-msg__main-container">
 | 
			
		||||
        <div className="ph-msg__header" />
 | 
			
		||||
        <div className="ph-msg__body">
 | 
			
		||||
          <div />
 | 
			
		||||
          <div />
 | 
			
		||||
          <div />
 | 
			
		||||
          <div />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const MessageAvatar = React.memo(({
 | 
			
		||||
  roomId, avatarSrc, userId, username,
 | 
			
		||||
}) => (
 | 
			
		||||
  <div className="message__avatar-container">
 | 
			
		||||
    <button type="button" onClick={() => openProfileViewer(userId, roomId)}>
 | 
			
		||||
      <Avatar imageSrc={avatarSrc} text={username} bgColor={colorMXID(userId)} size="small" />
 | 
			
		||||
    </button>
 | 
			
		||||
  </div>
 | 
			
		||||
));
 | 
			
		||||
 | 
			
		||||
const MessageHeader = React.memo(({
 | 
			
		||||
  userId, username, timestamp, fullTime,
 | 
			
		||||
}) => (
 | 
			
		||||
  <div className="message__header">
 | 
			
		||||
    <Text
 | 
			
		||||
      style={{ color: colorMXID(userId) }}
 | 
			
		||||
      className="message__profile"
 | 
			
		||||
      variant="b1"
 | 
			
		||||
      weight="medium"
 | 
			
		||||
      span
 | 
			
		||||
    >
 | 
			
		||||
      <span>{twemojify(username)}</span>
 | 
			
		||||
      <span>{twemojify(userId)}</span>
 | 
			
		||||
    </Text>
 | 
			
		||||
    <div className="message__time">
 | 
			
		||||
      <Text variant="b3">
 | 
			
		||||
        <Time timestamp={timestamp} fullTime={fullTime} />
 | 
			
		||||
      </Text>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
));
 | 
			
		||||
MessageHeader.defaultProps = {
 | 
			
		||||
  fullTime: false,
 | 
			
		||||
};
 | 
			
		||||
MessageHeader.propTypes = {
 | 
			
		||||
  userId: PropTypes.string.isRequired,
 | 
			
		||||
  username: PropTypes.string.isRequired,
 | 
			
		||||
  timestamp: PropTypes.number.isRequired,
 | 
			
		||||
  fullTime: PropTypes.bool,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function MessageReply({ name, color, body }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="message__reply">
 | 
			
		||||
      <Text variant="b2">
 | 
			
		||||
        <RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
 | 
			
		||||
        <span style={{ color }}>{twemojify(name)}</span>
 | 
			
		||||
        {' '}
 | 
			
		||||
        {twemojify(body)}
 | 
			
		||||
      </Text>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MessageReply.propTypes = {
 | 
			
		||||
  name: PropTypes.string.isRequired,
 | 
			
		||||
  color: PropTypes.string.isRequired,
 | 
			
		||||
  body: PropTypes.string.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
 | 
			
		||||
  const [reply, setReply] = useState(null);
 | 
			
		||||
  const isMountedRef = useRef(true);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const mx = initMatrix.matrixClient;
 | 
			
		||||
    const timelineSet = roomTimeline.getUnfilteredTimelineSet();
 | 
			
		||||
    const loadReply = async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        const eTimeline = await mx.getEventTimeline(timelineSet, eventId);
 | 
			
		||||
        await roomTimeline.decryptAllEventsOfTimeline(eTimeline);
 | 
			
		||||
 | 
			
		||||
        let mEvent = eTimeline.getTimelineSet().findEventById(eventId);
 | 
			
		||||
        const editedList = roomTimeline.editedTimeline.get(mEvent.getId());
 | 
			
		||||
        if (editedList) {
 | 
			
		||||
          mEvent = editedList[editedList.length - 1];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const rawBody = mEvent.getContent().body;
 | 
			
		||||
        const username = getUsernameOfRoomMember(mEvent.sender);
 | 
			
		||||
 | 
			
		||||
        if (isMountedRef.current === false) return;
 | 
			
		||||
        const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***';
 | 
			
		||||
        let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody;
 | 
			
		||||
        if (editedList && parsedBody.startsWith(' * ')) {
 | 
			
		||||
          parsedBody = parsedBody.slice(3);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setReply({
 | 
			
		||||
          to: username,
 | 
			
		||||
          color: colorMXID(mEvent.getSender()),
 | 
			
		||||
          body: parsedBody,
 | 
			
		||||
          event: mEvent,
 | 
			
		||||
        });
 | 
			
		||||
      } catch {
 | 
			
		||||
        setReply({
 | 
			
		||||
          to: '** Unknown user **',
 | 
			
		||||
          color: 'var(--tc-danger-normal)',
 | 
			
		||||
          body: '*** Unable to load reply ***',
 | 
			
		||||
          event: null,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    loadReply();
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      isMountedRef.current = false;
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const focusReply = (ev) => {
 | 
			
		||||
    if (!ev.key || ev.key === ' ' || ev.key === 'Enter') {
 | 
			
		||||
      if (ev.key) ev.preventDefault();
 | 
			
		||||
      if (reply?.event === null) return;
 | 
			
		||||
      if (reply?.event.isRedacted()) return;
 | 
			
		||||
      roomTimeline.loadEventTimeline(eventId);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className="message__reply-wrapper"
 | 
			
		||||
      onClick={focusReply}
 | 
			
		||||
      onKeyDown={focusReply}
 | 
			
		||||
      role="button"
 | 
			
		||||
      tabIndex="0"
 | 
			
		||||
    >
 | 
			
		||||
      {reply !== null && <MessageReply name={reply.to} color={reply.color} body={reply.body} />}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
MessageReplyWrapper.propTypes = {
 | 
			
		||||
  roomTimeline: PropTypes.shape({}).isRequired,
 | 
			
		||||
  eventId: PropTypes.string.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const MessageBody = React.memo(({
 | 
			
		||||
  senderName,
 | 
			
		||||
  body,
 | 
			
		||||
  isCustomHTML,
 | 
			
		||||
  isEdited,
 | 
			
		||||
  msgType,
 | 
			
		||||
}) => {
 | 
			
		||||
  // if body is not string it is a React element.
 | 
			
		||||
  if (typeof body !== 'string') return <div className="message__body">{body}</div>;
 | 
			
		||||
 | 
			
		||||
  let content = null;
 | 
			
		||||
  if (isCustomHTML) {
 | 
			
		||||
    try {
 | 
			
		||||
      content = twemojify(
 | 
			
		||||
        sanitizeCustomHtml(initMatrix.matrixClient, body),
 | 
			
		||||
        undefined,
 | 
			
		||||
        true,
 | 
			
		||||
        false,
 | 
			
		||||
        true,
 | 
			
		||||
      );
 | 
			
		||||
    } catch {
 | 
			
		||||
      console.error('Malformed custom html: ', body);
 | 
			
		||||
      content = twemojify(body, undefined);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    content = twemojify(body, undefined, true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Determine if this message should render with large emojis
 | 
			
		||||
  // Criteria:
 | 
			
		||||
  // - Contains only emoji
 | 
			
		||||
  // - Contains no more than 10 emoji
 | 
			
		||||
  let emojiOnly = false;
 | 
			
		||||
  if (content.type === 'img') {
 | 
			
		||||
    // If this messages contains only a single (inline) image
 | 
			
		||||
    emojiOnly = true;
 | 
			
		||||
  } else if (content.constructor.name === 'Array') {
 | 
			
		||||
    // Otherwise, it might be an array of images / texb
 | 
			
		||||
 | 
			
		||||
    // Count the number of emojis
 | 
			
		||||
    const nEmojis = content.filter((e) => e.type === 'img').length;
 | 
			
		||||
 | 
			
		||||
    // Make sure there's no text besides whitespace and variation selector U+FE0F
 | 
			
		||||
    if (nEmojis <= 10 && content.every((element) => (
 | 
			
		||||
      (typeof element === 'object' && element.type === 'img')
 | 
			
		||||
      || (typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element))
 | 
			
		||||
    ))) {
 | 
			
		||||
      emojiOnly = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!isCustomHTML) {
 | 
			
		||||
    // If this is a plaintext message, wrap it in a <p> element (automatically applying
 | 
			
		||||
    // white-space: pre-wrap) in order to preserve newlines
 | 
			
		||||
    content = (<p className="message__body-plain">{content}</p>);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="message__body">
 | 
			
		||||
      <div dir="auto" className={`text ${emojiOnly ? 'text-h1' : 'text-b1'}`}>
 | 
			
		||||
        { msgType === 'm.emote' && (
 | 
			
		||||
          <>
 | 
			
		||||
            {'* '}
 | 
			
		||||
            {twemojify(senderName)}
 | 
			
		||||
            {' '}
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
        { content }
 | 
			
		||||
      </div>
 | 
			
		||||
      { isEdited && <Text className="message__body-edited" variant="b3">(edited)</Text>}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
MessageBody.defaultProps = {
 | 
			
		||||
  isCustomHTML: false,
 | 
			
		||||
  isEdited: false,
 | 
			
		||||
  msgType: null,
 | 
			
		||||
};
 | 
			
		||||
MessageBody.propTypes = {
 | 
			
		||||
  senderName: PropTypes.string.isRequired,
 | 
			
		||||
  body: PropTypes.node.isRequired,
 | 
			
		||||
  isCustomHTML: PropTypes.bool,
 | 
			
		||||
  isEdited: PropTypes.bool,
 | 
			
		||||
  msgType: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function MessageEdit({ body, onSave, onCancel }) {
 | 
			
		||||
  const editInputRef = useRef(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // makes the cursor end up at the end of the line instead of the beginning
 | 
			
		||||
    editInputRef.current.value = '';
 | 
			
		||||
    editInputRef.current.value = body;
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const handleKeyDown = (e) => {
 | 
			
		||||
    if (e.key === 'Escape') {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      onCancel();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (e.key === 'Enter' && e.shiftKey === false) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      onSave(editInputRef.current.value, body);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <form className="message__edit" onSubmit={(e) => { e.preventDefault(); onSave(editInputRef.current.value, body); }}>
 | 
			
		||||
      <Input
 | 
			
		||||
        forwardRef={editInputRef}
 | 
			
		||||
        onKeyDown={handleKeyDown}
 | 
			
		||||
        value={body}
 | 
			
		||||
        placeholder="Edit message"
 | 
			
		||||
        required
 | 
			
		||||
        resizable
 | 
			
		||||
        autoFocus
 | 
			
		||||
      />
 | 
			
		||||
      <div className="message__edit-btns">
 | 
			
		||||
        <Button type="submit" variant="primary">Save</Button>
 | 
			
		||||
        <Button onClick={onCancel}>Cancel</Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
MessageEdit.propTypes = {
 | 
			
		||||
  body: PropTypes.string.isRequired,
 | 
			
		||||
  onSave: PropTypes.func.isRequired,
 | 
			
		||||
  onCancel: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function getMyEmojiEvent(emojiKey, eventId, roomTimeline) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const rEvents = roomTimeline.reactionTimeline.get(eventId);
 | 
			
		||||
  let rEvent = null;
 | 
			
		||||
  rEvents?.find((rE) => {
 | 
			
		||||
    if (rE.getRelation() === null) return false;
 | 
			
		||||
    if (rE.getRelation().key === emojiKey && rE.getSender() === mx.getUserId()) {
 | 
			
		||||
      rEvent = rE;
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  });
 | 
			
		||||
  return rEvent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleEmoji(roomId, eventId, emojiKey, shortcode, roomTimeline) {
 | 
			
		||||
  const myAlreadyReactEvent = getMyEmojiEvent(emojiKey, eventId, roomTimeline);
 | 
			
		||||
  if (myAlreadyReactEvent) {
 | 
			
		||||
    const rId = myAlreadyReactEvent.getId();
 | 
			
		||||
    if (rId.startsWith('~')) return;
 | 
			
		||||
    redactEvent(roomId, rId);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  sendReaction(roomId, eventId, emojiKey, shortcode);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function pickEmoji(e, roomId, eventId, roomTimeline) {
 | 
			
		||||
  openEmojiBoard(getEventCords(e), (emoji) => {
 | 
			
		||||
    toggleEmoji(roomId, eventId, emoji.mxc ?? emoji.unicode, emoji.shortcodes[0], roomTimeline);
 | 
			
		||||
    e.target.click();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function genReactionMsg(userIds, reaction, shortcode) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {userIds.map((userId, index) => (
 | 
			
		||||
        <React.Fragment key={userId}>
 | 
			
		||||
          {twemojify(getUsername(userId))}
 | 
			
		||||
          {index < userIds.length - 1 && (
 | 
			
		||||
            <span style={{ opacity: '.6' }}>
 | 
			
		||||
              {index === userIds.length - 2 ? ' and ' : ', '}
 | 
			
		||||
            </span>
 | 
			
		||||
          )}
 | 
			
		||||
        </React.Fragment>
 | 
			
		||||
      ))}
 | 
			
		||||
      <span style={{ opacity: '.6' }}>{' reacted with '}</span>
 | 
			
		||||
      {twemojify(shortcode ? `:${shortcode}:` : reaction, { className: 'react-emoji' })}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function MessageReaction({
 | 
			
		||||
  reaction, shortcode, count, users, isActive, onClick,
 | 
			
		||||
}) {
 | 
			
		||||
  let customEmojiUrl = null;
 | 
			
		||||
  if (reaction.match(/^mxc:\/\/\S+$/)) {
 | 
			
		||||
    customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(reaction);
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <Tooltip
 | 
			
		||||
      className="msg__reaction-tooltip"
 | 
			
		||||
      content={<Text variant="b2">{users.length > 0 ? genReactionMsg(users, reaction, shortcode) : 'Unable to load who has reacted'}</Text>}
 | 
			
		||||
    >
 | 
			
		||||
      <button
 | 
			
		||||
        onClick={onClick}
 | 
			
		||||
        type="button"
 | 
			
		||||
        className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
 | 
			
		||||
      >
 | 
			
		||||
        {
 | 
			
		||||
          customEmojiUrl
 | 
			
		||||
            ? <img className="react-emoji" draggable="false" alt={shortcode ?? reaction} src={customEmojiUrl} />
 | 
			
		||||
            : twemojify(reaction, { className: 'react-emoji' })
 | 
			
		||||
        }
 | 
			
		||||
        <Text variant="b3" className="msg__reaction-count">{count}</Text>
 | 
			
		||||
      </button>
 | 
			
		||||
    </Tooltip>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
MessageReaction.defaultProps = {
 | 
			
		||||
  shortcode: undefined,
 | 
			
		||||
};
 | 
			
		||||
MessageReaction.propTypes = {
 | 
			
		||||
  reaction: PropTypes.node.isRequired,
 | 
			
		||||
  shortcode: PropTypes.string,
 | 
			
		||||
  count: PropTypes.number.isRequired,
 | 
			
		||||
  users: PropTypes.arrayOf(PropTypes.string).isRequired,
 | 
			
		||||
  isActive: PropTypes.bool.isRequired,
 | 
			
		||||
  onClick: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function MessageReactionGroup({ roomTimeline, mEvent }) {
 | 
			
		||||
  const { roomId, room, reactionTimeline } = roomTimeline;
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const reactions = {};
 | 
			
		||||
  const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
 | 
			
		||||
 | 
			
		||||
  const eventReactions = reactionTimeline.get(mEvent.getId());
 | 
			
		||||
  const addReaction = (key, shortcode, count, senderId, isActive) => {
 | 
			
		||||
    let reaction = reactions[key];
 | 
			
		||||
    if (reaction === undefined) {
 | 
			
		||||
      reaction = {
 | 
			
		||||
        count: 0,
 | 
			
		||||
        users: [],
 | 
			
		||||
        isActive: false,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    if (shortcode) reaction.shortcode = shortcode;
 | 
			
		||||
    if (count) {
 | 
			
		||||
      reaction.count = count;
 | 
			
		||||
    } else {
 | 
			
		||||
      reaction.users.push(senderId);
 | 
			
		||||
      reaction.count = reaction.users.length;
 | 
			
		||||
      if (isActive) reaction.isActive = isActive;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    reactions[key] = reaction;
 | 
			
		||||
  };
 | 
			
		||||
  if (eventReactions) {
 | 
			
		||||
    eventReactions.forEach((rEvent) => {
 | 
			
		||||
      if (rEvent.getRelation() === null) return;
 | 
			
		||||
      const reaction = rEvent.getRelation();
 | 
			
		||||
      const senderId = rEvent.getSender();
 | 
			
		||||
      const { shortcode } = rEvent.getContent();
 | 
			
		||||
      const isActive = senderId === mx.getUserId();
 | 
			
		||||
 | 
			
		||||
      addReaction(reaction.key, shortcode, undefined, senderId, isActive);
 | 
			
		||||
    });
 | 
			
		||||
  } else {
 | 
			
		||||
    // Use aggregated reactions
 | 
			
		||||
    const aggregatedReaction = mEvent.getServerAggregatedRelation('m.annotation')?.chunk;
 | 
			
		||||
    if (!aggregatedReaction) return null;
 | 
			
		||||
    aggregatedReaction.forEach((reaction) => {
 | 
			
		||||
      if (reaction.type !== 'm.reaction') return;
 | 
			
		||||
      addReaction(reaction.key, undefined, reaction.count, undefined, false);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="message__reactions text text-b3 noselect">
 | 
			
		||||
      {
 | 
			
		||||
        Object.keys(reactions).map((key) => (
 | 
			
		||||
          <MessageReaction
 | 
			
		||||
            key={key}
 | 
			
		||||
            reaction={key}
 | 
			
		||||
            shortcode={reactions[key].shortcode}
 | 
			
		||||
            count={reactions[key].count}
 | 
			
		||||
            users={reactions[key].users}
 | 
			
		||||
            isActive={reactions[key].isActive}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              toggleEmoji(roomId, mEvent.getId(), key, reactions[key].shortcode, roomTimeline);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        ))
 | 
			
		||||
      }
 | 
			
		||||
      {canSendReaction && (
 | 
			
		||||
        <IconButton
 | 
			
		||||
          onClick={(e) => {
 | 
			
		||||
            pickEmoji(e, roomId, mEvent.getId(), roomTimeline);
 | 
			
		||||
          }}
 | 
			
		||||
          src={EmojiAddIC}
 | 
			
		||||
          size="extra-small"
 | 
			
		||||
          tooltip="Add reaction"
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
MessageReactionGroup.propTypes = {
 | 
			
		||||
  roomTimeline: PropTypes.shape({}).isRequired,
 | 
			
		||||
  mEvent: PropTypes.shape({}).isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function isMedia(mE) {
 | 
			
		||||
  return (
 | 
			
		||||
    mE.getContent()?.msgtype === 'm.file'
 | 
			
		||||
    || mE.getContent()?.msgtype === 'm.image'
 | 
			
		||||
    || mE.getContent()?.msgtype === 'm.audio'
 | 
			
		||||
    || mE.getContent()?.msgtype === 'm.video'
 | 
			
		||||
    || mE.getType() === 'm.sticker'
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// if editedTimeline has mEventId then pass editedMEvent else pass mEvent to openViewSource
 | 
			
		||||
function handleOpenViewSource(mEvent, roomTimeline) {
 | 
			
		||||
  const eventId = mEvent.getId();
 | 
			
		||||
  const { editedTimeline } = roomTimeline ?? {};
 | 
			
		||||
  let editedMEvent;
 | 
			
		||||
  if (editedTimeline?.has(eventId)) {
 | 
			
		||||
    const editedList = editedTimeline.get(eventId);
 | 
			
		||||
    editedMEvent = editedList[editedList.length - 1];
 | 
			
		||||
  }
 | 
			
		||||
  openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const MessageOptions = React.memo(({
 | 
			
		||||
  roomTimeline, mEvent, edit, reply,
 | 
			
		||||
}) => {
 | 
			
		||||
  const { roomId, room } = roomTimeline;
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const senderId = mEvent.getSender();
 | 
			
		||||
 | 
			
		||||
  const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel;
 | 
			
		||||
  const canIRedact = room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
 | 
			
		||||
  const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="message__options">
 | 
			
		||||
      {canSendReaction && (
 | 
			
		||||
        <IconButton
 | 
			
		||||
          onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
 | 
			
		||||
          src={EmojiAddIC}
 | 
			
		||||
          size="extra-small"
 | 
			
		||||
          tooltip="Add reaction"
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      <IconButton
 | 
			
		||||
        onClick={() => reply()}
 | 
			
		||||
        src={ReplyArrowIC}
 | 
			
		||||
        size="extra-small"
 | 
			
		||||
        tooltip="Reply"
 | 
			
		||||
      />
 | 
			
		||||
      {(senderId === mx.getUserId() && !isMedia(mEvent)) && (
 | 
			
		||||
        <IconButton
 | 
			
		||||
          onClick={() => edit(true)}
 | 
			
		||||
          src={PencilIC}
 | 
			
		||||
          size="extra-small"
 | 
			
		||||
          tooltip="Edit"
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      <ContextMenu
 | 
			
		||||
        content={() => (
 | 
			
		||||
          <>
 | 
			
		||||
            <MenuHeader>Options</MenuHeader>
 | 
			
		||||
            <MenuItem
 | 
			
		||||
              iconSrc={TickMarkIC}
 | 
			
		||||
              onClick={() => openReadReceipts(roomId, roomTimeline.getEventReaders(mEvent))}
 | 
			
		||||
            >
 | 
			
		||||
              Read receipts
 | 
			
		||||
            </MenuItem>
 | 
			
		||||
            <MenuItem
 | 
			
		||||
              iconSrc={CmdIC}
 | 
			
		||||
              onClick={() => handleOpenViewSource(mEvent, roomTimeline)}
 | 
			
		||||
            >
 | 
			
		||||
              View source
 | 
			
		||||
            </MenuItem>
 | 
			
		||||
            {(canIRedact || senderId === mx.getUserId()) && (
 | 
			
		||||
              <>
 | 
			
		||||
                <MenuBorder />
 | 
			
		||||
                <MenuItem
 | 
			
		||||
                  variant="danger"
 | 
			
		||||
                  iconSrc={BinIC}
 | 
			
		||||
                  onClick={async () => {
 | 
			
		||||
                    const isConfirmed = await confirmDialog(
 | 
			
		||||
                      'Delete message',
 | 
			
		||||
                      'Are you sure that you want to delete this message?',
 | 
			
		||||
                      'Delete',
 | 
			
		||||
                      'danger',
 | 
			
		||||
                    );
 | 
			
		||||
                    if (!isConfirmed) return;
 | 
			
		||||
                    redactEvent(roomId, mEvent.getId());
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  Delete
 | 
			
		||||
                </MenuItem>
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
        render={(toggleMenu) => (
 | 
			
		||||
          <IconButton
 | 
			
		||||
            onClick={toggleMenu}
 | 
			
		||||
            src={VerticalMenuIC}
 | 
			
		||||
            size="extra-small"
 | 
			
		||||
            tooltip="Options"
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
MessageOptions.propTypes = {
 | 
			
		||||
  roomTimeline: PropTypes.shape({}).isRequired,
 | 
			
		||||
  mEvent: PropTypes.shape({}).isRequired,
 | 
			
		||||
  edit: PropTypes.func.isRequired,
 | 
			
		||||
  reply: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function genMediaContent(mE) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const mContent = mE.getContent();
 | 
			
		||||
  if (!mContent || !mContent.body) return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
 | 
			
		||||
 | 
			
		||||
  let mediaMXC = mContent?.url;
 | 
			
		||||
  const isEncryptedFile = typeof mediaMXC === 'undefined';
 | 
			
		||||
  if (isEncryptedFile) mediaMXC = mContent?.file?.url;
 | 
			
		||||
 | 
			
		||||
  let thumbnailMXC = mContent?.info?.thumbnail_url;
 | 
			
		||||
 | 
			
		||||
  if (typeof mediaMXC === 'undefined' || mediaMXC === '') return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
 | 
			
		||||
 | 
			
		||||
  let msgType = mE.getContent()?.msgtype;
 | 
			
		||||
  const safeMimetype = getBlobSafeMimeType(mContent.info?.mimetype);
 | 
			
		||||
  if (mE.getType() === 'm.sticker') {
 | 
			
		||||
    msgType = 'm.sticker';
 | 
			
		||||
  } else if (safeMimetype === 'application/octet-stream') {
 | 
			
		||||
    msgType = 'm.file';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const blurhash = mContent?.info?.['xyz.amorgan.blurhash'];
 | 
			
		||||
 | 
			
		||||
  switch (msgType) {
 | 
			
		||||
    case 'm.file':
 | 
			
		||||
      return (
 | 
			
		||||
        <Media.File
 | 
			
		||||
          name={mContent.body}
 | 
			
		||||
          link={mx.mxcUrlToHttp(mediaMXC)}
 | 
			
		||||
          type={mContent.info?.mimetype}
 | 
			
		||||
          file={mContent.file || null}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    case 'm.image':
 | 
			
		||||
      return (
 | 
			
		||||
        <Media.Image
 | 
			
		||||
          name={mContent.body}
 | 
			
		||||
          width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
 | 
			
		||||
          height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
 | 
			
		||||
          link={mx.mxcUrlToHttp(mediaMXC)}
 | 
			
		||||
          file={isEncryptedFile ? mContent.file : null}
 | 
			
		||||
          type={mContent.info?.mimetype}
 | 
			
		||||
          blurhash={blurhash}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    case 'm.sticker':
 | 
			
		||||
      return (
 | 
			
		||||
        <Media.Sticker
 | 
			
		||||
          name={mContent.body}
 | 
			
		||||
          width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
 | 
			
		||||
          height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
 | 
			
		||||
          link={mx.mxcUrlToHttp(mediaMXC)}
 | 
			
		||||
          file={isEncryptedFile ? mContent.file : null}
 | 
			
		||||
          type={mContent.info?.mimetype}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    case 'm.audio':
 | 
			
		||||
      return (
 | 
			
		||||
        <Media.Audio
 | 
			
		||||
          name={mContent.body}
 | 
			
		||||
          link={mx.mxcUrlToHttp(mediaMXC)}
 | 
			
		||||
          type={mContent.info?.mimetype}
 | 
			
		||||
          file={mContent.file || null}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    case 'm.video':
 | 
			
		||||
      if (typeof thumbnailMXC === 'undefined') {
 | 
			
		||||
        thumbnailMXC = mContent.info?.thumbnail_file?.url || null;
 | 
			
		||||
      }
 | 
			
		||||
      return (
 | 
			
		||||
        <Media.Video
 | 
			
		||||
          name={mContent.body}
 | 
			
		||||
          link={mx.mxcUrlToHttp(mediaMXC)}
 | 
			
		||||
          thumbnail={thumbnailMXC === null ? null : mx.mxcUrlToHttp(thumbnailMXC)}
 | 
			
		||||
          thumbnailFile={isEncryptedFile ? mContent.info?.thumbnail_file : null}
 | 
			
		||||
          thumbnailType={mContent.info?.thumbnail_info?.mimetype || null}
 | 
			
		||||
          width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
 | 
			
		||||
          height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
 | 
			
		||||
          file={isEncryptedFile ? mContent.file : null}
 | 
			
		||||
          type={mContent.info?.mimetype}
 | 
			
		||||
          blurhash={blurhash}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getEditedBody(editedMEvent) {
 | 
			
		||||
  const newContent = editedMEvent.getContent()['m.new_content'];
 | 
			
		||||
  if (typeof newContent === 'undefined') return [null, false, null];
 | 
			
		||||
 | 
			
		||||
  const isCustomHTML = newContent.format === 'org.matrix.custom.html';
 | 
			
		||||
  const parsedContent = parseReply(newContent.body);
 | 
			
		||||
  if (parsedContent === null) {
 | 
			
		||||
    return [newContent.body, isCustomHTML, newContent.formatted_body ?? null];
 | 
			
		||||
  }
 | 
			
		||||
  return [parsedContent.body, isCustomHTML, newContent.formatted_body ?? null];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Message({
 | 
			
		||||
  mEvent, isBodyOnly, roomTimeline,
 | 
			
		||||
  focus, fullTime, isEdit, setEdit, cancelEdit,
 | 
			
		||||
}) {
 | 
			
		||||
  const roomId = mEvent.getRoomId();
 | 
			
		||||
  const { editedTimeline, reactionTimeline } = roomTimeline ?? {};
 | 
			
		||||
 | 
			
		||||
  const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')];
 | 
			
		||||
  if (focus) className.push('message--focus');
 | 
			
		||||
  const content = mEvent.getContent();
 | 
			
		||||
  const eventId = mEvent.getId();
 | 
			
		||||
  const msgType = content?.msgtype;
 | 
			
		||||
  const senderId = mEvent.getSender();
 | 
			
		||||
  let { body } = content;
 | 
			
		||||
  const username = mEvent.sender ? getUsernameOfRoomMember(mEvent.sender) : getUsername(senderId);
 | 
			
		||||
  const avatarSrc = mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
 | 
			
		||||
  let isCustomHTML = content.format === 'org.matrix.custom.html';
 | 
			
		||||
  let customHTML = isCustomHTML ? content.formatted_body : null;
 | 
			
		||||
 | 
			
		||||
  const edit = useCallback(() => {
 | 
			
		||||
    setEdit(eventId);
 | 
			
		||||
  }, []);
 | 
			
		||||
  const reply = useCallback(() => {
 | 
			
		||||
    replyTo(senderId, mEvent.getId(), body, customHTML);
 | 
			
		||||
  }, [body, customHTML]);
 | 
			
		||||
 | 
			
		||||
  if (msgType === 'm.emote') className.push('message--type-emote');
 | 
			
		||||
 | 
			
		||||
  const isEdited = roomTimeline ? editedTimeline.has(eventId) : false;
 | 
			
		||||
  const haveReactions = roomTimeline
 | 
			
		||||
    ? reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation')
 | 
			
		||||
    : false;
 | 
			
		||||
  const isReply = !!mEvent.replyEventId;
 | 
			
		||||
 | 
			
		||||
  if (isEdited) {
 | 
			
		||||
    const editedList = editedTimeline.get(eventId);
 | 
			
		||||
    const editedMEvent = editedList[editedList.length - 1];
 | 
			
		||||
    [body, isCustomHTML, customHTML] = getEditedBody(editedMEvent);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (isReply) {
 | 
			
		||||
    body = parseReply(body)?.body ?? body;
 | 
			
		||||
    customHTML = trimHTMLReply(customHTML);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (typeof body !== 'string') body = '';
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={className.join(' ')}>
 | 
			
		||||
      {
 | 
			
		||||
        isBodyOnly
 | 
			
		||||
          ? <div className="message__avatar-container" />
 | 
			
		||||
          : (
 | 
			
		||||
            <MessageAvatar
 | 
			
		||||
              roomId={roomId}
 | 
			
		||||
              avatarSrc={avatarSrc}
 | 
			
		||||
              userId={senderId}
 | 
			
		||||
              username={username}
 | 
			
		||||
            />
 | 
			
		||||
          )
 | 
			
		||||
      }
 | 
			
		||||
      <div className="message__main-container">
 | 
			
		||||
        {!isBodyOnly && (
 | 
			
		||||
          <MessageHeader
 | 
			
		||||
            userId={senderId}
 | 
			
		||||
            username={username}
 | 
			
		||||
            timestamp={mEvent.getTs()}
 | 
			
		||||
            fullTime={fullTime}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        {roomTimeline && isReply && (
 | 
			
		||||
          <MessageReplyWrapper
 | 
			
		||||
            roomTimeline={roomTimeline}
 | 
			
		||||
            eventId={mEvent.replyEventId}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        {!isEdit && (
 | 
			
		||||
          <MessageBody
 | 
			
		||||
            senderName={username}
 | 
			
		||||
            isCustomHTML={isCustomHTML}
 | 
			
		||||
            body={isMedia(mEvent) ? genMediaContent(mEvent) : customHTML ?? body}
 | 
			
		||||
            msgType={msgType}
 | 
			
		||||
            isEdited={isEdited}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        {isEdit && (
 | 
			
		||||
          <MessageEdit
 | 
			
		||||
            body={(customHTML
 | 
			
		||||
              ? html(customHTML, { kind: 'edit', onlyPlain: true }).plain
 | 
			
		||||
              : plain(body, { kind: 'edit', onlyPlain: true }).plain)}
 | 
			
		||||
            onSave={(newBody, oldBody) => {
 | 
			
		||||
              if (newBody !== oldBody) {
 | 
			
		||||
                initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
 | 
			
		||||
              }
 | 
			
		||||
              cancelEdit();
 | 
			
		||||
            }}
 | 
			
		||||
            onCancel={cancelEdit}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        {haveReactions && (
 | 
			
		||||
          <MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
 | 
			
		||||
        )}
 | 
			
		||||
        {roomTimeline && !isEdit && (
 | 
			
		||||
          <MessageOptions
 | 
			
		||||
            roomTimeline={roomTimeline}
 | 
			
		||||
            mEvent={mEvent}
 | 
			
		||||
            edit={edit}
 | 
			
		||||
            reply={reply}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
Message.defaultProps = {
 | 
			
		||||
  isBodyOnly: false,
 | 
			
		||||
  focus: false,
 | 
			
		||||
  roomTimeline: null,
 | 
			
		||||
  fullTime: false,
 | 
			
		||||
  isEdit: false,
 | 
			
		||||
  setEdit: null,
 | 
			
		||||
  cancelEdit: null,
 | 
			
		||||
};
 | 
			
		||||
Message.propTypes = {
 | 
			
		||||
  mEvent: PropTypes.shape({}).isRequired,
 | 
			
		||||
  isBodyOnly: PropTypes.bool,
 | 
			
		||||
  roomTimeline: PropTypes.shape({}),
 | 
			
		||||
  focus: PropTypes.bool,
 | 
			
		||||
  fullTime: PropTypes.bool,
 | 
			
		||||
  isEdit: PropTypes.bool,
 | 
			
		||||
  setEdit: PropTypes.func,
 | 
			
		||||
  cancelEdit: PropTypes.func,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { Message, MessageReply, PlaceholderMessage };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,479 +0,0 @@
 | 
			
		|||
@use '../../atoms/scroll/scrollbar';
 | 
			
		||||
@use '../../partials/text';
 | 
			
		||||
@use '../../partials/dir';
 | 
			
		||||
@use '../../partials/screen';
 | 
			
		||||
 | 
			
		||||
.message,
 | 
			
		||||
.ph-msg {
 | 
			
		||||
  padding: var(--sp-ultra-tight);
 | 
			
		||||
  @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
 | 
			
		||||
  display: flex;
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background-color: var(--bg-surface-hover);
 | 
			
		||||
    & .message__options {
 | 
			
		||||
      display: flex;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__avatar-container {
 | 
			
		||||
    padding-top: 6px;
 | 
			
		||||
    @include dir.side(margin, 0, var(--sp-tight));
 | 
			
		||||
 | 
			
		||||
    & .avatar-container {
 | 
			
		||||
      transition: transform 200ms var(--fluid-push);
 | 
			
		||||
      &:hover {
 | 
			
		||||
        transform: translateY(-4px);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & button {
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      display: flex;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__main-container {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
 | 
			
		||||
    position: relative;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.message {
 | 
			
		||||
  &--full + &--full,
 | 
			
		||||
  &--body-only + &--full,
 | 
			
		||||
  & + .timeline-change,
 | 
			
		||||
  .timeline-change + & {
 | 
			
		||||
    margin-top: var(--sp-normal);
 | 
			
		||||
  }
 | 
			
		||||
  &__avatar-container {
 | 
			
		||||
    width: var(--av-small);
 | 
			
		||||
  }
 | 
			
		||||
  &--focus {
 | 
			
		||||
    --ltr: inset 2px 0 0 var(--bg-caution);
 | 
			
		||||
    --rtl: inset -2px 0 0 var(--bg-caution);
 | 
			
		||||
    @include dir.prop(box-shadow, var(--ltr), var(--rtl));
 | 
			
		||||
    background-color: var(--bg-caution-hover);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ph-msg {
 | 
			
		||||
  &__avatar {
 | 
			
		||||
    width: var(--av-small);
 | 
			
		||||
    height: var(--av-small);
 | 
			
		||||
    background-color: var(--bg-surface-hover);
 | 
			
		||||
    border-radius: var(--bo-radius);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__header,
 | 
			
		||||
  &__body > div {
 | 
			
		||||
    margin: var(--sp-ultra-tight);
 | 
			
		||||
    @include dir.side(margin, 0, var(--sp-extra-tight));
 | 
			
		||||
    height: var(--fs-b1);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    max-width: 100px;
 | 
			
		||||
    background-color: var(--bg-surface-hover);
 | 
			
		||||
    border-radius: calc(var(--bo-radius) / 2);
 | 
			
		||||
  }
 | 
			
		||||
  &__body {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
  }
 | 
			
		||||
  &__body > div:nth-child(1n) {
 | 
			
		||||
    max-width: 10%;
 | 
			
		||||
  }
 | 
			
		||||
  &__body > div:nth-child(2n) {
 | 
			
		||||
    max-width: 50%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.message__reply,
 | 
			
		||||
.message__body,
 | 
			
		||||
.message__body__wrapper,
 | 
			
		||||
.message__edit,
 | 
			
		||||
.message__reactions {
 | 
			
		||||
  max-width: calc(100% - 88px);
 | 
			
		||||
  min-width: 0;
 | 
			
		||||
  @include screen.smallerThan(tabletBreakpoint) {
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.message__header {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: baseline;
 | 
			
		||||
 | 
			
		||||
  & .message__profile {
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
    color: var(--tc-surface-high);
 | 
			
		||||
    @include dir.side(margin, 0, var(--sp-tight));
 | 
			
		||||
 | 
			
		||||
    & > span {
 | 
			
		||||
      @extend .cp-txt__ellipsis;
 | 
			
		||||
      color: inherit;
 | 
			
		||||
    }
 | 
			
		||||
    & > span:last-child {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
    &:hover {
 | 
			
		||||
      & > span:first-child {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
      & > span:last-child {
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & .message__time {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
    & > .text {
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
      color: var(--tc-surface-low);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.message__reply {
 | 
			
		||||
  &-wrapper {
 | 
			
		||||
    min-height: 20px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    &:empty {
 | 
			
		||||
      border-radius: calc(var(--bo-radius) / 2);
 | 
			
		||||
      background-color: var(--bg-surface-hover);
 | 
			
		||||
      max-width: 200px;
 | 
			
		||||
      cursor: auto;
 | 
			
		||||
    }
 | 
			
		||||
    &:hover .text {
 | 
			
		||||
      color: var(--tc-surface-high);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .text {
 | 
			
		||||
    @extend .cp-txt__ellipsis;
 | 
			
		||||
    color: var(--tc-surface-low);
 | 
			
		||||
  }
 | 
			
		||||
  .ic-raw {
 | 
			
		||||
    width: 16px;
 | 
			
		||||
    height: 14px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.message__body {
 | 
			
		||||
  word-break: break-word;
 | 
			
		||||
 | 
			
		||||
  & > .text > .message__body-plain {
 | 
			
		||||
    white-space: pre-wrap;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & a {
 | 
			
		||||
    word-break: break-word;
 | 
			
		||||
  }
 | 
			
		||||
  & > .text > a {
 | 
			
		||||
    white-space: initial !important;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & > .text > p + p {
 | 
			
		||||
    margin-top: var(--sp-normal);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & span[data-mx-pill] {
 | 
			
		||||
    background-color: hsla(0, 0%, 64%, 0.15);
 | 
			
		||||
    padding: 0 2px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    font-weight: var(--fw-medium);
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: hsla(0, 0%, 64%, 0.3);
 | 
			
		||||
      color: var(--tc-surface-high);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &[data-mx-ping] {
 | 
			
		||||
      background-color: var(--bg-ping);
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background-color: var(--bg-ping-hover);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & span[data-mx-spoiler] {
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    background-color: rgba(124, 124, 124, 0.5);
 | 
			
		||||
    color: transparent;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    -webkit-touch-callout: none;
 | 
			
		||||
    -webkit-user-select: none;
 | 
			
		||||
    -khtml-user-select: none;
 | 
			
		||||
    -moz-user-select: none;
 | 
			
		||||
    -ms-user-select: none;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
    & > * {
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .data-mx-spoiler--visible {
 | 
			
		||||
    background-color: var(--bg-surface-active) !important;
 | 
			
		||||
    color: inherit !important;
 | 
			
		||||
    user-select: initial !important;
 | 
			
		||||
    & > * {
 | 
			
		||||
      opacity: inherit !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &-edited {
 | 
			
		||||
    color: var(--tc-surface-low);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.message__edit {
 | 
			
		||||
  padding: var(--sp-extra-tight) 0;
 | 
			
		||||
  &-btns button {
 | 
			
		||||
    margin: var(--sp-tight) 0 0 0;
 | 
			
		||||
    padding: var(--sp-ultra-tight) var(--sp-tight);
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
    @include dir.side(margin, 0, var(--sp-tight));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.message__reactions {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
 | 
			
		||||
  & .ic-btn-surface {
 | 
			
		||||
    display: none;
 | 
			
		||||
    padding: var(--sp-ultra-tight);
 | 
			
		||||
    margin-top: var(--sp-extra-tight);
 | 
			
		||||
  }
 | 
			
		||||
  &:hover .ic-btn-surface {
 | 
			
		||||
    display: block;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.msg__reaction {
 | 
			
		||||
  margin: var(--sp-extra-tight) 0 0 0;
 | 
			
		||||
  @include dir.side(margin, 0, var(--sp-extra-tight));
 | 
			
		||||
  padding: 0 var(--sp-ultra-tight);
 | 
			
		||||
  min-height: 26px;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  color: var(--tc-surface-normal);
 | 
			
		||||
  background-color: var(--bg-surface-low);
 | 
			
		||||
  border: 1px solid var(--bg-surface-border);
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
 | 
			
		||||
  & .react-emoji {
 | 
			
		||||
    height: 16px;
 | 
			
		||||
    margin: 2px;
 | 
			
		||||
  }
 | 
			
		||||
  &-count {
 | 
			
		||||
    margin: 0 var(--sp-ultra-tight);
 | 
			
		||||
    color: var(--tc-surface-normal);
 | 
			
		||||
  }
 | 
			
		||||
  &-tooltip .react-emoji {
 | 
			
		||||
    width: 16px;
 | 
			
		||||
    height: 16px;
 | 
			
		||||
    margin: 0 var(--sp-ultra-tight);
 | 
			
		||||
    margin-bottom: -2px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @media (hover: hover) {
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: var(--bg-surface-hover);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &:active {
 | 
			
		||||
    background-color: var(--bg-surface-active);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--active {
 | 
			
		||||
    background-color: var(--bg-caution-active);
 | 
			
		||||
 | 
			
		||||
    @media (hover: hover) {
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background-color: var(--bg-caution-hover);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &:active {
 | 
			
		||||
      background-color: var(--bg-caution-active);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.message__options {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  @include dir.prop(right, 60px, unset);
 | 
			
		||||
  @include dir.prop(left, unset, 60px);
 | 
			
		||||
 | 
			
		||||
  z-index: 99;
 | 
			
		||||
  transform: translateY(-100%);
 | 
			
		||||
 | 
			
		||||
  border-radius: var(--bo-radius);
 | 
			
		||||
  box-shadow: var(--bs-surface-border);
 | 
			
		||||
  background-color: var(--bg-surface-low);
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// markdown formating
 | 
			
		||||
.message__body {
 | 
			
		||||
  & h1,
 | 
			
		||||
  h2,
 | 
			
		||||
  h3,
 | 
			
		||||
  h4,
 | 
			
		||||
  h5,
 | 
			
		||||
  h6 {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    margin-bottom: var(--sp-ultra-tight);
 | 
			
		||||
    font-weight: var(--fw-medium);
 | 
			
		||||
    &:first-child {
 | 
			
		||||
      margin-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  & h1,
 | 
			
		||||
  & h2 {
 | 
			
		||||
    color: var(--tc-surface-high);
 | 
			
		||||
    margin-top: var(--sp-normal);
 | 
			
		||||
    font-size: var(--fs-h2);
 | 
			
		||||
    line-height: var(--lh-h2);
 | 
			
		||||
    letter-spacing: var(--ls-h2);
 | 
			
		||||
  }
 | 
			
		||||
  & h3,
 | 
			
		||||
  & h4 {
 | 
			
		||||
    color: var(--tc-surface-high);
 | 
			
		||||
    margin-top: var(--sp-tight);
 | 
			
		||||
    font-size: var(--fs-s1);
 | 
			
		||||
    line-height: var(--lh-s1);
 | 
			
		||||
    letter-spacing: var(--ls-s1);
 | 
			
		||||
  }
 | 
			
		||||
  & h5,
 | 
			
		||||
  & h6 {
 | 
			
		||||
    color: var(--tc-surface-high);
 | 
			
		||||
    margin-top: var(--sp-extra-tight);
 | 
			
		||||
    font-size: var(--fs-b1);
 | 
			
		||||
    line-height: var(--lh-b1);
 | 
			
		||||
    letter-spacing: var(--ls-b1);
 | 
			
		||||
  }
 | 
			
		||||
  & hr {
 | 
			
		||||
    border-color: var(--bg-divider);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .text img {
 | 
			
		||||
    margin: var(--sp-ultra-tight) 0;
 | 
			
		||||
    max-width: 296px;
 | 
			
		||||
    border-radius: calc(var(--bo-radius) / 2);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & p,
 | 
			
		||||
  & pre,
 | 
			
		||||
  & blockquote {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
  }
 | 
			
		||||
  & pre,
 | 
			
		||||
  & blockquote {
 | 
			
		||||
    margin: var(--sp-ultra-tight) 0;
 | 
			
		||||
    padding: var(--sp-extra-tight);
 | 
			
		||||
    background-color: var(--bg-surface-hover) !important;
 | 
			
		||||
    border-radius: calc(var(--bo-radius) / 2);
 | 
			
		||||
  }
 | 
			
		||||
  & pre {
 | 
			
		||||
    div {
 | 
			
		||||
      background: none !important;
 | 
			
		||||
      margin: 0 !important;
 | 
			
		||||
    }
 | 
			
		||||
    span {
 | 
			
		||||
      background: none !important;
 | 
			
		||||
    }
 | 
			
		||||
    .linenumber {
 | 
			
		||||
      min-width: 2.25em !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  & code {
 | 
			
		||||
    padding: 0 !important;
 | 
			
		||||
    color: var(--tc-code) !important;
 | 
			
		||||
    white-space: pre-wrap;
 | 
			
		||||
    @include scrollbar.scroll;
 | 
			
		||||
    @include scrollbar.scroll__h;
 | 
			
		||||
    @include scrollbar.scroll--auto-hide;
 | 
			
		||||
  }
 | 
			
		||||
  & pre {
 | 
			
		||||
    width: fit-content;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
    @include scrollbar.scroll;
 | 
			
		||||
    @include scrollbar.scroll__h;
 | 
			
		||||
    @include scrollbar.scroll--auto-hide;
 | 
			
		||||
    & code {
 | 
			
		||||
      color: var(--tc-surface-normal) !important;
 | 
			
		||||
      white-space: pre;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  & blockquote {
 | 
			
		||||
    width: fit-content;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
    @include dir.side(border, 4px solid var(--bg-surface-active), 0);
 | 
			
		||||
    white-space: initial !important;
 | 
			
		||||
 | 
			
		||||
    & > * {
 | 
			
		||||
      white-space: pre-wrap;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  & ul,
 | 
			
		||||
  & ol {
 | 
			
		||||
    margin: var(--sp-ultra-tight) 0;
 | 
			
		||||
    @include dir.side(padding, 24px, 0);
 | 
			
		||||
    white-space: initial !important;
 | 
			
		||||
  }
 | 
			
		||||
  & ul.contains-task-list {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    list-style: none;
 | 
			
		||||
  }
 | 
			
		||||
  & table {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
    white-space: normal !important;
 | 
			
		||||
    background-color: var(--bg-surface-hover);
 | 
			
		||||
    border-radius: calc(var(--bo-radius) / 2);
 | 
			
		||||
    border-spacing: 0;
 | 
			
		||||
    border: 1px solid var(--bg-surface-border);
 | 
			
		||||
    @include scrollbar.scroll;
 | 
			
		||||
    @include scrollbar.scroll__h;
 | 
			
		||||
    @include scrollbar.scroll--auto-hide;
 | 
			
		||||
 | 
			
		||||
    & td,
 | 
			
		||||
    & th {
 | 
			
		||||
      padding: var(--sp-extra-tight);
 | 
			
		||||
      border: 1px solid var(--bg-surface-border);
 | 
			
		||||
      border-width: 0 1px 1px 0;
 | 
			
		||||
      white-space: pre;
 | 
			
		||||
      &:last-child {
 | 
			
		||||
        border-width: 0;
 | 
			
		||||
        border-bottom-width: 1px;
 | 
			
		||||
        [dir='rtl'] & {
 | 
			
		||||
          border-width: 0 1px 1px 0;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      [dir='rtl'] &:first-child {
 | 
			
		||||
        border-width: 0;
 | 
			
		||||
        border-bottom-width: 1px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    & tbody tr:nth-child(2n + 1) {
 | 
			
		||||
      background-color: var(--bg-surface-hover);
 | 
			
		||||
    }
 | 
			
		||||
    & tr:last-child td {
 | 
			
		||||
      border-bottom-width: 0px !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.message.message--type-emote {
 | 
			
		||||
  .message__body {
 | 
			
		||||
    font-style: italic;
 | 
			
		||||
 | 
			
		||||
    // Remove blockness of first `<p>` so that markdown emotes stay on one line.
 | 
			
		||||
    p:first-of-type {
 | 
			
		||||
      display: inline;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,78 +0,0 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './TimelineChange.scss';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import RawIcon from '../../atoms/system-icons/RawIcon';
 | 
			
		||||
import Time from '../../atoms/time/Time';
 | 
			
		||||
 | 
			
		||||
import JoinArraowIC from '../../../../public/res/ic/outlined/join-arrow.svg';
 | 
			
		||||
import LeaveArraowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
 | 
			
		||||
import InviteArraowIC from '../../../../public/res/ic/outlined/invite-arrow.svg';
 | 
			
		||||
import InviteCancelArraowIC from '../../../../public/res/ic/outlined/invite-cancel-arrow.svg';
 | 
			
		||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
 | 
			
		||||
 | 
			
		||||
function TimelineChange({
 | 
			
		||||
  variant, content, timestamp, onClick,
 | 
			
		||||
}) {
 | 
			
		||||
  let iconSrc;
 | 
			
		||||
 | 
			
		||||
  switch (variant) {
 | 
			
		||||
    case 'join':
 | 
			
		||||
      iconSrc = JoinArraowIC;
 | 
			
		||||
      break;
 | 
			
		||||
    case 'leave':
 | 
			
		||||
      iconSrc = LeaveArraowIC;
 | 
			
		||||
      break;
 | 
			
		||||
    case 'invite':
 | 
			
		||||
      iconSrc = InviteArraowIC;
 | 
			
		||||
      break;
 | 
			
		||||
    case 'invite-cancel':
 | 
			
		||||
      iconSrc = InviteCancelArraowIC;
 | 
			
		||||
      break;
 | 
			
		||||
    case 'avatar':
 | 
			
		||||
      iconSrc = UserIC;
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      iconSrc = JoinArraowIC;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <button style={{ cursor: onClick === null ? 'default' : 'pointer' }} onClick={onClick} type="button" className="timeline-change">
 | 
			
		||||
      <div className="timeline-change__avatar-container">
 | 
			
		||||
        <RawIcon src={iconSrc} size="extra-small" />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="timeline-change__content">
 | 
			
		||||
        <Text variant="b2">
 | 
			
		||||
          {content}
 | 
			
		||||
        </Text>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="timeline-change__time">
 | 
			
		||||
        <Text variant="b3">
 | 
			
		||||
          <Time timestamp={timestamp} />
 | 
			
		||||
        </Text>
 | 
			
		||||
      </div>
 | 
			
		||||
    </button>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TimelineChange.defaultProps = {
 | 
			
		||||
  variant: 'other',
 | 
			
		||||
  onClick: null,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TimelineChange.propTypes = {
 | 
			
		||||
  variant: PropTypes.oneOf([
 | 
			
		||||
    'join', 'leave', 'invite',
 | 
			
		||||
    'invite-cancel', 'avatar', 'other',
 | 
			
		||||
  ]),
 | 
			
		||||
  content: PropTypes.oneOfType([
 | 
			
		||||
    PropTypes.string,
 | 
			
		||||
    PropTypes.node,
 | 
			
		||||
  ]).isRequired,
 | 
			
		||||
  timestamp: PropTypes.number.isRequired,
 | 
			
		||||
  onClick: PropTypes.func,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default TimelineChange;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,37 +0,0 @@
 | 
			
		|||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.timeline-change {
 | 
			
		||||
  padding: var(--sp-ultra-tight);
 | 
			
		||||
  @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
 | 
			
		||||
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background-color: var(--bg-surface-hover);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__avatar-container {
 | 
			
		||||
    width: var(--av-small);
 | 
			
		||||
    display: inline-flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    opacity: 0.38;
 | 
			
		||||
    .ic-raw {
 | 
			
		||||
      background-color: var(--tc-surface-low);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & .text {
 | 
			
		||||
    color: var(--tc-surface-low);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__content {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
 | 
			
		||||
    margin: 0 var(--sp-tight);
 | 
			
		||||
    word-break: break-word;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,16 +2,12 @@ import React from 'react';
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import './PeopleSelector.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import { blurOnBubbling } from '../../atoms/button/script';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import Avatar from '../../atoms/avatar/Avatar';
 | 
			
		||||
 | 
			
		||||
function PeopleSelector({
 | 
			
		||||
  avatarSrc, name, color, peopleRole, onClick,
 | 
			
		||||
}) {
 | 
			
		||||
function PeopleSelector({ avatarSrc, name, color, peopleRole, onClick }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="people-selector__container">
 | 
			
		||||
      <button
 | 
			
		||||
| 
						 | 
				
			
			@ -21,8 +17,14 @@ function PeopleSelector({
 | 
			
		|||
        type="button"
 | 
			
		||||
      >
 | 
			
		||||
        <Avatar imageSrc={avatarSrc} text={name} bgColor={color} size="extra-small" />
 | 
			
		||||
        <Text className="people-selector__name" variant="b1">{twemojify(name)}</Text>
 | 
			
		||||
        {peopleRole !== null && <Text className="people-selector__role" variant="b3">{peopleRole}</Text>}
 | 
			
		||||
        <Text className="people-selector__name" variant="b1">
 | 
			
		||||
          {name}
 | 
			
		||||
        </Text>
 | 
			
		||||
        {peopleRole !== null && (
 | 
			
		||||
          <Text className="people-selector__role" variant="b3">
 | 
			
		||||
            {peopleRole}
 | 
			
		||||
          </Text>
 | 
			
		||||
        )}
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,8 +2,6 @@ import React from 'react';
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import './PopupWindow.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
 | 
			
		||||
| 
						 | 
				
			
			@ -13,19 +11,11 @@ import RawModal from '../../atoms/modal/RawModal';
 | 
			
		|||
 | 
			
		||||
import ChevronLeftIC from '../../../../public/res/ic/outlined/chevron-left.svg';
 | 
			
		||||
 | 
			
		||||
function PWContentSelector({
 | 
			
		||||
  selected, variant, iconSrc,
 | 
			
		||||
  type, onClick, children,
 | 
			
		||||
}) {
 | 
			
		||||
function PWContentSelector({ selected, variant, iconSrc, type, onClick, children }) {
 | 
			
		||||
  const pwcsClass = selected ? ' pw-content-selector--selected' : '';
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={`pw-content-selector${pwcsClass}`}>
 | 
			
		||||
      <MenuItem
 | 
			
		||||
        variant={variant}
 | 
			
		||||
        iconSrc={iconSrc}
 | 
			
		||||
        type={type}
 | 
			
		||||
        onClick={onClick}
 | 
			
		||||
      >
 | 
			
		||||
      <MenuItem variant={variant} iconSrc={iconSrc} type={type} onClick={onClick}>
 | 
			
		||||
        {children}
 | 
			
		||||
      </MenuItem>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -49,9 +39,16 @@ PWContentSelector.propTypes = {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
function PopupWindow({
 | 
			
		||||
  className, isOpen, title, contentTitle,
 | 
			
		||||
  drawer, drawerOptions, contentOptions,
 | 
			
		||||
  onAfterClose, onRequestClose, children,
 | 
			
		||||
  className,
 | 
			
		||||
  isOpen,
 | 
			
		||||
  title,
 | 
			
		||||
  contentTitle,
 | 
			
		||||
  drawer,
 | 
			
		||||
  drawerOptions,
 | 
			
		||||
  contentOptions,
 | 
			
		||||
  onAfterClose,
 | 
			
		||||
  onRequestClose,
 | 
			
		||||
  children,
 | 
			
		||||
}) {
 | 
			
		||||
  const haveDrawer = drawer !== null;
 | 
			
		||||
  const cTitle = contentTitle !== null ? contentTitle : title;
 | 
			
		||||
| 
						 | 
				
			
			@ -69,21 +66,26 @@ function PopupWindow({
 | 
			
		|||
        {haveDrawer && (
 | 
			
		||||
          <div className="pw__drawer">
 | 
			
		||||
            <Header>
 | 
			
		||||
              <IconButton size="small" src={ChevronLeftIC} onClick={onRequestClose} tooltip="Back" />
 | 
			
		||||
              <IconButton
 | 
			
		||||
                size="small"
 | 
			
		||||
                src={ChevronLeftIC}
 | 
			
		||||
                onClick={onRequestClose}
 | 
			
		||||
                tooltip="Back"
 | 
			
		||||
              />
 | 
			
		||||
              <TitleWrapper>
 | 
			
		||||
                {
 | 
			
		||||
                  typeof title === 'string'
 | 
			
		||||
                    ? <Text variant="s1" weight="medium" primary>{twemojify(title)}</Text>
 | 
			
		||||
                    : title
 | 
			
		||||
                }
 | 
			
		||||
                {typeof title === 'string' ? (
 | 
			
		||||
                  <Text variant="s1" weight="medium" primary>
 | 
			
		||||
                    {title}
 | 
			
		||||
                  </Text>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  title
 | 
			
		||||
                )}
 | 
			
		||||
              </TitleWrapper>
 | 
			
		||||
              {drawerOptions}
 | 
			
		||||
            </Header>
 | 
			
		||||
            <div className="pw__drawer__content__wrapper">
 | 
			
		||||
              <ScrollView invisible>
 | 
			
		||||
                <div className="pw__drawer__content">
 | 
			
		||||
                  {drawer}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="pw__drawer__content">{drawer}</div>
 | 
			
		||||
              </ScrollView>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -91,19 +93,19 @@ function PopupWindow({
 | 
			
		|||
        <div className="pw__content">
 | 
			
		||||
          <Header>
 | 
			
		||||
            <TitleWrapper>
 | 
			
		||||
              {
 | 
			
		||||
                typeof cTitle === 'string'
 | 
			
		||||
                  ? <Text variant="h2" weight="medium" primary>{twemojify(cTitle)}</Text>
 | 
			
		||||
                  : cTitle
 | 
			
		||||
              }
 | 
			
		||||
              {typeof cTitle === 'string' ? (
 | 
			
		||||
                <Text variant="h2" weight="medium" primary>
 | 
			
		||||
                  {cTitle}
 | 
			
		||||
                </Text>
 | 
			
		||||
              ) : (
 | 
			
		||||
                cTitle
 | 
			
		||||
              )}
 | 
			
		||||
            </TitleWrapper>
 | 
			
		||||
            {contentOptions}
 | 
			
		||||
          </Header>
 | 
			
		||||
          <div className="pw__content__wrapper">
 | 
			
		||||
            <ScrollView autoHide>
 | 
			
		||||
              <div className="pw__content-container">
 | 
			
		||||
                {children}
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="pw__content-container">{children}</div>
 | 
			
		||||
            </ScrollView>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,28 +13,33 @@ import BellIC from '../../../../public/res/ic/outlined/bell.svg';
 | 
			
		|||
import BellRingIC from '../../../../public/res/ic/outlined/bell-ring.svg';
 | 
			
		||||
import BellPingIC from '../../../../public/res/ic/outlined/bell-ping.svg';
 | 
			
		||||
import BellOffIC from '../../../../public/res/ic/outlined/bell-off.svg';
 | 
			
		||||
import { getNotificationType } from '../../utils/room';
 | 
			
		||||
 | 
			
		||||
const items = [{
 | 
			
		||||
  iconSrc: BellIC,
 | 
			
		||||
  text: 'Global',
 | 
			
		||||
  type: cons.notifs.DEFAULT,
 | 
			
		||||
}, {
 | 
			
		||||
  iconSrc: BellRingIC,
 | 
			
		||||
  text: 'All messages',
 | 
			
		||||
  type: cons.notifs.ALL_MESSAGES,
 | 
			
		||||
}, {
 | 
			
		||||
  iconSrc: BellPingIC,
 | 
			
		||||
  text: 'Mentions & Keywords',
 | 
			
		||||
  type: cons.notifs.MENTIONS_AND_KEYWORDS,
 | 
			
		||||
}, {
 | 
			
		||||
  iconSrc: BellOffIC,
 | 
			
		||||
  text: 'Mute',
 | 
			
		||||
  type: cons.notifs.MUTE,
 | 
			
		||||
}];
 | 
			
		||||
const items = [
 | 
			
		||||
  {
 | 
			
		||||
    iconSrc: BellIC,
 | 
			
		||||
    text: 'Global',
 | 
			
		||||
    type: cons.notifs.DEFAULT,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    iconSrc: BellRingIC,
 | 
			
		||||
    text: 'All messages',
 | 
			
		||||
    type: cons.notifs.ALL_MESSAGES,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    iconSrc: BellPingIC,
 | 
			
		||||
    text: 'Mentions & Keywords',
 | 
			
		||||
    type: cons.notifs.MENTIONS_AND_KEYWORDS,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    iconSrc: BellOffIC,
 | 
			
		||||
    text: 'Mute',
 | 
			
		||||
    type: cons.notifs.MUTE,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
function setRoomNotifType(roomId, newType) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const { notifications } = initMatrix;
 | 
			
		||||
  let roomPushRule;
 | 
			
		||||
  try {
 | 
			
		||||
    roomPushRule = mx.getRoomPushRule('global', roomId);
 | 
			
		||||
| 
						 | 
				
			
			@ -47,22 +52,22 @@ function setRoomNotifType(roomId, newType) {
 | 
			
		|||
    if (roomPushRule) {
 | 
			
		||||
      promises.push(mx.deletePushRule('global', 'room', roomPushRule.rule_id));
 | 
			
		||||
    }
 | 
			
		||||
    promises.push(mx.addPushRule('global', 'override', roomId, {
 | 
			
		||||
      conditions: [
 | 
			
		||||
        {
 | 
			
		||||
          kind: 'event_match',
 | 
			
		||||
          key: 'room_id',
 | 
			
		||||
          pattern: roomId,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      actions: [
 | 
			
		||||
        'dont_notify',
 | 
			
		||||
      ],
 | 
			
		||||
    }));
 | 
			
		||||
    promises.push(
 | 
			
		||||
      mx.addPushRule('global', 'override', roomId, {
 | 
			
		||||
        conditions: [
 | 
			
		||||
          {
 | 
			
		||||
            kind: 'event_match',
 | 
			
		||||
            key: 'room_id',
 | 
			
		||||
            pattern: roomId,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        actions: ['dont_notify'],
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
    return promises;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const oldState = notifications.getNotiType(roomId);
 | 
			
		||||
  const oldState = getNotificationType(mx, roomId);
 | 
			
		||||
  if (oldState === cons.notifs.MUTE) {
 | 
			
		||||
    promises.push(mx.deletePushRule('global', 'override', roomId));
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -75,25 +80,27 @@ function setRoomNotifType(roomId, newType) {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  if (newType === cons.notifs.MENTIONS_AND_KEYWORDS) {
 | 
			
		||||
    promises.push(mx.addPushRule('global', 'room', roomId, {
 | 
			
		||||
      actions: [
 | 
			
		||||
        'dont_notify',
 | 
			
		||||
      ],
 | 
			
		||||
    }));
 | 
			
		||||
    promises.push(
 | 
			
		||||
      mx.addPushRule('global', 'room', roomId, {
 | 
			
		||||
        actions: ['dont_notify'],
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
    promises.push(mx.setPushRuleEnabled('global', 'room', roomId, true));
 | 
			
		||||
    return Promise.all(promises);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // cons.notifs.ALL_MESSAGES
 | 
			
		||||
  promises.push(mx.addPushRule('global', 'room', roomId, {
 | 
			
		||||
    actions: [
 | 
			
		||||
      'notify',
 | 
			
		||||
      {
 | 
			
		||||
        set_tweak: 'sound',
 | 
			
		||||
        value: 'default',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  }));
 | 
			
		||||
  promises.push(
 | 
			
		||||
    mx.addPushRule('global', 'room', roomId, {
 | 
			
		||||
      actions: [
 | 
			
		||||
        'notify',
 | 
			
		||||
        {
 | 
			
		||||
          set_tweak: 'sound',
 | 
			
		||||
          value: 'default',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  promises.push(mx.setPushRuleEnabled('global', 'room', roomId, true));
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -101,17 +108,20 @@ function setRoomNotifType(roomId, newType) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function useNotifications(roomId) {
 | 
			
		||||
  const { notifications } = initMatrix;
 | 
			
		||||
  const [activeType, setActiveType] = useState(notifications.getNotiType(roomId));
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const [activeType, setActiveType] = useState(getNotificationType(mx, roomId));
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setActiveType(notifications.getNotiType(roomId));
 | 
			
		||||
  }, [roomId]);
 | 
			
		||||
    setActiveType(getNotificationType(mx, roomId));
 | 
			
		||||
  }, [mx, roomId]);
 | 
			
		||||
 | 
			
		||||
  const setNotification = useCallback((item) => {
 | 
			
		||||
    if (item.type === activeType.type) return;
 | 
			
		||||
    setActiveType(item.type);
 | 
			
		||||
    setRoomNotifType(roomId, item.type);
 | 
			
		||||
  }, [activeType, roomId]);
 | 
			
		||||
  const setNotification = useCallback(
 | 
			
		||||
    (item) => {
 | 
			
		||||
      if (item.type === activeType.type) return;
 | 
			
		||||
      setActiveType(item.type);
 | 
			
		||||
      setRoomNotifType(roomId, item.type);
 | 
			
		||||
    },
 | 
			
		||||
    [activeType, roomId]
 | 
			
		||||
  );
 | 
			
		||||
  return [activeType, setNotification];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -120,21 +130,19 @@ function RoomNotification({ roomId }) {
 | 
			
		|||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="room-notification">
 | 
			
		||||
      {
 | 
			
		||||
        items.map((item) => (
 | 
			
		||||
          <MenuItem
 | 
			
		||||
            variant={activeType === item.type ? 'positive' : 'surface'}
 | 
			
		||||
            key={item.type}
 | 
			
		||||
            iconSrc={item.iconSrc}
 | 
			
		||||
            onClick={() => setNotification(item)}
 | 
			
		||||
          >
 | 
			
		||||
            <Text varient="b1">
 | 
			
		||||
              <span>{item.text}</span>
 | 
			
		||||
              <RadioButton isActive={activeType === item.type} />
 | 
			
		||||
            </Text>
 | 
			
		||||
          </MenuItem>
 | 
			
		||||
        ))
 | 
			
		||||
      }
 | 
			
		||||
      {items.map((item) => (
 | 
			
		||||
        <MenuItem
 | 
			
		||||
          variant={activeType === item.type ? 'positive' : 'surface'}
 | 
			
		||||
          key={item.type}
 | 
			
		||||
          iconSrc={item.iconSrc}
 | 
			
		||||
          onClick={() => setNotification(item)}
 | 
			
		||||
        >
 | 
			
		||||
          <Text varient="b1">
 | 
			
		||||
            <span>{item.text}</span>
 | 
			
		||||
            <RadioButton isActive={activeType === item.type} />
 | 
			
		||||
          </Text>
 | 
			
		||||
        </MenuItem>
 | 
			
		||||
      ))}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,73 +0,0 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import { openInviteUser } from '../../../client/action/navigation';
 | 
			
		||||
import * as roomActions from '../../../client/action/room';
 | 
			
		||||
import { markAsRead } from '../../../client/action/notifications';
 | 
			
		||||
 | 
			
		||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
 | 
			
		||||
import RoomNotification from '../room-notification/RoomNotification';
 | 
			
		||||
 | 
			
		||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
 | 
			
		||||
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
 | 
			
		||||
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
 | 
			
		||||
 | 
			
		||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
 | 
			
		||||
 | 
			
		||||
function RoomOptions({ roomId, afterOptionSelect }) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const room = mx.getRoom(roomId);
 | 
			
		||||
  const canInvite = room?.canInvite(mx.getUserId());
 | 
			
		||||
 | 
			
		||||
  const handleMarkAsRead = () => {
 | 
			
		||||
    markAsRead(roomId);
 | 
			
		||||
    afterOptionSelect();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleInviteClick = () => {
 | 
			
		||||
    openInviteUser(roomId);
 | 
			
		||||
    afterOptionSelect();
 | 
			
		||||
  };
 | 
			
		||||
  const handleLeaveClick = async () => {
 | 
			
		||||
    afterOptionSelect();
 | 
			
		||||
    const isConfirmed = await confirmDialog(
 | 
			
		||||
      'Leave room',
 | 
			
		||||
      `Are you sure that you want to leave "${room.name}" room?`,
 | 
			
		||||
      'Leave',
 | 
			
		||||
      'danger',
 | 
			
		||||
    );
 | 
			
		||||
    if (!isConfirmed) return;
 | 
			
		||||
    roomActions.leave(roomId);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div style={{ maxWidth: '256px' }}>
 | 
			
		||||
      <MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
 | 
			
		||||
      <MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
 | 
			
		||||
      <MenuItem
 | 
			
		||||
        iconSrc={AddUserIC}
 | 
			
		||||
        onClick={handleInviteClick}
 | 
			
		||||
        disabled={!canInvite}
 | 
			
		||||
      >
 | 
			
		||||
        Invite
 | 
			
		||||
      </MenuItem>
 | 
			
		||||
      <MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={handleLeaveClick}>Leave</MenuItem>
 | 
			
		||||
      <MenuHeader>Notification</MenuHeader>
 | 
			
		||||
      <RoomNotification roomId={roomId} />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RoomOptions.defaultProps = {
 | 
			
		||||
  afterOptionSelect: null,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
RoomOptions.propTypes = {
 | 
			
		||||
  roomId: PropTypes.string.isRequired,
 | 
			
		||||
  afterOptionSelect: PropTypes.func,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default RoomOptions;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { useAtomValue } from 'jotai';
 | 
			
		||||
import Linkify from 'linkify-react';
 | 
			
		||||
import './RoomProfile.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import colorMXID from '../../../util/colorMXID';
 | 
			
		||||
| 
						 | 
				
			
			@ -20,6 +20,8 @@ import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
 | 
			
		|||
import { useStore } from '../../hooks/useStore';
 | 
			
		||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
 | 
			
		||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
 | 
			
		||||
import { mDirectAtom } from '../../state/mDirectList';
 | 
			
		||||
import { LINKIFY_OPTS } from '../../plugins/react-custom-html-parser';
 | 
			
		||||
 | 
			
		||||
function RoomProfile({ roomId }) {
 | 
			
		||||
  const isMountStore = useStore();
 | 
			
		||||
| 
						 | 
				
			
			@ -31,9 +33,12 @@ function RoomProfile({ roomId }) {
 | 
			
		|||
  });
 | 
			
		||||
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const isDM = initMatrix.roomList.directs.has(roomId);
 | 
			
		||||
  const mDirects = useAtomValue(mDirectAtom);
 | 
			
		||||
  const isDM = mDirects.has(roomId);
 | 
			
		||||
  let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
 | 
			
		||||
  avatarSrc = isDM ? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc;
 | 
			
		||||
  avatarSrc = isDM
 | 
			
		||||
    ? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop')
 | 
			
		||||
    : avatarSrc;
 | 
			
		||||
  const room = mx.getRoom(roomId);
 | 
			
		||||
  const { currentState } = room;
 | 
			
		||||
  const roomName = room.name;
 | 
			
		||||
| 
						 | 
				
			
			@ -47,15 +52,14 @@ function RoomProfile({ roomId }) {
 | 
			
		|||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    isMountStore.setItem(true);
 | 
			
		||||
    const { roomList } = initMatrix;
 | 
			
		||||
    const handleProfileUpdate = (rId) => {
 | 
			
		||||
      if (roomId !== rId) return;
 | 
			
		||||
    const handleStateEvent = (mEvent) => {
 | 
			
		||||
      if (mEvent.event.room_id !== roomId) return;
 | 
			
		||||
      forceUpdate();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    roomList.on(cons.events.roomList.ROOM_PROFILE_UPDATED, handleProfileUpdate);
 | 
			
		||||
    mx.on('RoomState.events', handleStateEvent);
 | 
			
		||||
    return () => {
 | 
			
		||||
      roomList.removeListener(cons.events.roomList.ROOM_PROFILE_UPDATED, handleProfileUpdate);
 | 
			
		||||
      mx.removeListener('RoomState.events', handleStateEvent);
 | 
			
		||||
      isMountStore.setItem(false);
 | 
			
		||||
      setStatus({
 | 
			
		||||
        msg: null,
 | 
			
		||||
| 
						 | 
				
			
			@ -122,7 +126,7 @@ function RoomProfile({ roomId }) {
 | 
			
		|||
        'Remove avatar',
 | 
			
		||||
        'Are you sure that you want to remove room avatar?',
 | 
			
		||||
        'Remove',
 | 
			
		||||
        'caution',
 | 
			
		||||
        'caution'
 | 
			
		||||
      );
 | 
			
		||||
      if (isConfirmed) {
 | 
			
		||||
        await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
 | 
			
		||||
| 
						 | 
				
			
			@ -132,15 +136,45 @@ function RoomProfile({ roomId }) {
 | 
			
		|||
 | 
			
		||||
  const renderEditNameAndTopic = () => (
 | 
			
		||||
    <form className="room-profile__edit-form" onSubmit={handleOnSubmit}>
 | 
			
		||||
      {canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label="Name" />}
 | 
			
		||||
      {canChangeTopic && <Input value={roomTopic} name="room-topic" disabled={status.type === cons.status.IN_FLIGHT} minHeight={100} resizable label="Topic" />}
 | 
			
		||||
      {(!canChangeName || !canChangeTopic) && <Text variant="b3">{`You have permission to change ${room.isSpaceRoom() ? 'space' : 'room'} ${canChangeName ? 'name' : 'topic'} only.`}</Text>}
 | 
			
		||||
      { status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
 | 
			
		||||
      { status.type === cons.status.SUCCESS && <Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">{status.msg}</Text>}
 | 
			
		||||
      { status.type === cons.status.ERROR && <Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">{status.msg}</Text>}
 | 
			
		||||
      { status.type !== cons.status.IN_FLIGHT && (
 | 
			
		||||
      {canChangeName && (
 | 
			
		||||
        <Input
 | 
			
		||||
          value={roomName}
 | 
			
		||||
          name="room-name"
 | 
			
		||||
          disabled={status.type === cons.status.IN_FLIGHT}
 | 
			
		||||
          label="Name"
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {canChangeTopic && (
 | 
			
		||||
        <Input
 | 
			
		||||
          value={roomTopic}
 | 
			
		||||
          name="room-topic"
 | 
			
		||||
          disabled={status.type === cons.status.IN_FLIGHT}
 | 
			
		||||
          minHeight={100}
 | 
			
		||||
          resizable
 | 
			
		||||
          label="Topic"
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {(!canChangeName || !canChangeTopic) && (
 | 
			
		||||
        <Text variant="b3">{`You have permission to change ${
 | 
			
		||||
          room.isSpaceRoom() ? 'space' : 'room'
 | 
			
		||||
        } ${canChangeName ? 'name' : 'topic'} only.`}</Text>
 | 
			
		||||
      )}
 | 
			
		||||
      {status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
 | 
			
		||||
      {status.type === cons.status.SUCCESS && (
 | 
			
		||||
        <Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">
 | 
			
		||||
          {status.msg}
 | 
			
		||||
        </Text>
 | 
			
		||||
      )}
 | 
			
		||||
      {status.type === cons.status.ERROR && (
 | 
			
		||||
        <Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">
 | 
			
		||||
          {status.msg}
 | 
			
		||||
        </Text>
 | 
			
		||||
      )}
 | 
			
		||||
      {status.type !== cons.status.IN_FLIGHT && (
 | 
			
		||||
        <div>
 | 
			
		||||
          <Button type="submit" variant="primary">Save</Button>
 | 
			
		||||
          <Button type="submit" variant="primary">
 | 
			
		||||
            Save
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button onClick={handleCancelEditing}>Cancel</Button>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
| 
						 | 
				
			
			@ -148,10 +182,15 @@ function RoomProfile({ roomId }) {
 | 
			
		|||
  );
 | 
			
		||||
 | 
			
		||||
  const renderNameAndTopic = () => (
 | 
			
		||||
    <div className="room-profile__display" style={{ marginBottom: avatarSrc && canChangeAvatar ? '24px' : '0' }}>
 | 
			
		||||
    <div
 | 
			
		||||
      className="room-profile__display"
 | 
			
		||||
      style={{ marginBottom: avatarSrc && canChangeAvatar ? '24px' : '0' }}
 | 
			
		||||
    >
 | 
			
		||||
      <div>
 | 
			
		||||
        <Text variant="h2" weight="medium" primary>{twemojify(roomName)}</Text>
 | 
			
		||||
        { (canChangeName || canChangeTopic) && (
 | 
			
		||||
        <Text variant="h2" weight="medium" primary>
 | 
			
		||||
          {roomName}
 | 
			
		||||
        </Text>
 | 
			
		||||
        {(canChangeName || canChangeTopic) && (
 | 
			
		||||
          <IconButton
 | 
			
		||||
            src={PencilIC}
 | 
			
		||||
            size="extra-small"
 | 
			
		||||
| 
						 | 
				
			
			@ -161,15 +200,21 @@ function RoomProfile({ roomId }) {
 | 
			
		|||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
      <Text variant="b3">{room.getCanonicalAlias() || room.roomId}</Text>
 | 
			
		||||
      {roomTopic && <Text variant="b2">{twemojify(roomTopic, undefined, true)}</Text>}
 | 
			
		||||
      {roomTopic && (
 | 
			
		||||
        <Text variant="b2">
 | 
			
		||||
          <Linkify options={LINKIFY_OPTS}>{roomTopic}</Linkify>
 | 
			
		||||
        </Text>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="room-profile">
 | 
			
		||||
      <div className="room-profile__content">
 | 
			
		||||
        { !canChangeAvatar && <Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="large" />}
 | 
			
		||||
        { canChangeAvatar && (
 | 
			
		||||
        {!canChangeAvatar && (
 | 
			
		||||
          <Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="large" />
 | 
			
		||||
        )}
 | 
			
		||||
        {canChangeAvatar && (
 | 
			
		||||
          <ImageUpload
 | 
			
		||||
            text={roomName}
 | 
			
		||||
            bgColor={colorMXID(roomId)}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,201 +0,0 @@
 | 
			
		|||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './RoomSearch.scss';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import { selectRoom } from '../../../client/action/navigation';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import RawIcon from '../../atoms/system-icons/RawIcon';
 | 
			
		||||
import Button from '../../atoms/button/Button';
 | 
			
		||||
import Input from '../../atoms/input/Input';
 | 
			
		||||
import Spinner from '../../atoms/spinner/Spinner';
 | 
			
		||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
 | 
			
		||||
import { Message } from '../message/Message';
 | 
			
		||||
 | 
			
		||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
 | 
			
		||||
 | 
			
		||||
import { useStore } from '../../hooks/useStore';
 | 
			
		||||
 | 
			
		||||
const roomIdToBackup = new Map();
 | 
			
		||||
 | 
			
		||||
function useRoomSearch(roomId) {
 | 
			
		||||
  const [searchData, setSearchData] = useState(roomIdToBackup.get(roomId) ?? null);
 | 
			
		||||
  const [status, setStatus] = useState({
 | 
			
		||||
    type: cons.status.PRE_FLIGHT,
 | 
			
		||||
    term: null,
 | 
			
		||||
  });
 | 
			
		||||
  const mountStore = useStore(roomId);
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    mountStore.setItem(true)
 | 
			
		||||
  }, [roomId]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (searchData?.results?.length > 0) {
 | 
			
		||||
      roomIdToBackup.set(roomId, searchData);
 | 
			
		||||
    } else {
 | 
			
		||||
      roomIdToBackup.delete(roomId);
 | 
			
		||||
    }
 | 
			
		||||
  }, [searchData]);
 | 
			
		||||
 | 
			
		||||
  const search = async (term) => {
 | 
			
		||||
    setSearchData(null);
 | 
			
		||||
    if (term === '') {
 | 
			
		||||
      setStatus({ type: cons.status.PRE_FLIGHT, term: null });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setStatus({ type: cons.status.IN_FLIGHT, term });
 | 
			
		||||
    const body = {
 | 
			
		||||
      search_categories: {
 | 
			
		||||
        room_events: {
 | 
			
		||||
          search_term: term,
 | 
			
		||||
          filter: {
 | 
			
		||||
            limit: 10,
 | 
			
		||||
            rooms: [roomId],
 | 
			
		||||
          },
 | 
			
		||||
          order_by: 'recent',
 | 
			
		||||
          event_context: {
 | 
			
		||||
            before_limit: 0,
 | 
			
		||||
            after_limit: 0,
 | 
			
		||||
            include_profile: true,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await mx.search({ body });
 | 
			
		||||
      const data = mx.processRoomEventsSearch({
 | 
			
		||||
        _query: body,
 | 
			
		||||
        results: [],
 | 
			
		||||
        highlights: [],
 | 
			
		||||
      }, res);
 | 
			
		||||
      if (!mountStore.getItem()) return;
 | 
			
		||||
      setStatus({ type: cons.status.SUCCESS, term });
 | 
			
		||||
      setSearchData(data);
 | 
			
		||||
      if (!mountStore.getItem()) return;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      setSearchData(null);
 | 
			
		||||
      setStatus({ type: cons.status.ERROR, term });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const paginate = async () => {
 | 
			
		||||
    if (searchData === null) return;
 | 
			
		||||
    const term = searchData._query.search_categories.room_events.search_term;
 | 
			
		||||
 | 
			
		||||
    setStatus({ type: cons.status.IN_FLIGHT, term });
 | 
			
		||||
    try {
 | 
			
		||||
      const data = await mx.backPaginateRoomEventsSearch(searchData);
 | 
			
		||||
      if (!mountStore.getItem()) return;
 | 
			
		||||
      setStatus({ type: cons.status.SUCCESS, term });
 | 
			
		||||
      setSearchData(data);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      if (!mountStore.getItem()) return;
 | 
			
		||||
      setSearchData(null);
 | 
			
		||||
      setStatus({ type: cons.status.ERROR, term });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return [searchData, search, paginate, status];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function RoomSearch({ roomId }) {
 | 
			
		||||
  const [searchData, search, paginate, status] = useRoomSearch(roomId);
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const isRoomEncrypted = mx.isRoomEncrypted(roomId);
 | 
			
		||||
  const searchTerm = searchData?._query.search_categories.room_events.search_term ?? '';
 | 
			
		||||
 | 
			
		||||
  const handleSearch = (e) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    if (isRoomEncrypted) return;
 | 
			
		||||
    const searchTermInput = e.target.elements['room-search-input'];
 | 
			
		||||
    const term = searchTermInput.value.trim();
 | 
			
		||||
 | 
			
		||||
    search(term);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const renderTimeline = (timeline) => (
 | 
			
		||||
    <div className="room-search__result-item" key={timeline[0].getId()}>
 | 
			
		||||
      { timeline.map((mEvent) => {
 | 
			
		||||
        const id = mEvent.getId();
 | 
			
		||||
        return (
 | 
			
		||||
          <React.Fragment key={id}>
 | 
			
		||||
            <Message
 | 
			
		||||
              mEvent={mEvent}
 | 
			
		||||
              isBodyOnly={false}
 | 
			
		||||
              fullTime
 | 
			
		||||
            />
 | 
			
		||||
            <Button onClick={() => selectRoom(roomId, id)}>View</Button>
 | 
			
		||||
          </React.Fragment>
 | 
			
		||||
        );
 | 
			
		||||
      })}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="room-search">
 | 
			
		||||
      <form className="room-search__form" onSubmit={handleSearch}>
 | 
			
		||||
        <MenuHeader>Room search</MenuHeader>
 | 
			
		||||
        <div>
 | 
			
		||||
          <Input
 | 
			
		||||
            placeholder="Search for keywords"
 | 
			
		||||
            name="room-search-input"
 | 
			
		||||
            disabled={isRoomEncrypted}
 | 
			
		||||
            autoFocus
 | 
			
		||||
          />
 | 
			
		||||
          <Button iconSrc={SearchIC} variant="primary" type="submit">Search</Button>
 | 
			
		||||
        </div>
 | 
			
		||||
        {searchData?.results.length > 0 && (
 | 
			
		||||
          <Text>{`${searchData.count} results for "${searchTerm}"`}</Text>
 | 
			
		||||
        )}
 | 
			
		||||
        {!isRoomEncrypted && searchData === null && (
 | 
			
		||||
          <div className="room-search__help">
 | 
			
		||||
            {status.type === cons.status.IN_FLIGHT && <Spinner />}
 | 
			
		||||
            {status.type === cons.status.IN_FLIGHT && <Text>Searching room messages...</Text>}
 | 
			
		||||
            {status.type === cons.status.PRE_FLIGHT && <RawIcon src={SearchIC} size="large" />}
 | 
			
		||||
            {status.type === cons.status.PRE_FLIGHT && <Text>Search room messages</Text>}
 | 
			
		||||
            {status.type === cons.status.ERROR && <Text>Failed to search messages</Text>}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {!isRoomEncrypted && searchData?.results.length === 0 && (
 | 
			
		||||
          <div className="room-search__help">
 | 
			
		||||
            <Text>No results found</Text>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        {isRoomEncrypted && (
 | 
			
		||||
          <div className="room-search__help">
 | 
			
		||||
            <Text>Search does not work in encrypted room</Text>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </form>
 | 
			
		||||
      {searchData?.results.length > 0 && (
 | 
			
		||||
        <>
 | 
			
		||||
          <div className="room-search__content">
 | 
			
		||||
            {searchData.results.map((searchResult) => {
 | 
			
		||||
              const { timeline } = searchResult.context;
 | 
			
		||||
              return renderTimeline(timeline);
 | 
			
		||||
            })}
 | 
			
		||||
          </div>
 | 
			
		||||
          {searchData?.next_batch && (
 | 
			
		||||
            <div className="room-search__more">
 | 
			
		||||
              {status.type !== cons.status.IN_FLIGHT && (
 | 
			
		||||
                <Button onClick={paginate}>Load more</Button>
 | 
			
		||||
              )}
 | 
			
		||||
              {status.type === cons.status.IN_FLIGHT && <Spinner />}
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RoomSearch.propTypes = {
 | 
			
		||||
  roomId: PropTypes.string.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default RoomSearch;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,62 +0,0 @@
 | 
			
		|||
@use '../../partials/flex';
 | 
			
		||||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.room-search {
 | 
			
		||||
  &__form {
 | 
			
		||||
    & div:nth-child(2) {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: flex-end;
 | 
			
		||||
      padding: var(--sp-normal);;
 | 
			
		||||
      
 | 
			
		||||
      & .input-container {
 | 
			
		||||
        @extend .cp-fx__item-one;
 | 
			
		||||
        @include dir.side(margin, 0, var(--sp-normal));
 | 
			
		||||
      }
 | 
			
		||||
      & button {
 | 
			
		||||
        height: 46px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    & .context-menu__header {
 | 
			
		||||
      margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
    & > .text {
 | 
			
		||||
      padding: 0 var(--sp-normal) var(--sp-tight);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__help {
 | 
			
		||||
    height: 248px;
 | 
			
		||||
    @extend .cp-fx__column--c-c;
 | 
			
		||||
 | 
			
		||||
    & .ic-raw {
 | 
			
		||||
      opacity: .5;
 | 
			
		||||
    }
 | 
			
		||||
    .text {
 | 
			
		||||
      margin-top: var(--sp-normal);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &__more {
 | 
			
		||||
    margin-bottom: var(--sp-normal);
 | 
			
		||||
    @extend .cp-fx__row--c-c;
 | 
			
		||||
    button {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &__result-item {
 | 
			
		||||
    padding: var(--sp-tight) var(--sp-normal);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: flex-start;
 | 
			
		||||
 | 
			
		||||
    .message {
 | 
			
		||||
      @include dir.side(margin, 0, var(--sp-normal));
 | 
			
		||||
      @extend .cp-fx__item-one;
 | 
			
		||||
      padding: 0;
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background-color: transparent;
 | 
			
		||||
      }
 | 
			
		||||
      & .message__time {
 | 
			
		||||
        flex: 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,7 +2,6 @@ import React from 'react';
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import './RoomSelector.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
import colorMXID from '../../../util/colorMXID';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
| 
						 | 
				
			
			@ -11,8 +10,13 @@ import NotificationBadge from '../../atoms/badge/NotificationBadge';
 | 
			
		|||
import { blurOnBubbling } from '../../atoms/button/script';
 | 
			
		||||
 | 
			
		||||
function RoomSelectorWrapper({
 | 
			
		||||
  isSelected, isMuted, isUnread, onClick,
 | 
			
		||||
  content, options, onContextMenu,
 | 
			
		||||
  isSelected,
 | 
			
		||||
  isMuted,
 | 
			
		||||
  isUnread,
 | 
			
		||||
  onClick,
 | 
			
		||||
  content,
 | 
			
		||||
  options,
 | 
			
		||||
  onContextMenu,
 | 
			
		||||
}) {
 | 
			
		||||
  const classes = ['room-selector'];
 | 
			
		||||
  if (isMuted) classes.push('room-selector--muted');
 | 
			
		||||
| 
						 | 
				
			
			@ -50,16 +54,26 @@ RoomSelectorWrapper.propTypes = {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
function RoomSelector({
 | 
			
		||||
  name, parentName, roomId, imageSrc, iconSrc,
 | 
			
		||||
  isSelected, isMuted, isUnread, notificationCount, isAlert,
 | 
			
		||||
  options, onClick, onContextMenu,
 | 
			
		||||
  name,
 | 
			
		||||
  parentName,
 | 
			
		||||
  roomId,
 | 
			
		||||
  imageSrc,
 | 
			
		||||
  iconSrc,
 | 
			
		||||
  isSelected,
 | 
			
		||||
  isMuted,
 | 
			
		||||
  isUnread,
 | 
			
		||||
  notificationCount,
 | 
			
		||||
  isAlert,
 | 
			
		||||
  options,
 | 
			
		||||
  onClick,
 | 
			
		||||
  onContextMenu,
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <RoomSelectorWrapper
 | 
			
		||||
      isSelected={isSelected}
 | 
			
		||||
      isMuted={isMuted}
 | 
			
		||||
      isUnread={isUnread}
 | 
			
		||||
      content={(
 | 
			
		||||
      content={
 | 
			
		||||
        <>
 | 
			
		||||
          <Avatar
 | 
			
		||||
            text={name}
 | 
			
		||||
| 
						 | 
				
			
			@ -70,22 +84,22 @@ function RoomSelector({
 | 
			
		|||
            size="extra-small"
 | 
			
		||||
          />
 | 
			
		||||
          <Text variant="b1" weight={isUnread ? 'medium' : 'normal'}>
 | 
			
		||||
            {twemojify(name)}
 | 
			
		||||
            {name}
 | 
			
		||||
            {parentName && (
 | 
			
		||||
              <Text variant="b3" span>
 | 
			
		||||
                {' — '}
 | 
			
		||||
                {twemojify(parentName)}
 | 
			
		||||
                {parentName}
 | 
			
		||||
              </Text>
 | 
			
		||||
            )}
 | 
			
		||||
          </Text>
 | 
			
		||||
          { isUnread && (
 | 
			
		||||
          {isUnread && (
 | 
			
		||||
            <NotificationBadge
 | 
			
		||||
              alert={isAlert}
 | 
			
		||||
              content={notificationCount !== 0 ? notificationCount : null}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
      }
 | 
			
		||||
      options={options}
 | 
			
		||||
      onClick={onClick}
 | 
			
		||||
      onContextMenu={onContextMenu}
 | 
			
		||||
| 
						 | 
				
			
			@ -110,10 +124,7 @@ RoomSelector.propTypes = {
 | 
			
		|||
  isSelected: PropTypes.bool,
 | 
			
		||||
  isMuted: PropTypes.bool,
 | 
			
		||||
  isUnread: PropTypes.bool.isRequired,
 | 
			
		||||
  notificationCount: PropTypes.oneOfType([
 | 
			
		||||
    PropTypes.string,
 | 
			
		||||
    PropTypes.number,
 | 
			
		||||
  ]).isRequired,
 | 
			
		||||
  notificationCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
 | 
			
		||||
  isAlert: PropTypes.bool.isRequired,
 | 
			
		||||
  options: PropTypes.node,
 | 
			
		||||
  onClick: PropTypes.func.isRequired,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,46 +2,35 @@ import React from 'react';
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import './RoomTile.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import colorMXID from '../../../util/colorMXID';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import Avatar from '../../atoms/avatar/Avatar';
 | 
			
		||||
 | 
			
		||||
function RoomTile({
 | 
			
		||||
  avatarSrc, name, id,
 | 
			
		||||
  inviterName, memberCount, desc, options,
 | 
			
		||||
}) {
 | 
			
		||||
function RoomTile({ avatarSrc, name, id, inviterName, memberCount, desc, options }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="room-tile">
 | 
			
		||||
      <div className="room-tile__avatar">
 | 
			
		||||
        <Avatar
 | 
			
		||||
          imageSrc={avatarSrc}
 | 
			
		||||
          bgColor={colorMXID(id)}
 | 
			
		||||
          text={name}
 | 
			
		||||
        />
 | 
			
		||||
        <Avatar imageSrc={avatarSrc} bgColor={colorMXID(id)} text={name} />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="room-tile__content">
 | 
			
		||||
        <Text variant="s1">{twemojify(name)}</Text>
 | 
			
		||||
        <Text variant="s1">{name}</Text>
 | 
			
		||||
        <Text variant="b3">
 | 
			
		||||
          {
 | 
			
		||||
            inviterName !== null
 | 
			
		||||
              ? `Invited by ${inviterName} to ${id}${memberCount === null ? '' : ` • ${memberCount} members`}`
 | 
			
		||||
              : id + (memberCount === null ? '' : ` • ${memberCount} members`)
 | 
			
		||||
          }
 | 
			
		||||
          {inviterName !== null
 | 
			
		||||
            ? `Invited by ${inviterName} to ${id}${
 | 
			
		||||
                memberCount === null ? '' : ` • ${memberCount} members`
 | 
			
		||||
              }`
 | 
			
		||||
            : id + (memberCount === null ? '' : ` • ${memberCount} members`)}
 | 
			
		||||
        </Text>
 | 
			
		||||
        {
 | 
			
		||||
          desc !== null && (typeof desc === 'string')
 | 
			
		||||
            ? <Text className="room-tile__content__desc" variant="b2">{twemojify(desc, undefined, true)}</Text>
 | 
			
		||||
            : desc
 | 
			
		||||
        }
 | 
			
		||||
        {desc !== null && typeof desc === 'string' ? (
 | 
			
		||||
          <Text className="room-tile__content__desc" variant="b2">
 | 
			
		||||
            {desc}
 | 
			
		||||
          </Text>
 | 
			
		||||
        ) : (
 | 
			
		||||
          desc
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
      { options !== null && (
 | 
			
		||||
        <div className="room-tile__options">
 | 
			
		||||
          {options}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      {options !== null && <div className="room-tile__options">{options}</div>}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -58,10 +47,7 @@ RoomTile.propTypes = {
 | 
			
		|||
  name: PropTypes.string.isRequired,
 | 
			
		||||
  id: PropTypes.string.isRequired,
 | 
			
		||||
  inviterName: PropTypes.string,
 | 
			
		||||
  memberCount: PropTypes.oneOfType([
 | 
			
		||||
    PropTypes.string,
 | 
			
		||||
    PropTypes.number,
 | 
			
		||||
  ]),
 | 
			
		||||
  memberCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
 | 
			
		||||
  desc: PropTypes.node,
 | 
			
		||||
  options: PropTypes.node,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,55 +0,0 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './SidebarAvatar.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import Tooltip from '../../atoms/tooltip/Tooltip';
 | 
			
		||||
import { blurOnBubbling } from '../../atoms/button/script';
 | 
			
		||||
 | 
			
		||||
const SidebarAvatar = React.forwardRef(({
 | 
			
		||||
  className, tooltip, active, onClick,
 | 
			
		||||
  onContextMenu, avatar, notificationBadge,
 | 
			
		||||
}, ref) => {
 | 
			
		||||
  const classes = ['sidebar-avatar'];
 | 
			
		||||
  if (active) classes.push('sidebar-avatar--active');
 | 
			
		||||
  if (className) classes.push(className);
 | 
			
		||||
  return (
 | 
			
		||||
    <Tooltip
 | 
			
		||||
      content={<Text variant="b1">{twemojify(tooltip)}</Text>}
 | 
			
		||||
      placement="right"
 | 
			
		||||
    >
 | 
			
		||||
      <button
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        className={classes.join(' ')}
 | 
			
		||||
        type="button"
 | 
			
		||||
        onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
 | 
			
		||||
        onClick={onClick}
 | 
			
		||||
        onContextMenu={onContextMenu}
 | 
			
		||||
      >
 | 
			
		||||
        {avatar}
 | 
			
		||||
        {notificationBadge}
 | 
			
		||||
      </button>
 | 
			
		||||
    </Tooltip>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
SidebarAvatar.defaultProps = {
 | 
			
		||||
  className: null,
 | 
			
		||||
  active: false,
 | 
			
		||||
  onClick: null,
 | 
			
		||||
  onContextMenu: null,
 | 
			
		||||
  notificationBadge: null,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SidebarAvatar.propTypes = {
 | 
			
		||||
  className: PropTypes.string,
 | 
			
		||||
  tooltip: PropTypes.string.isRequired,
 | 
			
		||||
  active: PropTypes.bool,
 | 
			
		||||
  onClick: PropTypes.func,
 | 
			
		||||
  onContextMenu: PropTypes.func,
 | 
			
		||||
  avatar: PropTypes.node.isRequired,
 | 
			
		||||
  notificationBadge: PropTypes.node,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SidebarAvatar;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,64 +0,0 @@
 | 
			
		|||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.sidebar-avatar {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  
 | 
			
		||||
  & .notification-badge {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    @include dir.prop(left, unset, 0);
 | 
			
		||||
    @include dir.prop(right, 0, unset);
 | 
			
		||||
    top: 0;
 | 
			
		||||
    box-shadow: 0 0 0 2px var(--bg-surface-low);
 | 
			
		||||
    @include dir.prop(transform, translate(20%, -20%), translate(-20%, -20%));
 | 
			
		||||
 | 
			
		||||
    margin: 0 !important;
 | 
			
		||||
  }
 | 
			
		||||
  & .avatar-container,
 | 
			
		||||
  & .notification-badge {
 | 
			
		||||
    transition: transform 200ms var(--fluid-push);
 | 
			
		||||
  }
 | 
			
		||||
  &:hover .avatar-container {
 | 
			
		||||
    @include dir.prop(transform, translateX(4px), translateX(-4px));
 | 
			
		||||
  }
 | 
			
		||||
  &:hover .notification-badge {
 | 
			
		||||
    --ltr: translate(calc(20% + 4px), -20%);
 | 
			
		||||
    --rtl: translate(calc(-20% - 4px), -20%);
 | 
			
		||||
    @include dir.prop(transform, var(--ltr), var(--rtl));
 | 
			
		||||
  }
 | 
			
		||||
  &:focus {
 | 
			
		||||
    outline: none;
 | 
			
		||||
  }
 | 
			
		||||
  &:active .avatar-container {
 | 
			
		||||
    box-shadow: var(--bs-surface-outline);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:hover::before,
 | 
			
		||||
  &:focus::before,
 | 
			
		||||
  &--active::before {
 | 
			
		||||
    content: "";
 | 
			
		||||
    display: block;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    @include dir.prop(left, -11px, unset);
 | 
			
		||||
    @include dir.prop(right, unset, -11px);
 | 
			
		||||
    top: 50%;
 | 
			
		||||
    transform: translateY(-50%);
 | 
			
		||||
 | 
			
		||||
    width: 3px;
 | 
			
		||||
    height: 12px;
 | 
			
		||||
    background-color: var(--tc-surface-high);
 | 
			
		||||
    @include dir.prop(border-radius, 0 4px 4px 0, 4px 0 0 4px);
 | 
			
		||||
    transition: height 200ms linear;
 | 
			
		||||
  }
 | 
			
		||||
  &--active:hover::before,
 | 
			
		||||
  &--active:focus::before,
 | 
			
		||||
  &--active::before {
 | 
			
		||||
    height: 28px;
 | 
			
		||||
  }
 | 
			
		||||
  &--active .avatar-container {
 | 
			
		||||
    background-color: var(--bg-surface);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,8 @@
 | 
			
		|||
import React, { useState, useEffect, useCallback } from 'react';
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { useAtomValue } from 'jotai';
 | 
			
		||||
import './SpaceAddExisting.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +23,9 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 | 
			
		|||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
 | 
			
		||||
 | 
			
		||||
import { useStore } from '../../hooks/useStore';
 | 
			
		||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
 | 
			
		||||
import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList';
 | 
			
		||||
import { allRoomsAtom } from '../../state/room-list/roomList';
 | 
			
		||||
 | 
			
		||||
function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
 | 
			
		||||
  const mountStore = useStore(roomId);
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +35,10 @@ function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
 | 
			
		|||
  const [selected, setSelected] = useState([]);
 | 
			
		||||
  const [searchIds, setSearchIds] = useState(null);
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const { spaces, rooms, directs, roomIdToParents } = initMatrix.roomList;
 | 
			
		||||
  const roomIdToParents = useAtomValue(roomToParentsAtom);
 | 
			
		||||
  const spaces = useSpaces(mx, allRoomsAtom);
 | 
			
		||||
  const rooms = useRooms(mx, allRoomsAtom);
 | 
			
		||||
  const directs = useDirects(mx, allRoomsAtom);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const roomIds = onlySpaces ? [...spaces] : [...rooms, ...directs];
 | 
			
		||||
| 
						 | 
				
			
			@ -217,7 +222,7 @@ function SpaceAddExisting() {
 | 
			
		|||
      className="space-add-existing"
 | 
			
		||||
      title={
 | 
			
		||||
        <Text variant="s1" weight="medium" primary>
 | 
			
		||||
          {room && twemojify(room.name)}
 | 
			
		||||
          {room && room.name}
 | 
			
		||||
          <span style={{ color: 'var(--tc-surface-low)' }}>
 | 
			
		||||
            {' '}
 | 
			
		||||
            — add existing {data?.spaces ? 'spaces' : 'rooms'}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,128 +0,0 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import { openSpaceSettings, openSpaceManage, openInviteUser } from '../../../client/action/navigation';
 | 
			
		||||
import { markAsRead } from '../../../client/action/notifications';
 | 
			
		||||
import { leave } from '../../../client/action/room';
 | 
			
		||||
import {
 | 
			
		||||
  createSpaceShortcut,
 | 
			
		||||
  deleteSpaceShortcut,
 | 
			
		||||
  categorizeSpace,
 | 
			
		||||
  unCategorizeSpace,
 | 
			
		||||
} from '../../../client/action/accountData';
 | 
			
		||||
 | 
			
		||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
 | 
			
		||||
 | 
			
		||||
import CategoryIC from '../../../../public/res/ic/outlined/category.svg';
 | 
			
		||||
import CategoryFilledIC from '../../../../public/res/ic/filled/category.svg';
 | 
			
		||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
 | 
			
		||||
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
 | 
			
		||||
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
 | 
			
		||||
import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg';
 | 
			
		||||
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
 | 
			
		||||
import PinIC from '../../../../public/res/ic/outlined/pin.svg';
 | 
			
		||||
import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
 | 
			
		||||
 | 
			
		||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
 | 
			
		||||
 | 
			
		||||
function SpaceOptions({ roomId, afterOptionSelect }) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const { roomList } = initMatrix;
 | 
			
		||||
  const room = mx.getRoom(roomId);
 | 
			
		||||
  const canInvite = room?.canInvite(mx.getUserId());
 | 
			
		||||
  const isPinned = initMatrix.accountData.spaceShortcut.has(roomId);
 | 
			
		||||
  const isCategorized = initMatrix.accountData.categorizedSpaces.has(roomId);
 | 
			
		||||
 | 
			
		||||
  const handleMarkAsRead = () => {
 | 
			
		||||
    const spaceChildren = roomList.getCategorizedSpaces([roomId]);
 | 
			
		||||
    spaceChildren?.forEach((childIds) => {
 | 
			
		||||
      childIds?.forEach((childId) => {
 | 
			
		||||
        markAsRead(childId);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    afterOptionSelect();
 | 
			
		||||
  };
 | 
			
		||||
  const handleInviteClick = () => {
 | 
			
		||||
    openInviteUser(roomId);
 | 
			
		||||
    afterOptionSelect();
 | 
			
		||||
  };
 | 
			
		||||
  const handlePinClick = () => {
 | 
			
		||||
    if (isPinned) deleteSpaceShortcut(roomId);
 | 
			
		||||
    else createSpaceShortcut(roomId);
 | 
			
		||||
    afterOptionSelect();
 | 
			
		||||
  };
 | 
			
		||||
  const handleCategorizeClick = () => {
 | 
			
		||||
    if (isCategorized) unCategorizeSpace(roomId);
 | 
			
		||||
    else categorizeSpace(roomId);
 | 
			
		||||
    afterOptionSelect();
 | 
			
		||||
  };
 | 
			
		||||
  const handleSettingsClick = () => {
 | 
			
		||||
    openSpaceSettings(roomId);
 | 
			
		||||
    afterOptionSelect();
 | 
			
		||||
  };
 | 
			
		||||
  const handleManageRoom = () => {
 | 
			
		||||
    openSpaceManage(roomId);
 | 
			
		||||
    afterOptionSelect();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleLeaveClick = async () => {
 | 
			
		||||
    afterOptionSelect();
 | 
			
		||||
    const isConfirmed = await confirmDialog(
 | 
			
		||||
      'Leave space',
 | 
			
		||||
      `Are you sure that you want to leave "${room.name}" space?`,
 | 
			
		||||
      'Leave',
 | 
			
		||||
      'danger',
 | 
			
		||||
    );
 | 
			
		||||
    if (!isConfirmed) return;
 | 
			
		||||
    leave(roomId);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div style={{ maxWidth: 'calc(var(--navigation-drawer-width) - var(--sp-normal))' }}>
 | 
			
		||||
      <MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
 | 
			
		||||
      <MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
 | 
			
		||||
      <MenuItem
 | 
			
		||||
        onClick={handleCategorizeClick}
 | 
			
		||||
        iconSrc={isCategorized ? CategoryFilledIC : CategoryIC}
 | 
			
		||||
      >
 | 
			
		||||
        {isCategorized ? 'Uncategorize subspaces' : 'Categorize subspaces'}
 | 
			
		||||
      </MenuItem>
 | 
			
		||||
      <MenuItem
 | 
			
		||||
        onClick={handlePinClick}
 | 
			
		||||
        iconSrc={isPinned ? PinFilledIC : PinIC}
 | 
			
		||||
      >
 | 
			
		||||
        {isPinned ? 'Unpin from sidebar' : 'Pin to sidebar'}
 | 
			
		||||
      </MenuItem>
 | 
			
		||||
      <MenuItem
 | 
			
		||||
        iconSrc={AddUserIC}
 | 
			
		||||
        onClick={handleInviteClick}
 | 
			
		||||
        disabled={!canInvite}
 | 
			
		||||
      >
 | 
			
		||||
        Invite
 | 
			
		||||
      </MenuItem>
 | 
			
		||||
      <MenuItem onClick={handleManageRoom} iconSrc={HashSearchIC}>Manage rooms</MenuItem>
 | 
			
		||||
      <MenuItem onClick={handleSettingsClick} iconSrc={SettingsIC}>Settings</MenuItem>
 | 
			
		||||
      <MenuItem
 | 
			
		||||
        variant="danger"
 | 
			
		||||
        onClick={handleLeaveClick}
 | 
			
		||||
        iconSrc={LeaveArrowIC}
 | 
			
		||||
      >
 | 
			
		||||
        Leave
 | 
			
		||||
      </MenuItem>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SpaceOptions.defaultProps = {
 | 
			
		||||
  afterOptionSelect: null,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SpaceOptions.propTypes = {
 | 
			
		||||
  roomId: PropTypes.string.isRequired,
 | 
			
		||||
  afterOptionSelect: PropTypes.func,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SpaceOptions;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,41 +0,0 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './SSOButtons.scss';
 | 
			
		||||
 | 
			
		||||
import { createTemporaryClient, startSsoLogin } from '../../../client/action/auth';
 | 
			
		||||
 | 
			
		||||
import Button from '../../atoms/button/Button';
 | 
			
		||||
 | 
			
		||||
function SSOButtons({ type, identityProviders, baseUrl }) {
 | 
			
		||||
  const tempClient = createTemporaryClient(baseUrl);
 | 
			
		||||
  function handleClick(id) {
 | 
			
		||||
    startSsoLogin(baseUrl, type, id);
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="sso-buttons">
 | 
			
		||||
      {identityProviders
 | 
			
		||||
        .sort((idp, idp2) => {
 | 
			
		||||
          if (typeof idp.icon !== 'string') return -1;
 | 
			
		||||
          return idp.name.toLowerCase() > idp2.name.toLowerCase() ? 1 : -1;
 | 
			
		||||
        })
 | 
			
		||||
        .map((idp) => (
 | 
			
		||||
          idp.icon
 | 
			
		||||
            ? (
 | 
			
		||||
              <button key={idp.id} type="button" className="sso-btn" onClick={() => handleClick(idp.id)}>
 | 
			
		||||
                <img className="sso-btn__img" src={tempClient.mxcUrlToHttp(idp.icon)} alt={idp.name} />
 | 
			
		||||
              </button>
 | 
			
		||||
            ) : <Button key={idp.id} className="sso-btn__text-only" onClick={() => handleClick(idp.id)}>{`Login with ${idp.name}`}</Button>
 | 
			
		||||
        ))}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SSOButtons.propTypes = {
 | 
			
		||||
  identityProviders: PropTypes.arrayOf(
 | 
			
		||||
    PropTypes.shape({}),
 | 
			
		||||
  ).isRequired,
 | 
			
		||||
  baseUrl: PropTypes.string.isRequired,
 | 
			
		||||
  type: PropTypes.oneOf(['sso', 'cas']).isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SSOButtons;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,25 +0,0 @@
 | 
			
		|||
.sso-buttons {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  flex-wrap: wrap;  
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sso-btn {
 | 
			
		||||
  margin: var(--sp-tight);
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
 | 
			
		||||
  &__img {
 | 
			
		||||
    height: var(--av-small);
 | 
			
		||||
    width: var(--av-small);
 | 
			
		||||
  }
 | 
			
		||||
  &__text-only {
 | 
			
		||||
    margin-top: var(--sp-normal);
 | 
			
		||||
    flex-basis: 100%;
 | 
			
		||||
    & .text {
 | 
			
		||||
      color: var(--tc-link);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,11 +2,10 @@ import React, { useState, useEffect, useRef } from 'react';
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import './CreateRoom.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import { selectRoom, openReusableContextMenu } from '../../../client/action/navigation';
 | 
			
		||||
import { openReusableContextMenu } from '../../../client/action/navigation';
 | 
			
		||||
import * as roomActions from '../../../client/action/room';
 | 
			
		||||
import { isRoomAliasAvailable, getIdServer } from '../../../util/matrixUtil';
 | 
			
		||||
import { getEventCords } from '../../../util/common';
 | 
			
		||||
| 
						 | 
				
			
			@ -32,12 +31,14 @@ import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg';
 | 
			
		|||
import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg';
 | 
			
		||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
 | 
			
		||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 | 
			
		||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
 | 
			
		||||
 | 
			
		||||
function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
 | 
			
		||||
  const [joinRule, setJoinRule] = useState(parentId ? 'restricted' : 'invite');
 | 
			
		||||
  const [isEncrypted, setIsEncrypted] = useState(true);
 | 
			
		||||
  const [isCreatingRoom, setIsCreatingRoom] = useState(false);
 | 
			
		||||
  const [creatingError, setCreatingError] = useState(null);
 | 
			
		||||
  const { navigateRoom, navigateSpace } = useRoomNavigate();
 | 
			
		||||
 | 
			
		||||
  const [isValidAddress, setIsValidAddress] = useState(null);
 | 
			
		||||
  const [addressValue, setAddressValue] = useState(undefined);
 | 
			
		||||
| 
						 | 
				
			
			@ -48,25 +49,6 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
 | 
			
		|||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const userHs = getIdServer(mx.getUserId());
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const { roomList } = initMatrix;
 | 
			
		||||
    const onCreated = (roomId) => {
 | 
			
		||||
      setIsCreatingRoom(false);
 | 
			
		||||
      setCreatingError(null);
 | 
			
		||||
      setIsValidAddress(null);
 | 
			
		||||
      setAddressValue(undefined);
 | 
			
		||||
 | 
			
		||||
      if (!mx.getRoom(roomId)?.isSpaceRoom()) {
 | 
			
		||||
        selectRoom(roomId);
 | 
			
		||||
      }
 | 
			
		||||
      onRequestClose();
 | 
			
		||||
    };
 | 
			
		||||
    roomList.on(cons.events.roomList.ROOM_CREATED, onCreated);
 | 
			
		||||
    return () => {
 | 
			
		||||
      roomList.removeListener(cons.events.roomList.ROOM_CREATED, onCreated);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async (evt) => {
 | 
			
		||||
    evt.preventDefault();
 | 
			
		||||
    const { target } = evt;
 | 
			
		||||
| 
						 | 
				
			
			@ -87,16 +69,26 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
 | 
			
		|||
    const powerLevel = roleIndex === 1 ? 101 : undefined;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await roomActions.createRoom({
 | 
			
		||||
      const data = await roomActions.createRoom({
 | 
			
		||||
        name,
 | 
			
		||||
        topic,
 | 
			
		||||
        joinRule,
 | 
			
		||||
        alias: roomAlias,
 | 
			
		||||
        isEncrypted: (isSpace || joinRule === 'public') ? false : isEncrypted,
 | 
			
		||||
        isEncrypted: isSpace || joinRule === 'public' ? false : isEncrypted,
 | 
			
		||||
        powerLevel,
 | 
			
		||||
        isSpace,
 | 
			
		||||
        parentId,
 | 
			
		||||
      });
 | 
			
		||||
      setIsCreatingRoom(false);
 | 
			
		||||
      setCreatingError(null);
 | 
			
		||||
      setIsValidAddress(null);
 | 
			
		||||
      setAddressValue(undefined);
 | 
			
		||||
      onRequestClose();
 | 
			
		||||
      if (isSpace) {
 | 
			
		||||
        navigateSpace(data.room_id);
 | 
			
		||||
      } else {
 | 
			
		||||
        navigateRoom(data.room_id);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e.message === 'M_UNKNOWN: Invalid characters in room alias') {
 | 
			
		||||
        setCreatingError('ERROR: Invalid characters in address');
 | 
			
		||||
| 
						 | 
				
			
			@ -131,36 +123,35 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
 | 
			
		|||
 | 
			
		||||
  const joinRules = ['invite', 'restricted', 'public'];
 | 
			
		||||
  const joinRuleShortText = ['Private', 'Restricted', 'Public'];
 | 
			
		||||
  const joinRuleText = ['Private (invite only)', 'Restricted (space member can join)', 'Public (anyone can join)'];
 | 
			
		||||
  const joinRuleText = [
 | 
			
		||||
    'Private (invite only)',
 | 
			
		||||
    'Restricted (space member can join)',
 | 
			
		||||
    'Public (anyone can join)',
 | 
			
		||||
  ];
 | 
			
		||||
  const jrRoomIC = [HashLockIC, HashIC, HashGlobeIC];
 | 
			
		||||
  const jrSpaceIC = [SpaceLockIC, SpaceIC, SpaceGlobeIC];
 | 
			
		||||
  const handleJoinRule = (evt) => {
 | 
			
		||||
    openReusableContextMenu(
 | 
			
		||||
      'bottom',
 | 
			
		||||
      getEventCords(evt, '.btn-surface'),
 | 
			
		||||
      (closeMenu) => (
 | 
			
		||||
        <>
 | 
			
		||||
          <MenuHeader>Visibility (who can join)</MenuHeader>
 | 
			
		||||
          {
 | 
			
		||||
            joinRules.map((rule) => (
 | 
			
		||||
              <MenuItem
 | 
			
		||||
                key={rule}
 | 
			
		||||
                variant={rule === joinRule ? 'positive' : 'surface'}
 | 
			
		||||
                iconSrc={
 | 
			
		||||
                  isSpace
 | 
			
		||||
                    ? jrSpaceIC[joinRules.indexOf(rule)]
 | 
			
		||||
                    : jrRoomIC[joinRules.indexOf(rule)]
 | 
			
		||||
                }
 | 
			
		||||
                onClick={() => { closeMenu(); setJoinRule(rule); }}
 | 
			
		||||
                disabled={!parentId && rule === 'restricted'}
 | 
			
		||||
              >
 | 
			
		||||
                { joinRuleText[joinRules.indexOf(rule)] }
 | 
			
		||||
              </MenuItem>
 | 
			
		||||
            ))
 | 
			
		||||
          }
 | 
			
		||||
        </>
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    openReusableContextMenu('bottom', getEventCords(evt, '.btn-surface'), (closeMenu) => (
 | 
			
		||||
      <>
 | 
			
		||||
        <MenuHeader>Visibility (who can join)</MenuHeader>
 | 
			
		||||
        {joinRules.map((rule) => (
 | 
			
		||||
          <MenuItem
 | 
			
		||||
            key={rule}
 | 
			
		||||
            variant={rule === joinRule ? 'positive' : 'surface'}
 | 
			
		||||
            iconSrc={
 | 
			
		||||
              isSpace ? jrSpaceIC[joinRules.indexOf(rule)] : jrRoomIC[joinRules.indexOf(rule)]
 | 
			
		||||
            }
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              closeMenu();
 | 
			
		||||
              setJoinRule(rule);
 | 
			
		||||
            }}
 | 
			
		||||
            disabled={!parentId && rule === 'restricted'}
 | 
			
		||||
          >
 | 
			
		||||
            {joinRuleText[joinRules.indexOf(rule)]}
 | 
			
		||||
          </MenuItem>
 | 
			
		||||
        ))}
 | 
			
		||||
      </>
 | 
			
		||||
    ));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
| 
						 | 
				
			
			@ -168,50 +159,64 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
 | 
			
		|||
      <form className="create-room__form" onSubmit={handleSubmit}>
 | 
			
		||||
        <SettingTile
 | 
			
		||||
          title="Visibility"
 | 
			
		||||
          options={(
 | 
			
		||||
          options={
 | 
			
		||||
            <Button onClick={handleJoinRule} iconSrc={ChevronBottomIC}>
 | 
			
		||||
              {joinRuleShortText[joinRules.indexOf(joinRule)]}
 | 
			
		||||
            </Button>
 | 
			
		||||
          )}
 | 
			
		||||
          content={<Text variant="b3">{`Select who can join this ${isSpace ? 'space' : 'room'}.`}</Text>}
 | 
			
		||||
          }
 | 
			
		||||
          content={
 | 
			
		||||
            <Text variant="b3">{`Select who can join this ${isSpace ? 'space' : 'room'}.`}</Text>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        {joinRule === 'public' && (
 | 
			
		||||
          <div>
 | 
			
		||||
            <Text className="create-room__address__label" variant="b2">{isSpace ? 'Space address' : 'Room address'}</Text>
 | 
			
		||||
            <Text className="create-room__address__label" variant="b2">
 | 
			
		||||
              {isSpace ? 'Space address' : 'Room address'}
 | 
			
		||||
            </Text>
 | 
			
		||||
            <div className="create-room__address">
 | 
			
		||||
              <Text variant="b1">#</Text>
 | 
			
		||||
              <Input
 | 
			
		||||
                value={addressValue}
 | 
			
		||||
                onChange={validateAddress}
 | 
			
		||||
                state={(isValidAddress === false) ? 'error' : 'normal'}
 | 
			
		||||
                state={isValidAddress === false ? 'error' : 'normal'}
 | 
			
		||||
                forwardRef={addressRef}
 | 
			
		||||
                placeholder="my_address"
 | 
			
		||||
                required
 | 
			
		||||
              />
 | 
			
		||||
              <Text variant="b1">{`:${userHs}`}</Text>
 | 
			
		||||
            </div>
 | 
			
		||||
            {isValidAddress === false && <Text className="create-room__address__tip" variant="b3"><span style={{ color: 'var(--bg-danger)' }}>{`#${addressValue}:${userHs} is already in use`}</span></Text>}
 | 
			
		||||
            {isValidAddress === false && (
 | 
			
		||||
              <Text className="create-room__address__tip" variant="b3">
 | 
			
		||||
                <span
 | 
			
		||||
                  style={{ color: 'var(--bg-danger)' }}
 | 
			
		||||
                >{`#${addressValue}:${userHs} is already in use`}</span>
 | 
			
		||||
              </Text>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        {!isSpace && joinRule !== 'public' && (
 | 
			
		||||
          <SettingTile
 | 
			
		||||
            title="Enable end-to-end encryption"
 | 
			
		||||
            options={<Toggle isActive={isEncrypted} onToggle={setIsEncrypted} />}
 | 
			
		||||
            content={<Text variant="b3">You can’t disable this later. Bridges & most bots won’t work yet.</Text>}
 | 
			
		||||
            content={
 | 
			
		||||
              <Text variant="b3">
 | 
			
		||||
                You can’t disable this later. Bridges & most bots won’t work yet.
 | 
			
		||||
              </Text>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        <SettingTile
 | 
			
		||||
          title="Select your role"
 | 
			
		||||
          options={(
 | 
			
		||||
          options={
 | 
			
		||||
            <SegmentControl
 | 
			
		||||
              selected={roleIndex}
 | 
			
		||||
              segments={[{ text: 'Admin' }, { text: 'Founder' }]}
 | 
			
		||||
              onSelect={setRoleIndex}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          content={(
 | 
			
		||||
          }
 | 
			
		||||
          content={
 | 
			
		||||
            <Text variant="b3">Selecting Admin sets 100 power level whereas Founder sets 101.</Text>
 | 
			
		||||
          )}
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Input name="topic" minHeight={174} resizable label="Topic (optional)" />
 | 
			
		||||
        <div className="create-room__name-wrapper">
 | 
			
		||||
| 
						 | 
				
			
			@ -231,7 +236,11 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
 | 
			
		|||
            <Text>{`Creating ${isSpace ? 'space' : 'room'}...`}</Text>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        {typeof creatingError === 'string' && <Text className="create-room__error" variant="b3">{creatingError}</Text>}
 | 
			
		||||
        {typeof creatingError === 'string' && (
 | 
			
		||||
          <Text className="create-room__error" variant="b3">
 | 
			
		||||
            {creatingError}
 | 
			
		||||
          </Text>
 | 
			
		||||
        )}
 | 
			
		||||
      </form>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			@ -275,27 +284,22 @@ function CreateRoom() {
 | 
			
		|||
  return (
 | 
			
		||||
    <Dialog
 | 
			
		||||
      isOpen={create !== null}
 | 
			
		||||
      title={(
 | 
			
		||||
      title={
 | 
			
		||||
        <Text variant="s1" weight="medium" primary>
 | 
			
		||||
          {parentId ? twemojify(room.name) : 'Home'}
 | 
			
		||||
          {parentId ? room.name : 'Home'}
 | 
			
		||||
          <span style={{ color: 'var(--tc-surface-low)' }}>
 | 
			
		||||
            {` — create ${isSpace ? 'space' : 'room'}`}
 | 
			
		||||
          </span>
 | 
			
		||||
        </Text>
 | 
			
		||||
      )}
 | 
			
		||||
      }
 | 
			
		||||
      contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
 | 
			
		||||
      onRequestClose={onRequestClose}
 | 
			
		||||
    >
 | 
			
		||||
      {
 | 
			
		||||
        create
 | 
			
		||||
          ? (
 | 
			
		||||
            <CreateRoomContent
 | 
			
		||||
              isSpace={isSpace}
 | 
			
		||||
              parentId={parentId}
 | 
			
		||||
              onRequestClose={onRequestClose}
 | 
			
		||||
            />
 | 
			
		||||
          ) : <div />
 | 
			
		||||
      }
 | 
			
		||||
      {create ? (
 | 
			
		||||
        <CreateRoomContent isSpace={isSpace} parentId={parentId} onRequestClose={onRequestClose} />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <div />
 | 
			
		||||
      )}
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,356 +0,0 @@
 | 
			
		|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
 | 
			
		||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
 | 
			
		||||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './EmojiBoard.scss';
 | 
			
		||||
 | 
			
		||||
import parse from 'html-react-parser';
 | 
			
		||||
import twemoji from 'twemoji';
 | 
			
		||||
import { emojiGroups, emojis } from './emoji';
 | 
			
		||||
import { getRelevantPacks } from './custom-emoji';
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import AsyncSearch from '../../../util/AsyncSearch';
 | 
			
		||||
import { addRecentEmoji, getRecentEmojis } from './recent';
 | 
			
		||||
import { TWEMOJI_BASE_URL } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import RawIcon from '../../atoms/system-icons/RawIcon';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import Input from '../../atoms/input/Input';
 | 
			
		||||
import ScrollView from '../../atoms/scroll/ScrollView';
 | 
			
		||||
 | 
			
		||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
 | 
			
		||||
import RecentClockIC from '../../../../public/res/ic/outlined/recent-clock.svg';
 | 
			
		||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
 | 
			
		||||
import DogIC from '../../../../public/res/ic/outlined/dog.svg';
 | 
			
		||||
import CupIC from '../../../../public/res/ic/outlined/cup.svg';
 | 
			
		||||
import BallIC from '../../../../public/res/ic/outlined/ball.svg';
 | 
			
		||||
import PhotoIC from '../../../../public/res/ic/outlined/photo.svg';
 | 
			
		||||
import BulbIC from '../../../../public/res/ic/outlined/bulb.svg';
 | 
			
		||||
import PeaceIC from '../../../../public/res/ic/outlined/peace.svg';
 | 
			
		||||
import FlagIC from '../../../../public/res/ic/outlined/flag.svg';
 | 
			
		||||
 | 
			
		||||
const ROW_EMOJIS_COUNT = 7;
 | 
			
		||||
 | 
			
		||||
const EmojiGroup = React.memo(({ name, groupEmojis }) => {
 | 
			
		||||
  function getEmojiBoard() {
 | 
			
		||||
    const emojiBoard = [];
 | 
			
		||||
    const totalEmojis = groupEmojis.length;
 | 
			
		||||
 | 
			
		||||
    for (let r = 0; r < totalEmojis; r += ROW_EMOJIS_COUNT) {
 | 
			
		||||
      const emojiRow = [];
 | 
			
		||||
      for (let c = r; c < r + ROW_EMOJIS_COUNT; c += 1) {
 | 
			
		||||
        const emojiIndex = c;
 | 
			
		||||
        if (emojiIndex >= totalEmojis) break;
 | 
			
		||||
        const emoji = groupEmojis[emojiIndex];
 | 
			
		||||
        emojiRow.push(
 | 
			
		||||
          <span key={emojiIndex}>
 | 
			
		||||
            {emoji.hexcode ? (
 | 
			
		||||
              // This is a unicode emoji, and should be rendered with twemoji
 | 
			
		||||
              parse(
 | 
			
		||||
                twemoji.parse(emoji.unicode, {
 | 
			
		||||
                  attributes: () => ({
 | 
			
		||||
                    unicode: emoji.unicode,
 | 
			
		||||
                    shortcodes: emoji.shortcodes?.toString(),
 | 
			
		||||
                    hexcode: emoji.hexcode,
 | 
			
		||||
                    loading: 'lazy',
 | 
			
		||||
                  }),
 | 
			
		||||
                  base: TWEMOJI_BASE_URL,
 | 
			
		||||
                })
 | 
			
		||||
              )
 | 
			
		||||
            ) : (
 | 
			
		||||
              // This is a custom emoji, and should be render as an mxc
 | 
			
		||||
              <img
 | 
			
		||||
                className="emoji"
 | 
			
		||||
                draggable="false"
 | 
			
		||||
                loading="lazy"
 | 
			
		||||
                alt={emoji.shortcode}
 | 
			
		||||
                unicode={`:${emoji.shortcode}:`}
 | 
			
		||||
                shortcodes={emoji.shortcode}
 | 
			
		||||
                src={initMatrix.matrixClient.mxcUrlToHttp(emoji.mxc)}
 | 
			
		||||
                data-mx-emoticon={emoji.mxc}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </span>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      emojiBoard.push(
 | 
			
		||||
        <div key={r} className="emoji-row">
 | 
			
		||||
          {emojiRow}
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return emojiBoard;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="emoji-group">
 | 
			
		||||
      <Text className="emoji-group__header" variant="b2" weight="bold">
 | 
			
		||||
        {name}
 | 
			
		||||
      </Text>
 | 
			
		||||
      {groupEmojis.length !== 0 && <div className="emoji-set noselect">{getEmojiBoard()}</div>}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
EmojiGroup.propTypes = {
 | 
			
		||||
  name: PropTypes.string.isRequired,
 | 
			
		||||
  groupEmojis: PropTypes.arrayOf(
 | 
			
		||||
    PropTypes.shape({
 | 
			
		||||
      length: PropTypes.number,
 | 
			
		||||
      unicode: PropTypes.string,
 | 
			
		||||
      hexcode: PropTypes.string,
 | 
			
		||||
      mxc: PropTypes.string,
 | 
			
		||||
      shortcode: PropTypes.string,
 | 
			
		||||
      shortcodes: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
 | 
			
		||||
    })
 | 
			
		||||
  ).isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const asyncSearch = new AsyncSearch();
 | 
			
		||||
asyncSearch.setup(emojis, { keys: ['shortcode'], isContain: true, limit: 40 });
 | 
			
		||||
function SearchedEmoji() {
 | 
			
		||||
  const [searchedEmojis, setSearchedEmojis] = useState(null);
 | 
			
		||||
 | 
			
		||||
  function handleSearchEmoji(resultEmojis, term) {
 | 
			
		||||
    if (term === '' || resultEmojis.length === 0) {
 | 
			
		||||
      if (term === '') setSearchedEmojis(null);
 | 
			
		||||
      else setSearchedEmojis({ emojis: [] });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setSearchedEmojis({ emojis: resultEmojis });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    asyncSearch.on(asyncSearch.RESULT_SENT, handleSearchEmoji);
 | 
			
		||||
    return () => {
 | 
			
		||||
      asyncSearch.removeListener(asyncSearch.RESULT_SENT, handleSearchEmoji);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  if (searchedEmojis === null) return false;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <EmojiGroup
 | 
			
		||||
      key="-1"
 | 
			
		||||
      name={searchedEmojis.emojis.length === 0 ? 'No search result found' : 'Search results'}
 | 
			
		||||
      groupEmojis={searchedEmojis.emojis}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function EmojiBoard({ onSelect, searchRef }) {
 | 
			
		||||
  const scrollEmojisRef = useRef(null);
 | 
			
		||||
  const emojiInfo = useRef(null);
 | 
			
		||||
 | 
			
		||||
  function isTargetNotEmoji(target) {
 | 
			
		||||
    return target.classList.contains('emoji') === false;
 | 
			
		||||
  }
 | 
			
		||||
  function getEmojiDataFromTarget(target) {
 | 
			
		||||
    const unicode = target.getAttribute('unicode');
 | 
			
		||||
    const hexcode = target.getAttribute('hexcode');
 | 
			
		||||
    const mxc = target.getAttribute('data-mx-emoticon');
 | 
			
		||||
    let shortcodes = target.getAttribute('shortcodes');
 | 
			
		||||
    if (typeof shortcodes === 'undefined') shortcodes = undefined;
 | 
			
		||||
    else shortcodes = shortcodes.split(',');
 | 
			
		||||
    return {
 | 
			
		||||
      unicode,
 | 
			
		||||
      hexcode,
 | 
			
		||||
      shortcodes,
 | 
			
		||||
      mxc,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function selectEmoji(e) {
 | 
			
		||||
    if (isTargetNotEmoji(e.target)) return;
 | 
			
		||||
 | 
			
		||||
    const emoji = getEmojiDataFromTarget(e.target);
 | 
			
		||||
    onSelect(emoji);
 | 
			
		||||
    if (emoji.hexcode) addRecentEmoji(emoji.unicode);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function setEmojiInfo(emoji) {
 | 
			
		||||
    const infoEmoji = emojiInfo.current.firstElementChild.firstElementChild;
 | 
			
		||||
    const infoShortcode = emojiInfo.current.lastElementChild;
 | 
			
		||||
 | 
			
		||||
    infoEmoji.src = emoji.src;
 | 
			
		||||
    infoEmoji.alt = emoji.unicode;
 | 
			
		||||
    infoShortcode.textContent = `:${emoji.shortcode}:`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function hoverEmoji(e) {
 | 
			
		||||
    if (isTargetNotEmoji(e.target)) return;
 | 
			
		||||
 | 
			
		||||
    const emoji = e.target;
 | 
			
		||||
    const { shortcodes, unicode } = getEmojiDataFromTarget(emoji);
 | 
			
		||||
    const { src } = e.target;
 | 
			
		||||
 | 
			
		||||
    if (typeof shortcodes === 'undefined') {
 | 
			
		||||
      searchRef.current.placeholder = 'Search';
 | 
			
		||||
      setEmojiInfo({
 | 
			
		||||
        unicode: '🙂',
 | 
			
		||||
        shortcode: 'slight_smile',
 | 
			
		||||
        src: 'https://twemoji.maxcdn.com/v/13.1.0/72x72/1f642.png',
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (searchRef.current.placeholder === shortcodes[0]) return;
 | 
			
		||||
    searchRef.current.setAttribute('placeholder', shortcodes[0]);
 | 
			
		||||
    setEmojiInfo({ shortcode: shortcodes[0], src, unicode });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleSearchChange() {
 | 
			
		||||
    const term = searchRef.current.value;
 | 
			
		||||
    asyncSearch.search(term);
 | 
			
		||||
    scrollEmojisRef.current.scrollTop = 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [availableEmojis, setAvailableEmojis] = useState([]);
 | 
			
		||||
  const [recentEmojis, setRecentEmojis] = useState([]);
 | 
			
		||||
 | 
			
		||||
  const recentOffset = recentEmojis.length > 0 ? 1 : 0;
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const updateAvailableEmoji = (selectedRoomId) => {
 | 
			
		||||
      if (!selectedRoomId) {
 | 
			
		||||
        setAvailableEmojis([]);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const mx = initMatrix.matrixClient;
 | 
			
		||||
      const room = mx.getRoom(selectedRoomId);
 | 
			
		||||
      const parentIds = initMatrix.roomList.getAllParentSpaces(room.roomId);
 | 
			
		||||
      const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
 | 
			
		||||
      if (room) {
 | 
			
		||||
        const packs = getRelevantPacks(room.client, [room, ...parentRooms]).filter(
 | 
			
		||||
          (pack) => pack.getEmojis().length !== 0
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Set an index for each pack so that we know where to jump when the user uses the nav
 | 
			
		||||
        for (let i = 0; i < packs.length; i += 1) {
 | 
			
		||||
          packs[i].packIndex = i;
 | 
			
		||||
        }
 | 
			
		||||
        setAvailableEmojis(packs);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onOpen = () => {
 | 
			
		||||
      searchRef.current.value = '';
 | 
			
		||||
      handleSearchChange();
 | 
			
		||||
 | 
			
		||||
      // only update when board is getting opened to prevent shifting UI
 | 
			
		||||
      setRecentEmojis(getRecentEmojis(3 * ROW_EMOJIS_COUNT));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    navigation.on(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
 | 
			
		||||
    navigation.on(cons.events.navigation.EMOJIBOARD_OPENED, onOpen);
 | 
			
		||||
    return () => {
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.EMOJIBOARD_OPENED, onOpen);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  function openGroup(groupOrder) {
 | 
			
		||||
    let tabIndex = groupOrder;
 | 
			
		||||
    const $emojiContent = scrollEmojisRef.current.firstElementChild;
 | 
			
		||||
    const groupCount = $emojiContent.childElementCount;
 | 
			
		||||
    if (groupCount > emojiGroups.length) {
 | 
			
		||||
      tabIndex += groupCount - emojiGroups.length - availableEmojis.length - recentOffset;
 | 
			
		||||
    }
 | 
			
		||||
    $emojiContent.children[tabIndex].scrollIntoView();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div id="emoji-board" className="emoji-board">
 | 
			
		||||
      <ScrollView invisible>
 | 
			
		||||
        <div className="emoji-board__nav">
 | 
			
		||||
          {recentEmojis.length > 0 && (
 | 
			
		||||
            <IconButton
 | 
			
		||||
              onClick={() => openGroup(0)}
 | 
			
		||||
              src={RecentClockIC}
 | 
			
		||||
              tooltip="Recent"
 | 
			
		||||
              tooltipPlacement="left"
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          <div className="emoji-board__nav-custom">
 | 
			
		||||
            {availableEmojis.map((pack) => {
 | 
			
		||||
              const src = initMatrix.matrixClient.mxcUrlToHttp(
 | 
			
		||||
                pack.avatarUrl ?? pack.getEmojis()[0].mxc
 | 
			
		||||
              );
 | 
			
		||||
              return (
 | 
			
		||||
                <IconButton
 | 
			
		||||
                  onClick={() => openGroup(recentOffset + pack.packIndex)}
 | 
			
		||||
                  src={src}
 | 
			
		||||
                  key={pack.packIndex}
 | 
			
		||||
                  tooltip={pack.displayName ?? 'Unknown'}
 | 
			
		||||
                  tooltipPlacement="left"
 | 
			
		||||
                  isImage
 | 
			
		||||
                />
 | 
			
		||||
              );
 | 
			
		||||
            })}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="emoji-board__nav-twemoji">
 | 
			
		||||
            {[
 | 
			
		||||
              [0, EmojiIC, 'Smilies'],
 | 
			
		||||
              [1, DogIC, 'Animals'],
 | 
			
		||||
              [2, CupIC, 'Food'],
 | 
			
		||||
              [3, BallIC, 'Activities'],
 | 
			
		||||
              [4, PhotoIC, 'Travel'],
 | 
			
		||||
              [5, BulbIC, 'Objects'],
 | 
			
		||||
              [6, PeaceIC, 'Symbols'],
 | 
			
		||||
              [7, FlagIC, 'Flags'],
 | 
			
		||||
            ].map(([indx, ico, name]) => (
 | 
			
		||||
              <IconButton
 | 
			
		||||
                onClick={() => openGroup(recentOffset + availableEmojis.length + indx)}
 | 
			
		||||
                key={indx}
 | 
			
		||||
                src={ico}
 | 
			
		||||
                tooltip={name}
 | 
			
		||||
                tooltipPlacement="left"
 | 
			
		||||
              />
 | 
			
		||||
            ))}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </ScrollView>
 | 
			
		||||
      <div className="emoji-board__content">
 | 
			
		||||
        <div className="emoji-board__content__search">
 | 
			
		||||
          <RawIcon size="small" src={SearchIC} />
 | 
			
		||||
          <Input onChange={handleSearchChange} forwardRef={searchRef} placeholder="Search" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="emoji-board__content__emojis">
 | 
			
		||||
          <ScrollView ref={scrollEmojisRef} autoHide>
 | 
			
		||||
            <div onMouseMove={hoverEmoji} onClick={selectEmoji}>
 | 
			
		||||
              <SearchedEmoji />
 | 
			
		||||
              {recentEmojis.length > 0 && (
 | 
			
		||||
                <EmojiGroup name="Recently used" groupEmojis={recentEmojis} />
 | 
			
		||||
              )}
 | 
			
		||||
              {availableEmojis.map((pack) => (
 | 
			
		||||
                <EmojiGroup
 | 
			
		||||
                  name={pack.displayName ?? 'Unknown'}
 | 
			
		||||
                  key={pack.packIndex}
 | 
			
		||||
                  groupEmojis={pack.getEmojis()}
 | 
			
		||||
                  className="custom-emoji-group"
 | 
			
		||||
                />
 | 
			
		||||
              ))}
 | 
			
		||||
              {emojiGroups.map((group) => (
 | 
			
		||||
                <EmojiGroup key={group.name} name={group.name} groupEmojis={group.emojis} />
 | 
			
		||||
              ))}
 | 
			
		||||
            </div>
 | 
			
		||||
          </ScrollView>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div ref={emojiInfo} className="emoji-board__content__info">
 | 
			
		||||
          <div>{parse(twemoji.parse('🙂', { base: TWEMOJI_BASE_URL }))}</div>
 | 
			
		||||
          <Text>:slight_smile:</Text>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
EmojiBoard.propTypes = {
 | 
			
		||||
  onSelect: PropTypes.func.isRequired,
 | 
			
		||||
  searchRef: PropTypes.shape({}).isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default EmojiBoard;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,137 +0,0 @@
 | 
			
		|||
@use '../../partials/flex';
 | 
			
		||||
@use '../../partials/text';
 | 
			
		||||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.emoji-board {
 | 
			
		||||
  --emoji-board-height: 390px;
 | 
			
		||||
  --emoji-board-width: 286px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  max-width: 90vw;
 | 
			
		||||
  max-height: 90vh;
 | 
			
		||||
  
 | 
			
		||||
  &__content {
 | 
			
		||||
    @extend .cp-fx__item-one;
 | 
			
		||||
    @extend .cp-fx__column;
 | 
			
		||||
    height: var(--emoji-board-height);
 | 
			
		||||
    width: var(--emoji-board-width);
 | 
			
		||||
  }
 | 
			
		||||
  & > .scrollbar {
 | 
			
		||||
    width: initial;
 | 
			
		||||
    height: var(--emoji-board-height);
 | 
			
		||||
  }
 | 
			
		||||
  &__nav {
 | 
			
		||||
    @extend .cp-fx__column;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
 | 
			
		||||
    min-height: 100%;
 | 
			
		||||
    padding: 4px 6px;
 | 
			
		||||
    @include dir.side(border, none, 1px solid var(--bg-surface-border));
 | 
			
		||||
 | 
			
		||||
    position: relative;
 | 
			
		||||
    
 | 
			
		||||
    & .ic-btn-surface {
 | 
			
		||||
      opacity: 0.8;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &__nav-custom,
 | 
			
		||||
  &__nav-twemoji {
 | 
			
		||||
    @extend .cp-fx__column;
 | 
			
		||||
  }
 | 
			
		||||
  &__nav-twemoji {
 | 
			
		||||
    background-color: var(--bg-surface);
 | 
			
		||||
    position: sticky;
 | 
			
		||||
    bottom: -70%;
 | 
			
		||||
    z-index: 999;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.emoji-board__content__search {
 | 
			
		||||
  padding: var(--sp-extra-tight);
 | 
			
		||||
  position: relative;
 | 
			
		||||
  
 | 
			
		||||
  & .ic-raw {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    @include dir.prop(left, var(--sp-normal), unset);
 | 
			
		||||
    @include dir.prop(right, unset, var(--sp-normal));
 | 
			
		||||
    top: var(--sp-normal);
 | 
			
		||||
    transform: translateY(1px);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & .input-container {
 | 
			
		||||
    & .input {
 | 
			
		||||
      min-width: 100%;
 | 
			
		||||
      width: 0;
 | 
			
		||||
      padding: var(--sp-extra-tight) 36px;
 | 
			
		||||
      border-radius: calc(var(--bo-radius) / 2);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.emoji-board__content__emojis {
 | 
			
		||||
  @extend .cp-fx__item-one;
 | 
			
		||||
  @extend .cp-fx__column;
 | 
			
		||||
}
 | 
			
		||||
.emoji-board__content__info {
 | 
			
		||||
  margin: 0 var(--sp-extra-tight);
 | 
			
		||||
  padding: var(--sp-tight) var(--sp-extra-tight);
 | 
			
		||||
  border-top: 1px solid var(--bg-surface-border);
 | 
			
		||||
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
 | 
			
		||||
  & > div:first-child {
 | 
			
		||||
    line-height: 0;
 | 
			
		||||
    .emoji {
 | 
			
		||||
      width: 32px;
 | 
			
		||||
      height: 32px;
 | 
			
		||||
      object-fit: contain;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  & > p:last-child {
 | 
			
		||||
    @extend .cp-fx__item-one;
 | 
			
		||||
    @extend .cp-txt__ellipsis;
 | 
			
		||||
    margin: 0 var(--sp-tight);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.emoji-row {
 | 
			
		||||
  display: flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.emoji-group {
 | 
			
		||||
  --emoji-padding: 6px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  margin-bottom: var(--sp-normal);
 | 
			
		||||
  
 | 
			
		||||
  &__header {
 | 
			
		||||
    position: sticky;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    z-index: 99;
 | 
			
		||||
    background-color: var(--bg-surface);
 | 
			
		||||
 | 
			
		||||
    @include dir.side(margin, var(--sp-extra-tight), 0);
 | 
			
		||||
    padding: var(--sp-extra-tight) var(--sp-ultra-tight);
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
    box-shadow: 0 -4px 0 0 var(--bg-surface);
 | 
			
		||||
    border-bottom: 1px solid var(--bg-surface-border);
 | 
			
		||||
  }
 | 
			
		||||
  & .emoji-set {
 | 
			
		||||
    --left-margin: calc(var(--sp-normal) - var(--emoji-padding));
 | 
			
		||||
    --right-margin: calc(var(--sp-extra-tight) - var(--emoji-padding));
 | 
			
		||||
    margin: var(--sp-extra-tight);
 | 
			
		||||
    @include dir.side(margin, var(--left-margin), var(--right-margin));
 | 
			
		||||
  }
 | 
			
		||||
  & .emoji {
 | 
			
		||||
    max-width: 38px;
 | 
			
		||||
    max-height: 38px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    object-fit: contain;
 | 
			
		||||
    padding: var(--emoji-padding);
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: var(--bg-surface-hover);
 | 
			
		||||
      border-radius: var(--bo-radius);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,78 +0,0 @@
 | 
			
		|||
import React, { useEffect, useRef } from 'react';
 | 
			
		||||
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import settings from '../../../client/state/settings';
 | 
			
		||||
 | 
			
		||||
import ContextMenu from '../../atoms/context-menu/ContextMenu';
 | 
			
		||||
import EmojiBoard from './EmojiBoard';
 | 
			
		||||
 | 
			
		||||
let requestCallback = null;
 | 
			
		||||
let isEmojiBoardVisible = false;
 | 
			
		||||
function EmojiBoardOpener() {
 | 
			
		||||
  const openerRef = useRef(null);
 | 
			
		||||
  const searchRef = useRef(null);
 | 
			
		||||
 | 
			
		||||
  function openEmojiBoard(cords, requestEmojiCallback) {
 | 
			
		||||
    if (requestCallback !== null || isEmojiBoardVisible) {
 | 
			
		||||
      requestCallback = null;
 | 
			
		||||
      if (cords.detail === 0) openerRef.current.click();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    openerRef.current.style.transform = `translate(${cords.x}px, ${cords.y}px)`;
 | 
			
		||||
    requestCallback = requestEmojiCallback;
 | 
			
		||||
    openerRef.current.click();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function afterEmojiBoardToggle(isVisible) {
 | 
			
		||||
    isEmojiBoardVisible = isVisible;
 | 
			
		||||
    if (isVisible) {
 | 
			
		||||
      if (!settings.isTouchScreenDevice) searchRef.current.focus();
 | 
			
		||||
    } else {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        if (!isEmojiBoardVisible) requestCallback = null;
 | 
			
		||||
      }, 500);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function addEmoji(emoji) {
 | 
			
		||||
    requestCallback(emoji);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    navigation.on(cons.events.navigation.EMOJIBOARD_OPENED, openEmojiBoard);
 | 
			
		||||
    return () => {
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.EMOJIBOARD_OPENED, openEmojiBoard);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ContextMenu
 | 
			
		||||
      content={(
 | 
			
		||||
        <EmojiBoard onSelect={addEmoji} searchRef={searchRef} />
 | 
			
		||||
      )}
 | 
			
		||||
      afterToggle={afterEmojiBoardToggle}
 | 
			
		||||
      render={(toggleMenu) => (
 | 
			
		||||
        <input
 | 
			
		||||
          ref={openerRef}
 | 
			
		||||
          onClick={toggleMenu}
 | 
			
		||||
          type="button"
 | 
			
		||||
          style={{
 | 
			
		||||
            width: '32px',
 | 
			
		||||
            height: '32px',
 | 
			
		||||
            backgroundColor: 'transparent',
 | 
			
		||||
            position: 'absolute',
 | 
			
		||||
            top: 0,
 | 
			
		||||
            left: 0,
 | 
			
		||||
            padding: 0,
 | 
			
		||||
            border: 'none',
 | 
			
		||||
            visibility: 'hidden',
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default EmojiBoardOpener;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +1,6 @@
 | 
			
		|||
import { emojis } from './emoji';
 | 
			
		||||
 | 
			
		||||
// https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md
 | 
			
		||||
 | 
			
		||||
class ImagePack {
 | 
			
		||||
export class ImagePack {
 | 
			
		||||
  static parsePack(eventId, packContent) {
 | 
			
		||||
    if (!eventId || typeof packContent?.images !== 'object') {
 | 
			
		||||
      return null;
 | 
			
		||||
| 
						 | 
				
			
			@ -141,127 +139,4 @@ class ImagePack {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getGlobalImagePacks(mx) {
 | 
			
		||||
  const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent();
 | 
			
		||||
  if (typeof globalContent !== 'object') return [];
 | 
			
		||||
 | 
			
		||||
  const { rooms } = globalContent;
 | 
			
		||||
  if (typeof rooms !== 'object') return [];
 | 
			
		||||
 | 
			
		||||
  const roomIds = Object.keys(rooms);
 | 
			
		||||
 | 
			
		||||
  const packs = roomIds.flatMap((roomId) => {
 | 
			
		||||
    if (typeof rooms[roomId] !== 'object') return [];
 | 
			
		||||
    const room = mx.getRoom(roomId);
 | 
			
		||||
    if (!room) return [];
 | 
			
		||||
    const stateKeys = Object.keys(rooms[roomId]);
 | 
			
		||||
 | 
			
		||||
    return stateKeys.map((stateKey) => {
 | 
			
		||||
      const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
 | 
			
		||||
      const pack = ImagePack.parsePack(data?.getId(), data?.getContent());
 | 
			
		||||
      if (pack) {
 | 
			
		||||
        pack.displayName ??= room.name;
 | 
			
		||||
        pack.avatarUrl ??= room.getMxcAvatarUrl();
 | 
			
		||||
      }
 | 
			
		||||
      return pack;
 | 
			
		||||
    }).filter((pack) => pack !== null);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return packs;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getUserImagePack(mx) {
 | 
			
		||||
  const accountDataEmoji = mx.getAccountData('im.ponies.user_emotes');
 | 
			
		||||
  if (!accountDataEmoji) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const userImagePack = ImagePack.parsePack(mx.getUserId(), accountDataEmoji.event.content);
 | 
			
		||||
  if (userImagePack) userImagePack.displayName ??= 'Personal Emoji';
 | 
			
		||||
  return userImagePack;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getRoomImagePacks(room) {
 | 
			
		||||
  const dataEvents = room.currentState.getStateEvents('im.ponies.room_emotes');
 | 
			
		||||
 | 
			
		||||
  return dataEvents
 | 
			
		||||
    .map((data) => {
 | 
			
		||||
      const pack = ImagePack.parsePack(data?.getId(), data?.getContent());
 | 
			
		||||
      if (pack) {
 | 
			
		||||
        pack.displayName ??= room.name;
 | 
			
		||||
        pack.avatarUrl ??= room.getMxcAvatarUrl();
 | 
			
		||||
      }
 | 
			
		||||
      return pack;
 | 
			
		||||
    })
 | 
			
		||||
    .filter((pack) => pack !== null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {MatrixClient} mx Provide if you want to include user personal/global pack
 | 
			
		||||
 * @param {Room[]} rooms Provide rooms if you want to include rooms pack
 | 
			
		||||
 * @returns {ImagePack[]} packs
 | 
			
		||||
 */
 | 
			
		||||
function getRelevantPacks(mx, rooms) {
 | 
			
		||||
  const userPack = mx ? getUserImagePack(mx) : [];
 | 
			
		||||
  const globalPacks = mx ? getGlobalImagePacks(mx) : [];
 | 
			
		||||
  const globalPackIds = new Set(globalPacks.map((pack) => pack.id));
 | 
			
		||||
  const roomsPack = rooms?.flatMap(getRoomImagePacks) ?? [];
 | 
			
		||||
 | 
			
		||||
  return [].concat(
 | 
			
		||||
    userPack ?? [],
 | 
			
		||||
    globalPacks,
 | 
			
		||||
    roomsPack.filter((pack) => !globalPackIds.has(pack.id)),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getShortcodeToEmoji(mx, rooms) {
 | 
			
		||||
  const allEmoji = new Map();
 | 
			
		||||
 | 
			
		||||
  emojis.forEach((emoji) => {
 | 
			
		||||
    if (Array.isArray(emoji.shortcodes)) {
 | 
			
		||||
      emoji.shortcodes.forEach((shortcode) => {
 | 
			
		||||
        allEmoji.set(shortcode, emoji);
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      allEmoji.set(emoji.shortcodes, emoji);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  getRelevantPacks(mx, rooms)
 | 
			
		||||
    .flatMap((pack) => pack.getEmojis())
 | 
			
		||||
    .forEach((emoji) => {
 | 
			
		||||
      allEmoji.set(emoji.shortcode, emoji);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  return allEmoji;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getShortcodeToCustomEmoji(room) {
 | 
			
		||||
  const allEmoji = new Map();
 | 
			
		||||
 | 
			
		||||
  getRelevantPacks(room.client, [room])
 | 
			
		||||
    .flatMap((pack) => pack.getEmojis())
 | 
			
		||||
    .forEach((emoji) => {
 | 
			
		||||
      allEmoji.set(emoji.shortcode, emoji);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  return allEmoji;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getEmojiForCompletion(mx, rooms) {
 | 
			
		||||
  const allEmoji = new Map();
 | 
			
		||||
  getRelevantPacks(mx, rooms)
 | 
			
		||||
    .flatMap((pack) => pack.getEmojis())
 | 
			
		||||
    .forEach((emoji) => {
 | 
			
		||||
      allEmoji.set(emoji.shortcode, emoji);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  return Array.from(allEmoji.values()).concat(emojis.filter((e) => !allEmoji.has(e.shortcode)));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  ImagePack,
 | 
			
		||||
  getUserImagePack, getGlobalImagePacks, getRoomImagePacks,
 | 
			
		||||
  getShortcodeToEmoji, getShortcodeToCustomEmoji,
 | 
			
		||||
  getRelevantPacks, getEmojiForCompletion,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,69 +0,0 @@
 | 
			
		|||
import emojisData from 'emojibase-data/en/compact.json';
 | 
			
		||||
import joypixels from 'emojibase-data/en/shortcodes/joypixels.json';
 | 
			
		||||
import emojibase from 'emojibase-data/en/shortcodes/emojibase.json';
 | 
			
		||||
 | 
			
		||||
const emojiGroups = [{
 | 
			
		||||
  name: 'Smileys & people',
 | 
			
		||||
  order: 0,
 | 
			
		||||
  emojis: [],
 | 
			
		||||
}, {
 | 
			
		||||
  name: 'Animals & nature',
 | 
			
		||||
  order: 1,
 | 
			
		||||
  emojis: [],
 | 
			
		||||
}, {
 | 
			
		||||
  name: 'Food & drinks',
 | 
			
		||||
  order: 2,
 | 
			
		||||
  emojis: [],
 | 
			
		||||
}, {
 | 
			
		||||
  name: 'Activity',
 | 
			
		||||
  order: 3,
 | 
			
		||||
  emojis: [],
 | 
			
		||||
}, {
 | 
			
		||||
  name: 'Travel & places',
 | 
			
		||||
  order: 4,
 | 
			
		||||
  emojis: [],
 | 
			
		||||
}, {
 | 
			
		||||
  name: 'Objects',
 | 
			
		||||
  order: 5,
 | 
			
		||||
  emojis: [],
 | 
			
		||||
}, {
 | 
			
		||||
  name: 'Symbols',
 | 
			
		||||
  order: 6,
 | 
			
		||||
  emojis: [],
 | 
			
		||||
}, {
 | 
			
		||||
  name: 'Flags',
 | 
			
		||||
  order: 7,
 | 
			
		||||
  emojis: [],
 | 
			
		||||
}];
 | 
			
		||||
Object.freeze(emojiGroups);
 | 
			
		||||
 | 
			
		||||
function addEmoji(emoji, order) {
 | 
			
		||||
  emojiGroups[order].emojis.push(emoji);
 | 
			
		||||
}
 | 
			
		||||
function addToGroup(emoji) {
 | 
			
		||||
  if (emoji.group === 0 || emoji.group === 1) addEmoji(emoji, 0);
 | 
			
		||||
  else if (emoji.group === 3) addEmoji(emoji, 1);
 | 
			
		||||
  else if (emoji.group === 4) addEmoji(emoji, 2);
 | 
			
		||||
  else if (emoji.group === 6) addEmoji(emoji, 3);
 | 
			
		||||
  else if (emoji.group === 5) addEmoji(emoji, 4);
 | 
			
		||||
  else if (emoji.group === 7) addEmoji(emoji, 5);
 | 
			
		||||
  else if (emoji.group === 8 || typeof emoji.group === 'undefined') addEmoji(emoji, 6);
 | 
			
		||||
  else if (emoji.group === 9) addEmoji(emoji, 7);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const emojis = [];
 | 
			
		||||
emojisData.forEach((emoji) => {
 | 
			
		||||
  const myShortCodes = joypixels[emoji.hexcode] || emojibase[emoji.hexcode];
 | 
			
		||||
  if (!myShortCodes) return;
 | 
			
		||||
  const em = {
 | 
			
		||||
    ...emoji,
 | 
			
		||||
    shortcode: Array.isArray(myShortCodes) ? myShortCodes[0] : myShortCodes,
 | 
			
		||||
    shortcodes: myShortCodes,
 | 
			
		||||
  };
 | 
			
		||||
  addToGroup(em);
 | 
			
		||||
  emojis.push(em);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  emojis, emojiGroups,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,36 +0,0 @@
 | 
			
		|||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import { emojis } from './emoji';
 | 
			
		||||
 | 
			
		||||
const eventType = 'io.element.recent_emoji';
 | 
			
		||||
 | 
			
		||||
function getRecentEmojisRaw() {
 | 
			
		||||
  return initMatrix.matrixClient.getAccountData(eventType)?.getContent().recent_emoji ?? [];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getRecentEmojis(limit) {
 | 
			
		||||
  const res = [];
 | 
			
		||||
  getRecentEmojisRaw()
 | 
			
		||||
    .sort((a, b) => b[1] - a[1])
 | 
			
		||||
    .find(([unicode]) => {
 | 
			
		||||
      const emoji = emojis.find((e) => e.unicode === unicode);
 | 
			
		||||
      if (emoji) return res.push(emoji) >= limit;
 | 
			
		||||
      return false;
 | 
			
		||||
    });
 | 
			
		||||
  return res;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function addRecentEmoji(unicode) {
 | 
			
		||||
  const recent = getRecentEmojisRaw();
 | 
			
		||||
  const i = recent.findIndex(([u]) => u === unicode);
 | 
			
		||||
  let entry;
 | 
			
		||||
  if (i < 0) {
 | 
			
		||||
    entry = [unicode, 1];
 | 
			
		||||
  } else {
 | 
			
		||||
    [entry] = recent.splice(i, 1);
 | 
			
		||||
    entry[1] += 1;
 | 
			
		||||
  }
 | 
			
		||||
  recent.unshift(entry);
 | 
			
		||||
  initMatrix.matrixClient.setAccountData(eventType, {
 | 
			
		||||
    recent_emoji: recent.slice(0, 100),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,7 +2,6 @@
 | 
			
		|||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './EmojiVerification.scss';
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
| 
						 | 
				
			
			@ -30,8 +29,9 @@ function EmojiVerificationContent({ data, requestClose }) {
 | 
			
		|||
 | 
			
		||||
  const beginVerification = async () => {
 | 
			
		||||
    if (
 | 
			
		||||
      isCrossVerified(mx.deviceId)
 | 
			
		||||
      && (mx.getCrossSigningId() === null || await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing') === false)
 | 
			
		||||
      isCrossVerified(mx.deviceId) &&
 | 
			
		||||
      (mx.getCrossSigningId() === null ||
 | 
			
		||||
        (await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing')) === false)
 | 
			
		||||
    ) {
 | 
			
		||||
      if (!hasPrivateKey(getDefaultSSKey())) {
 | 
			
		||||
        const keyData = await accessSecretStorage('Emoji verification');
 | 
			
		||||
| 
						 | 
				
			
			@ -106,16 +106,20 @@ function EmojiVerificationContent({ data, requestClose }) {
 | 
			
		|||
          {sas.sas.emoji.map((emoji, i) => (
 | 
			
		||||
            // eslint-disable-next-line react/no-array-index-key
 | 
			
		||||
            <div className="emoji-verification__emoji-block" key={`${emoji[1]}-${i}`}>
 | 
			
		||||
              <Text variant="h1">{twemojify(emoji[0])}</Text>
 | 
			
		||||
              <Text variant="h1">{emoji[0]}</Text>
 | 
			
		||||
              <Text>{emoji[1]}</Text>
 | 
			
		||||
            </div>
 | 
			
		||||
          ))}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="emoji-verification__buttons">
 | 
			
		||||
          {process ? renderWait() : (
 | 
			
		||||
          {process ? (
 | 
			
		||||
            renderWait()
 | 
			
		||||
          ) : (
 | 
			
		||||
            <>
 | 
			
		||||
              <Button variant="primary" onClick={sasConfirm}>They match</Button>
 | 
			
		||||
              <Button onClick={sasMismatch}>{'They don\'t match'}</Button>
 | 
			
		||||
              <Button variant="primary" onClick={sasConfirm}>
 | 
			
		||||
                They match
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button onClick={sasMismatch}>No match</Button>
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -127,9 +131,7 @@ function EmojiVerificationContent({ data, requestClose }) {
 | 
			
		|||
    return (
 | 
			
		||||
      <div className="emoji-verification__content">
 | 
			
		||||
        <Text>Please accept the request from other device.</Text>
 | 
			
		||||
        <div className="emoji-verification__buttons">
 | 
			
		||||
          {renderWait()}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="emoji-verification__buttons">{renderWait()}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -138,11 +140,13 @@ function EmojiVerificationContent({ data, requestClose }) {
 | 
			
		|||
    <div className="emoji-verification__content">
 | 
			
		||||
      <Text>Click accept to start the verification process.</Text>
 | 
			
		||||
      <div className="emoji-verification__buttons">
 | 
			
		||||
        {
 | 
			
		||||
          process
 | 
			
		||||
            ? renderWait()
 | 
			
		||||
            : <Button variant="primary" onClick={beginVerification}>Accept</Button>
 | 
			
		||||
        }
 | 
			
		||||
        {process ? (
 | 
			
		||||
          renderWait()
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Button variant="primary" onClick={beginVerification}>
 | 
			
		||||
            Accept
 | 
			
		||||
          </Button>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			@ -180,19 +184,19 @@ function EmojiVerification() {
 | 
			
		|||
    <Dialog
 | 
			
		||||
      isOpen={data !== null}
 | 
			
		||||
      className="emoji-verification"
 | 
			
		||||
      title={(
 | 
			
		||||
      title={
 | 
			
		||||
        <Text variant="s1" weight="medium" primary>
 | 
			
		||||
          Emoji verification
 | 
			
		||||
        </Text>
 | 
			
		||||
      )}
 | 
			
		||||
      }
 | 
			
		||||
      contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
 | 
			
		||||
      onRequestClose={requestClose}
 | 
			
		||||
    >
 | 
			
		||||
      {
 | 
			
		||||
        data !== null
 | 
			
		||||
          ? <EmojiVerificationContent data={data} requestClose={requestClose} />
 | 
			
		||||
          : <div />
 | 
			
		||||
      }
 | 
			
		||||
      {data !== null ? (
 | 
			
		||||
        <EmojiVerificationContent data={data} requestClose={requestClose} />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <div />
 | 
			
		||||
      )}
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,145 +0,0 @@
 | 
			
		|||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './InviteList.scss';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import * as roomActions from '../../../client/action/room';
 | 
			
		||||
import { selectRoom, selectTab } from '../../../client/action/navigation';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import Button from '../../atoms/button/Button';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import Spinner from '../../atoms/spinner/Spinner';
 | 
			
		||||
import PopupWindow from '../../molecules/popup-window/PopupWindow';
 | 
			
		||||
import RoomTile from '../../molecules/room-tile/RoomTile';
 | 
			
		||||
 | 
			
		||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 | 
			
		||||
 | 
			
		||||
function InviteList({ isOpen, onRequestClose }) {
 | 
			
		||||
  const [procInvite, changeProcInvite] = useState(new Set());
 | 
			
		||||
 | 
			
		||||
  function acceptInvite(roomId, isDM) {
 | 
			
		||||
    procInvite.add(roomId);
 | 
			
		||||
    changeProcInvite(new Set(Array.from(procInvite)));
 | 
			
		||||
    roomActions.join(roomId, isDM);
 | 
			
		||||
  }
 | 
			
		||||
  function rejectInvite(roomId, isDM) {
 | 
			
		||||
    procInvite.add(roomId);
 | 
			
		||||
    changeProcInvite(new Set(Array.from(procInvite)));
 | 
			
		||||
    roomActions.leave(roomId, isDM);
 | 
			
		||||
  }
 | 
			
		||||
  function updateInviteList(roomId) {
 | 
			
		||||
    if (procInvite.has(roomId)) procInvite.delete(roomId);
 | 
			
		||||
    changeProcInvite(new Set(Array.from(procInvite)));
 | 
			
		||||
 | 
			
		||||
    const rl = initMatrix.roomList;
 | 
			
		||||
    const totalInvites = rl.inviteDirects.size + rl.inviteRooms.size + rl.inviteSpaces.size;
 | 
			
		||||
    const room = initMatrix.matrixClient.getRoom(roomId);
 | 
			
		||||
    const isRejected = room === null || room?.getMyMembership() !== 'join';
 | 
			
		||||
    if (!isRejected) {
 | 
			
		||||
      if (room.isSpaceRoom()) selectTab(roomId);
 | 
			
		||||
      else selectRoom(roomId);
 | 
			
		||||
      onRequestClose();
 | 
			
		||||
    }
 | 
			
		||||
    if (totalInvites === 0) onRequestClose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    initMatrix.roomList.on(cons.events.roomList.INVITELIST_UPDATED, updateInviteList);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      initMatrix.roomList.removeListener(cons.events.roomList.INVITELIST_UPDATED, updateInviteList);
 | 
			
		||||
    };
 | 
			
		||||
  }, [procInvite]);
 | 
			
		||||
 | 
			
		||||
  function renderRoomTile(roomId) {
 | 
			
		||||
    const mx = initMatrix.matrixClient;
 | 
			
		||||
    const myRoom = mx.getRoom(roomId);
 | 
			
		||||
    if (!myRoom) return null;
 | 
			
		||||
    const roomName = myRoom.name;
 | 
			
		||||
    let roomAlias = myRoom.getCanonicalAlias();
 | 
			
		||||
    if (!roomAlias) roomAlias = myRoom.roomId;
 | 
			
		||||
    const inviterName = myRoom.getMember(mx.getUserId())?.events?.member?.getSender?.() ?? '';
 | 
			
		||||
    return (
 | 
			
		||||
      <RoomTile
 | 
			
		||||
        key={myRoom.roomId}
 | 
			
		||||
        name={roomName}
 | 
			
		||||
        avatarSrc={initMatrix.matrixClient.getRoom(roomId).getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop')}
 | 
			
		||||
        id={roomAlias}
 | 
			
		||||
        inviterName={inviterName}
 | 
			
		||||
        options={
 | 
			
		||||
          procInvite.has(myRoom.roomId)
 | 
			
		||||
            ? (<Spinner size="small" />)
 | 
			
		||||
            : (
 | 
			
		||||
              <div className="invite-btn__container">
 | 
			
		||||
                <Button onClick={() => rejectInvite(myRoom.roomId)}>Reject</Button>
 | 
			
		||||
                <Button onClick={() => acceptInvite(myRoom.roomId)} variant="primary">Accept</Button>
 | 
			
		||||
              </div>
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <PopupWindow
 | 
			
		||||
      isOpen={isOpen}
 | 
			
		||||
      title="Invites"
 | 
			
		||||
      contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
 | 
			
		||||
      onRequestClose={onRequestClose}
 | 
			
		||||
    >
 | 
			
		||||
      <div className="invites-content">
 | 
			
		||||
        { initMatrix.roomList.inviteDirects.size !== 0 && (
 | 
			
		||||
          <div className="invites-content__subheading">
 | 
			
		||||
            <Text variant="b3" weight="bold">Direct Messages</Text>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        {
 | 
			
		||||
          Array.from(initMatrix.roomList.inviteDirects).map((roomId) => {
 | 
			
		||||
            const myRoom = initMatrix.matrixClient.getRoom(roomId);
 | 
			
		||||
            if (myRoom === null) return null;
 | 
			
		||||
            const roomName = myRoom.name;
 | 
			
		||||
            return (
 | 
			
		||||
              <RoomTile
 | 
			
		||||
                key={myRoom.roomId}
 | 
			
		||||
                name={roomName}
 | 
			
		||||
                id={myRoom.getDMInviter() || roomId}
 | 
			
		||||
                options={
 | 
			
		||||
                  procInvite.has(myRoom.roomId)
 | 
			
		||||
                    ? (<Spinner size="small" />)
 | 
			
		||||
                    : (
 | 
			
		||||
                      <div className="invite-btn__container">
 | 
			
		||||
                        <Button onClick={() => rejectInvite(myRoom.roomId, true)}>Reject</Button>
 | 
			
		||||
                        <Button onClick={() => acceptInvite(myRoom.roomId, true)} variant="primary">Accept</Button>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
            );
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
        { initMatrix.roomList.inviteSpaces.size !== 0 && (
 | 
			
		||||
          <div className="invites-content__subheading">
 | 
			
		||||
            <Text variant="b3" weight="bold">Spaces</Text>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        { Array.from(initMatrix.roomList.inviteSpaces).map(renderRoomTile) }
 | 
			
		||||
 | 
			
		||||
        { initMatrix.roomList.inviteRooms.size !== 0 && (
 | 
			
		||||
          <div className="invites-content__subheading">
 | 
			
		||||
            <Text variant="b3" weight="bold">Rooms</Text>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        { Array.from(initMatrix.roomList.inviteRooms).map(renderRoomTile) }
 | 
			
		||||
      </div>
 | 
			
		||||
    </PopupWindow>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
InviteList.propTypes = {
 | 
			
		||||
  isOpen: PropTypes.bool.isRequired,
 | 
			
		||||
  onRequestClose: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default InviteList;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,26 +0,0 @@
 | 
			
		|||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.invites-content {
 | 
			
		||||
  @include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
 | 
			
		||||
 | 
			
		||||
  &__subheading {
 | 
			
		||||
    margin-top: var(--sp-extra-loose);
 | 
			
		||||
 | 
			
		||||
    & .text {
 | 
			
		||||
      text-transform: uppercase;
 | 
			
		||||
    }
 | 
			
		||||
    &:first-child {
 | 
			
		||||
      margin-top: var(--sp-tight);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & .room-tile {
 | 
			
		||||
    margin-top: var(--sp-normal);
 | 
			
		||||
    &__options {
 | 
			
		||||
      align-self: flex-end;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  & .invite-btn__container .btn-surface {
 | 
			
		||||
    @include dir.side(margin, 0, var(--sp-normal));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,10 +3,8 @@ import PropTypes from 'prop-types';
 | 
			
		|||
import './InviteUser.scss';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import * as roomActions from '../../../client/action/room';
 | 
			
		||||
import { selectRoom } from '../../../client/action/navigation';
 | 
			
		||||
import { hasDMWith, hasDevices } from '../../../util/matrixUtil';
 | 
			
		||||
import { hasDevices } from '../../../util/matrixUtil';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import Button from '../../atoms/button/Button';
 | 
			
		||||
| 
						 | 
				
			
			@ -18,10 +16,10 @@ import RoomTile from '../../molecules/room-tile/RoomTile';
 | 
			
		|||
 | 
			
		||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 | 
			
		||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
 | 
			
		||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
 | 
			
		||||
import { getDMRoomFor } from '../../utils/matrix';
 | 
			
		||||
 | 
			
		||||
function InviteUser({
 | 
			
		||||
  isOpen, roomId, searchTerm, onRequestClose,
 | 
			
		||||
}) {
 | 
			
		||||
function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) {
 | 
			
		||||
  const [isSearching, updateIsSearching] = useState(false);
 | 
			
		||||
  const [searchQuery, updateSearchQuery] = useState({});
 | 
			
		||||
  const [users, updateUsers] = useState([]);
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +35,7 @@ function InviteUser({
 | 
			
		|||
  const usernameRef = useRef(null);
 | 
			
		||||
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const { navigateRoom } = useRoomNavigate();
 | 
			
		||||
 | 
			
		||||
  function getMapCopy(myMap) {
 | 
			
		||||
    const newMap = new Map();
 | 
			
		||||
| 
						 | 
				
			
			@ -76,11 +75,13 @@ function InviteUser({
 | 
			
		|||
    if (isInputUserId) {
 | 
			
		||||
      try {
 | 
			
		||||
        const result = await mx.getProfileInfo(inputUsername);
 | 
			
		||||
        updateUsers([{
 | 
			
		||||
          user_id: inputUsername,
 | 
			
		||||
          display_name: result.displayname,
 | 
			
		||||
          avatar_url: result.avatar_url,
 | 
			
		||||
        }]);
 | 
			
		||||
        updateUsers([
 | 
			
		||||
          {
 | 
			
		||||
            user_id: inputUsername,
 | 
			
		||||
            display_name: result.displayname,
 | 
			
		||||
            avatar_url: result.avatar_url,
 | 
			
		||||
          },
 | 
			
		||||
        ]);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        updateSearchQuery({ error: `${inputUsername} not found!` });
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -105,9 +106,9 @@ function InviteUser({
 | 
			
		|||
 | 
			
		||||
  async function createDM(userId) {
 | 
			
		||||
    if (mx.getUserId() === userId) return;
 | 
			
		||||
    const dmRoomId = hasDMWith(userId);
 | 
			
		||||
    const dmRoomId = getDMRoomFor(mx, userId)?.roomId;
 | 
			
		||||
    if (dmRoomId) {
 | 
			
		||||
      selectRoom(dmRoomId);
 | 
			
		||||
      navigateRoom(dmRoomId);
 | 
			
		||||
      onRequestClose();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -120,6 +121,7 @@ function InviteUser({
 | 
			
		|||
      const result = await roomActions.createDM(userId, await hasDevices(userId));
 | 
			
		||||
      roomIdToUserId.set(result.room_id, userId);
 | 
			
		||||
      updateRoomIdToUserId(getMapCopy(roomIdToUserId));
 | 
			
		||||
      onDMCreated(result.room_id);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      deleteUserFromProc(userId);
 | 
			
		||||
      if (typeof e.message === 'string') procUserError.set(userId, e.message);
 | 
			
		||||
| 
						 | 
				
			
			@ -150,7 +152,13 @@ function InviteUser({
 | 
			
		|||
 | 
			
		||||
  function renderUserList() {
 | 
			
		||||
    const renderOptions = (userId) => {
 | 
			
		||||
      const messageJSX = (message, isPositive) => <Text variant="b2"><span style={{ color: isPositive ? 'var(--bg-positive)' : 'var(--bg-negative)' }}>{message}</span></Text>;
 | 
			
		||||
      const messageJSX = (message, isPositive) => (
 | 
			
		||||
        <Text variant="b2">
 | 
			
		||||
          <span style={{ color: isPositive ? 'var(--bg-positive)' : 'var(--bg-negative)' }}>
 | 
			
		||||
            {message}
 | 
			
		||||
          </span>
 | 
			
		||||
        </Text>
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (mx.getUserId() === userId) return null;
 | 
			
		||||
      if (procUsers.has(userId)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -158,7 +166,16 @@ function InviteUser({
 | 
			
		|||
      }
 | 
			
		||||
      if (createdDM.has(userId)) {
 | 
			
		||||
        // eslint-disable-next-line max-len
 | 
			
		||||
        return <Button onClick={() => { selectRoom(createdDM.get(userId)); onRequestClose(); }}>Open</Button>;
 | 
			
		||||
        return (
 | 
			
		||||
          <Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              navigateRoom(createdDM.get(userId));
 | 
			
		||||
              onRequestClose();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            Open
 | 
			
		||||
          </Button>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      if (invitedUserIds.has(userId)) {
 | 
			
		||||
        return messageJSX('Invited', true);
 | 
			
		||||
| 
						 | 
				
			
			@ -178,13 +195,23 @@ function InviteUser({
 | 
			
		|||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return (typeof roomId === 'string')
 | 
			
		||||
        ? <Button onClick={() => inviteToRoom(userId)} variant="primary">Invite</Button>
 | 
			
		||||
        : <Button onClick={() => createDM(userId)} variant="primary">Message</Button>;
 | 
			
		||||
      return typeof roomId === 'string' ? (
 | 
			
		||||
        <Button onClick={() => inviteToRoom(userId)} variant="primary">
 | 
			
		||||
          Invite
 | 
			
		||||
        </Button>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <Button onClick={() => createDM(userId)} variant="primary">
 | 
			
		||||
          Message
 | 
			
		||||
        </Button>
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
    const renderError = (userId) => {
 | 
			
		||||
      if (!procUserError.has(userId)) return null;
 | 
			
		||||
      return <Text variant="b2"><span style={{ color: 'var(--bg-danger)' }}>{procUserError.get(userId)}</span></Text>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Text variant="b2">
 | 
			
		||||
          <span style={{ color: 'var(--bg-danger)' }}>{procUserError.get(userId)}</span>
 | 
			
		||||
        </Text>
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return users.map((user) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -193,7 +220,11 @@ function InviteUser({
 | 
			
		|||
      return (
 | 
			
		||||
        <RoomTile
 | 
			
		||||
          key={userId}
 | 
			
		||||
          avatarSrc={typeof user.avatar_url === 'string' ? mx.mxcUrlToHttp(user.avatar_url, 42, 42, 'crop') : null}
 | 
			
		||||
          avatarSrc={
 | 
			
		||||
            typeof user.avatar_url === 'string'
 | 
			
		||||
              ? mx.mxcUrlToHttp(user.avatar_url, 42, 42, 'crop')
 | 
			
		||||
              : null
 | 
			
		||||
          }
 | 
			
		||||
          name={name}
 | 
			
		||||
          id={userId}
 | 
			
		||||
          options={renderOptions(userId)}
 | 
			
		||||
| 
						 | 
				
			
			@ -217,48 +248,43 @@ function InviteUser({
 | 
			
		|||
    };
 | 
			
		||||
  }, [isOpen, searchTerm]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    initMatrix.roomList.on(cons.events.roomList.ROOM_CREATED, onDMCreated);
 | 
			
		||||
    return () => {
 | 
			
		||||
      initMatrix.roomList.removeListener(cons.events.roomList.ROOM_CREATED, onDMCreated);
 | 
			
		||||
    };
 | 
			
		||||
  }, [isOpen, procUsers, createdDM, roomIdToUserId]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <PopupWindow
 | 
			
		||||
      isOpen={isOpen}
 | 
			
		||||
      title={(typeof roomId === 'string' ? `Invite to ${mx.getRoom(roomId).name}` : 'Direct message')}
 | 
			
		||||
      title={typeof roomId === 'string' ? `Invite to ${mx.getRoom(roomId).name}` : 'Direct message'}
 | 
			
		||||
      contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
 | 
			
		||||
      onRequestClose={onRequestClose}
 | 
			
		||||
    >
 | 
			
		||||
      <div className="invite-user">
 | 
			
		||||
        <form className="invite-user__form" onSubmit={(e) => { e.preventDefault(); searchUser(usernameRef.current.value); }}>
 | 
			
		||||
        <form
 | 
			
		||||
          className="invite-user__form"
 | 
			
		||||
          onSubmit={(e) => {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            searchUser(usernameRef.current.value);
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Input value={searchTerm} forwardRef={usernameRef} label="Name or userId" />
 | 
			
		||||
          <Button disabled={isSearching} iconSrc={UserIC} variant="primary" type="submit">Search</Button>
 | 
			
		||||
          <Button disabled={isSearching} iconSrc={UserIC} variant="primary" type="submit">
 | 
			
		||||
            Search
 | 
			
		||||
          </Button>
 | 
			
		||||
        </form>
 | 
			
		||||
        <div className="invite-user__search-status">
 | 
			
		||||
          {
 | 
			
		||||
            typeof searchQuery.username !== 'undefined' && isSearching && (
 | 
			
		||||
              <div className="flex--center">
 | 
			
		||||
                <Spinner size="small" />
 | 
			
		||||
                <Text variant="b2">{`Searching for user "${searchQuery.username}"...`}</Text>
 | 
			
		||||
              </div>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            typeof searchQuery.username !== 'undefined' && !isSearching && (
 | 
			
		||||
              <Text variant="b2">{`Search result for user "${searchQuery.username}"`}</Text>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            searchQuery.error && <Text className="invite-user__search-error" variant="b2">{searchQuery.error}</Text>
 | 
			
		||||
          }
 | 
			
		||||
          {typeof searchQuery.username !== 'undefined' && isSearching && (
 | 
			
		||||
            <div className="flex--center">
 | 
			
		||||
              <Spinner size="small" />
 | 
			
		||||
              <Text variant="b2">{`Searching for user "${searchQuery.username}"...`}</Text>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
          {typeof searchQuery.username !== 'undefined' && !isSearching && (
 | 
			
		||||
            <Text variant="b2">{`Search result for user "${searchQuery.username}"`}</Text>
 | 
			
		||||
          )}
 | 
			
		||||
          {searchQuery.error && (
 | 
			
		||||
            <Text className="invite-user__search-error" variant="b2">
 | 
			
		||||
              {searchQuery.error}
 | 
			
		||||
            </Text>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
        { users.length !== 0 && (
 | 
			
		||||
          <div className="invite-user__content">
 | 
			
		||||
            {renderUserList()}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        {users.length !== 0 && <div className="invite-user__content">{renderUserList()}</div>}
 | 
			
		||||
      </div>
 | 
			
		||||
    </PopupWindow>
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,6 @@ import initMatrix from '../../../client/initMatrix';
 | 
			
		|||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import { join } from '../../../client/action/room';
 | 
			
		||||
import { selectRoom, selectTab } from '../../../client/action/navigation';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
| 
						 | 
				
			
			@ -18,36 +17,24 @@ import Dialog from '../../molecules/dialog/Dialog';
 | 
			
		|||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 | 
			
		||||
 | 
			
		||||
import { useStore } from '../../hooks/useStore';
 | 
			
		||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
 | 
			
		||||
 | 
			
		||||
const ALIAS_OR_ID_REG = /^[#|!].+:.+\..+$/;
 | 
			
		||||
 | 
			
		||||
function JoinAliasContent({ term, requestClose }) {
 | 
			
		||||
  const [process, setProcess] = useState(false);
 | 
			
		||||
  const [error, setError] = useState(undefined);
 | 
			
		||||
  const [lastJoinId, setLastJoinId] = useState(undefined);
 | 
			
		||||
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const mountStore = useStore();
 | 
			
		||||
 | 
			
		||||
  const { navigateRoom } = useRoomNavigate();
 | 
			
		||||
 | 
			
		||||
  const openRoom = (roomId) => {
 | 
			
		||||
    const room = mx.getRoom(roomId);
 | 
			
		||||
    if (!room) return;
 | 
			
		||||
    if (room.isSpaceRoom()) selectTab(roomId);
 | 
			
		||||
    else selectRoom(roomId);
 | 
			
		||||
    navigateRoom(roomId);
 | 
			
		||||
    requestClose();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleJoin = (roomId) => {
 | 
			
		||||
      if (lastJoinId !== roomId) return;
 | 
			
		||||
      openRoom(roomId);
 | 
			
		||||
    };
 | 
			
		||||
    initMatrix.roomList.on(cons.events.roomList.ROOM_JOINED, handleJoin);
 | 
			
		||||
    return () => {
 | 
			
		||||
      initMatrix.roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleJoin);
 | 
			
		||||
    };
 | 
			
		||||
  }, [lastJoinId]);
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async (e) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    mountStore.setItem(true);
 | 
			
		||||
| 
						 | 
				
			
			@ -70,13 +57,14 @@ function JoinAliasContent({ term, requestClose }) {
 | 
			
		|||
      } catch (err) {
 | 
			
		||||
        if (!mountStore.getItem()) return;
 | 
			
		||||
        setProcess(false);
 | 
			
		||||
        setError(`Unable to find room/space with ${alias}. Either room/space is private or doesn't exist.`);
 | 
			
		||||
        setError(
 | 
			
		||||
          `Unable to find room/space with ${alias}. Either room/space is private or doesn't exist.`
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const roomId = await join(alias, false, via);
 | 
			
		||||
      if (!mountStore.getItem()) return;
 | 
			
		||||
      setLastJoinId(roomId);
 | 
			
		||||
      openRoom(roomId);
 | 
			
		||||
    } catch {
 | 
			
		||||
      if (!mountStore.getItem()) return;
 | 
			
		||||
| 
						 | 
				
			
			@ -87,24 +75,23 @@ function JoinAliasContent({ term, requestClose }) {
 | 
			
		|||
 | 
			
		||||
  return (
 | 
			
		||||
    <form className="join-alias" onSubmit={handleSubmit}>
 | 
			
		||||
      <Input
 | 
			
		||||
        label="Address"
 | 
			
		||||
        value={term}
 | 
			
		||||
        name="alias"
 | 
			
		||||
        required
 | 
			
		||||
      />
 | 
			
		||||
      {error && <Text className="join-alias__error" variant="b3">{error}</Text>}
 | 
			
		||||
      <Input label="Address" value={term} name="alias" required />
 | 
			
		||||
      {error && (
 | 
			
		||||
        <Text className="join-alias__error" variant="b3">
 | 
			
		||||
          {error}
 | 
			
		||||
        </Text>
 | 
			
		||||
      )}
 | 
			
		||||
      <div className="join-alias__btn">
 | 
			
		||||
        {
 | 
			
		||||
          process
 | 
			
		||||
            ? (
 | 
			
		||||
              <>
 | 
			
		||||
                <Spinner size="small" />
 | 
			
		||||
                <Text>{process}</Text>
 | 
			
		||||
              </>
 | 
			
		||||
            )
 | 
			
		||||
            : <Button variant="primary" type="submit">Join</Button>
 | 
			
		||||
        }
 | 
			
		||||
        {process ? (
 | 
			
		||||
          <>
 | 
			
		||||
            <Spinner size="small" />
 | 
			
		||||
            <Text>{process}</Text>
 | 
			
		||||
          </>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Button variant="primary" type="submit">
 | 
			
		||||
            Join
 | 
			
		||||
          </Button>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			@ -141,13 +128,15 @@ function JoinAlias() {
 | 
			
		|||
  return (
 | 
			
		||||
    <Dialog
 | 
			
		||||
      isOpen={data !== null}
 | 
			
		||||
      title={(
 | 
			
		||||
        <Text variant="s1" weight="medium" primary>Join with address</Text>
 | 
			
		||||
      )}
 | 
			
		||||
      title={
 | 
			
		||||
        <Text variant="s1" weight="medium" primary>
 | 
			
		||||
          Join with address
 | 
			
		||||
        </Text>
 | 
			
		||||
      }
 | 
			
		||||
      contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
 | 
			
		||||
      onRequestClose={requestClose}
 | 
			
		||||
    >
 | 
			
		||||
      { data ? <JoinAliasContent term={data.term} requestClose={requestClose} /> : <div /> }
 | 
			
		||||
      {data ? <JoinAliasContent term={data.term} requestClose={requestClose} /> : <div />}
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,71 +0,0 @@
 | 
			
		|||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import Postie from '../../../util/Postie';
 | 
			
		||||
import { roomIdByActivity } from '../../../util/sort';
 | 
			
		||||
 | 
			
		||||
import RoomsCategory from './RoomsCategory';
 | 
			
		||||
 | 
			
		||||
const drawerPostie = new Postie();
 | 
			
		||||
function Directs({ size }) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const { roomList, notifications } = initMatrix;
 | 
			
		||||
  const [directIds, setDirectIds] = useState([]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => setDirectIds([...roomList.directs].sort(roomIdByActivity)), [size]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleTimeline = (event, room, toStartOfTimeline, removed, data) => {
 | 
			
		||||
      if (!roomList.directs.has(room.roomId)) return;
 | 
			
		||||
      if (!data.liveEvent) return;
 | 
			
		||||
      if (directIds[0] === room.roomId) return;
 | 
			
		||||
      const newDirectIds = [room.roomId];
 | 
			
		||||
      directIds.forEach((id) => {
 | 
			
		||||
        if (id === room.roomId) return;
 | 
			
		||||
        newDirectIds.push(id);
 | 
			
		||||
      });
 | 
			
		||||
      setDirectIds(newDirectIds);
 | 
			
		||||
    };
 | 
			
		||||
    mx.on('Room.timeline', handleTimeline);
 | 
			
		||||
    return () => {
 | 
			
		||||
      mx.removeListener('Room.timeline', handleTimeline);
 | 
			
		||||
    };
 | 
			
		||||
  }, [directIds]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const selectorChanged = (selectedRoomId, prevSelectedRoomId) => {
 | 
			
		||||
      if (!drawerPostie.hasTopic('selector-change')) return;
 | 
			
		||||
      const addresses = [];
 | 
			
		||||
      if (drawerPostie.hasSubscriber('selector-change', selectedRoomId)) addresses.push(selectedRoomId);
 | 
			
		||||
      if (drawerPostie.hasSubscriber('selector-change', prevSelectedRoomId)) addresses.push(prevSelectedRoomId);
 | 
			
		||||
      if (addresses.length === 0) return;
 | 
			
		||||
      drawerPostie.post('selector-change', addresses, selectedRoomId);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const notiChanged = (roomId, total, prevTotal) => {
 | 
			
		||||
      if (total === prevTotal) return;
 | 
			
		||||
      if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) {
 | 
			
		||||
        drawerPostie.post('unread-change', roomId);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
 | 
			
		||||
    notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
 | 
			
		||||
    notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged);
 | 
			
		||||
    return () => {
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
 | 
			
		||||
      notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
 | 
			
		||||
      notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return <RoomsCategory name="People" hideHeader roomIds={directIds} drawerPostie={drawerPostie} />;
 | 
			
		||||
}
 | 
			
		||||
Directs.propTypes = {
 | 
			
		||||
  size: PropTypes.number.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Directs;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,93 +0,0 @@
 | 
			
		|||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import './Drawer.scss';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import ScrollView from '../../atoms/scroll/ScrollView';
 | 
			
		||||
 | 
			
		||||
import DrawerHeader from './DrawerHeader';
 | 
			
		||||
import DrawerBreadcrumb from './DrawerBreadcrumb';
 | 
			
		||||
import Home from './Home';
 | 
			
		||||
import Directs from './Directs';
 | 
			
		||||
 | 
			
		||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
 | 
			
		||||
import { useSelectedTab } from '../../hooks/useSelectedTab';
 | 
			
		||||
import { useSelectedSpace } from '../../hooks/useSelectedSpace';
 | 
			
		||||
 | 
			
		||||
function useSystemState() {
 | 
			
		||||
  const [systemState, setSystemState] = useState(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleSystemState = (state) => {
 | 
			
		||||
      if (state === 'ERROR' || state === 'RECONNECTING' || state === 'STOPPED') {
 | 
			
		||||
        setSystemState({ status: 'Connection lost!' });
 | 
			
		||||
      }
 | 
			
		||||
      if (systemState !== null) setSystemState(null);
 | 
			
		||||
    };
 | 
			
		||||
    initMatrix.matrixClient.on('sync', handleSystemState);
 | 
			
		||||
    return () => {
 | 
			
		||||
      initMatrix.matrixClient.removeListener('sync', handleSystemState);
 | 
			
		||||
    };
 | 
			
		||||
  }, [systemState]);
 | 
			
		||||
 | 
			
		||||
  return [systemState];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Drawer() {
 | 
			
		||||
  const [systemState] = useSystemState();
 | 
			
		||||
  const [selectedTab] = useSelectedTab();
 | 
			
		||||
  const [spaceId] = useSelectedSpace();
 | 
			
		||||
  const [, forceUpdate] = useForceUpdate();
 | 
			
		||||
  const scrollRef = useRef(null);
 | 
			
		||||
  const { roomList } = initMatrix;
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleUpdate = () => {
 | 
			
		||||
      forceUpdate();
 | 
			
		||||
    };
 | 
			
		||||
    roomList.on(cons.events.roomList.ROOMLIST_UPDATED, handleUpdate);
 | 
			
		||||
    return () => {
 | 
			
		||||
      roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, handleUpdate);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    requestAnimationFrame(() => {
 | 
			
		||||
      if (scrollRef.current) {
 | 
			
		||||
        scrollRef.current.scrollTop = 0;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }, [selectedTab]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="drawer">
 | 
			
		||||
      <DrawerHeader selectedTab={selectedTab} spaceId={spaceId} />
 | 
			
		||||
      <div className="drawer__content-wrapper">
 | 
			
		||||
        {navigation.selectedSpacePath.length > 1 && selectedTab !== cons.tabs.DIRECTS && (
 | 
			
		||||
          <DrawerBreadcrumb spaceId={spaceId} />
 | 
			
		||||
        )}
 | 
			
		||||
        <div className="rooms__wrapper">
 | 
			
		||||
          <ScrollView ref={scrollRef} autoHide>
 | 
			
		||||
            <div className="rooms-container">
 | 
			
		||||
              {selectedTab !== cons.tabs.DIRECTS ? (
 | 
			
		||||
                <Home spaceId={spaceId} />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <Directs size={roomList.directs.size} />
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          </ScrollView>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      {systemState !== null && (
 | 
			
		||||
        <div className="drawer__state">
 | 
			
		||||
          <Text>{systemState.status}</Text>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Drawer;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,56 +0,0 @@
 | 
			
		|||
@use '../../partials/flex';
 | 
			
		||||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.drawer {
 | 
			
		||||
  @extend .cp-fx__column;
 | 
			
		||||
  @extend .cp-fx__item-one;
 | 
			
		||||
  min-width: 0;
 | 
			
		||||
  @include dir.side(border,
 | 
			
		||||
    none,
 | 
			
		||||
    1px solid var(--bg-surface-border),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  & .header {
 | 
			
		||||
    padding: var(--sp-extra-tight);
 | 
			
		||||
    & > .header__title-wrapper {
 | 
			
		||||
      @include dir.side(margin, var(--sp-ultra-tight), 0);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__content-wrapper {
 | 
			
		||||
    @extend .cp-fx__item-one;
 | 
			
		||||
    @extend .cp-fx__column;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__state {
 | 
			
		||||
    padding: var(--sp-extra-tight);
 | 
			
		||||
    border-top: 1px solid var(--bg-surface-border);
 | 
			
		||||
    @extend .cp-fx__row--c-c;
 | 
			
		||||
 | 
			
		||||
    & .text {
 | 
			
		||||
      color: var(--tc-danger-high);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.rooms__wrapper {
 | 
			
		||||
  @extend .cp-fx__item-one;
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.rooms-container {
 | 
			
		||||
  padding-bottom: var(--sp-extra-loose);
 | 
			
		||||
 | 
			
		||||
  &::before {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    z-index: 99;
 | 
			
		||||
    content: '';
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 8px;
 | 
			
		||||
    background-image: linear-gradient(
 | 
			
		||||
      to bottom,
 | 
			
		||||
      var(--bg-surface-low),
 | 
			
		||||
      var(--bg-surface-low-transparent));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,142 +0,0 @@
 | 
			
		|||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './DrawerBreadcrumb.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import { selectTab, selectSpace } from '../../../client/action/navigation';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import { abbreviateNumber } from '../../../util/common';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import RawIcon from '../../atoms/system-icons/RawIcon';
 | 
			
		||||
import Button from '../../atoms/button/Button';
 | 
			
		||||
import ScrollView from '../../atoms/scroll/ScrollView';
 | 
			
		||||
import NotificationBadge from '../../atoms/badge/NotificationBadge';
 | 
			
		||||
 | 
			
		||||
import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg';
 | 
			
		||||
 | 
			
		||||
function DrawerBreadcrumb({ spaceId }) {
 | 
			
		||||
  const [, forceUpdate] = useState({});
 | 
			
		||||
  const scrollRef = useRef(null);
 | 
			
		||||
  const { roomList, notifications, accountData } = initMatrix;
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const spacePath = navigation.selectedSpacePath;
 | 
			
		||||
 | 
			
		||||
  function onNotiChanged(roomId, total, prevTotal) {
 | 
			
		||||
    if (total === prevTotal) return;
 | 
			
		||||
    if (navigation.selectedSpacePath.includes(roomId)) {
 | 
			
		||||
      forceUpdate({});
 | 
			
		||||
    }
 | 
			
		||||
    if (navigation.selectedSpacePath[0] === cons.tabs.HOME) {
 | 
			
		||||
      if (!roomList.isOrphan(roomId)) return;
 | 
			
		||||
      if (roomList.directs.has(roomId)) return;
 | 
			
		||||
      forceUpdate({});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    requestAnimationFrame(() => {
 | 
			
		||||
      if (scrollRef?.current === null) return;
 | 
			
		||||
      scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;
 | 
			
		||||
    });
 | 
			
		||||
    notifications.on(cons.events.notifications.NOTI_CHANGED, onNotiChanged);
 | 
			
		||||
    return () => {
 | 
			
		||||
      notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotiChanged);
 | 
			
		||||
    };
 | 
			
		||||
  }, [spaceId]);
 | 
			
		||||
 | 
			
		||||
  function getHomeNotiExcept(childId) {
 | 
			
		||||
    const orphans = roomList.getOrphans()
 | 
			
		||||
      .filter((id) => (id !== childId))
 | 
			
		||||
      .filter((id) => !accountData.spaceShortcut.has(id));
 | 
			
		||||
 | 
			
		||||
    let noti = null;
 | 
			
		||||
 | 
			
		||||
    orphans.forEach((roomId) => {
 | 
			
		||||
      if (!notifications.hasNoti(roomId)) return;
 | 
			
		||||
      if (noti === null) noti = { total: 0, highlight: 0 };
 | 
			
		||||
      const childNoti = notifications.getNoti(roomId);
 | 
			
		||||
      noti.total += childNoti.total;
 | 
			
		||||
      noti.highlight += childNoti.highlight;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return noti;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getNotiExcept(roomId, childId) {
 | 
			
		||||
    if (!notifications.hasNoti(roomId)) return null;
 | 
			
		||||
 | 
			
		||||
    const noti = notifications.getNoti(roomId);
 | 
			
		||||
    if (!notifications.hasNoti(childId)) return noti;
 | 
			
		||||
    if (noti.from === null) return noti;
 | 
			
		||||
 | 
			
		||||
    const childNoti = notifications.getNoti(childId);
 | 
			
		||||
 | 
			
		||||
    let noOther = true;
 | 
			
		||||
    let total = 0;
 | 
			
		||||
    let highlight = 0;
 | 
			
		||||
    noti.from.forEach((fromId) => {
 | 
			
		||||
      if (childNoti.from.has(fromId)) return;
 | 
			
		||||
      noOther = false;
 | 
			
		||||
      const fromNoti = notifications.getNoti(fromId);
 | 
			
		||||
      total += fromNoti.total;
 | 
			
		||||
      highlight += fromNoti.highlight;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (noOther) return null;
 | 
			
		||||
    return { total, highlight };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="drawer-breadcrumb__wrapper">
 | 
			
		||||
      <ScrollView ref={scrollRef} horizontal vertical={false} invisible>
 | 
			
		||||
        <div className="drawer-breadcrumb">
 | 
			
		||||
          {
 | 
			
		||||
            spacePath.map((id, index) => {
 | 
			
		||||
              const noti = (id !== cons.tabs.HOME && index < spacePath.length)
 | 
			
		||||
                ? getNotiExcept(id, (index === spacePath.length - 1) ? null : spacePath[index + 1])
 | 
			
		||||
                : getHomeNotiExcept((index === spacePath.length - 1) ? null : spacePath[index + 1]);
 | 
			
		||||
 | 
			
		||||
              return (
 | 
			
		||||
                <React.Fragment
 | 
			
		||||
                  key={id}
 | 
			
		||||
                >
 | 
			
		||||
                  { index !== 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
 | 
			
		||||
                  <Button
 | 
			
		||||
                    className={index === spacePath.length - 1 ? 'drawer-breadcrumb__btn--selected' : ''}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      if (id === cons.tabs.HOME) selectTab(id);
 | 
			
		||||
                      else selectSpace(id);
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Text variant="b2">{id === cons.tabs.HOME ? 'Home' : twemojify(mx.getRoom(id).name)}</Text>
 | 
			
		||||
                    { noti !== null && (
 | 
			
		||||
                      <NotificationBadge
 | 
			
		||||
                        alert={noti.highlight !== 0}
 | 
			
		||||
                        content={noti.total > 0 ? abbreviateNumber(noti.total) : null}
 | 
			
		||||
                      />
 | 
			
		||||
                    )}
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </React.Fragment>
 | 
			
		||||
              );
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
          <div style={{ width: 'var(--sp-extra-tight)', height: '100%' }} />
 | 
			
		||||
        </div>
 | 
			
		||||
      </ScrollView>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DrawerBreadcrumb.defaultProps = {
 | 
			
		||||
  spaceId: null,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
DrawerBreadcrumb.propTypes = {
 | 
			
		||||
  spaceId: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DrawerBreadcrumb;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,66 +0,0 @@
 | 
			
		|||
@use '../../partials/text';
 | 
			
		||||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.drawer-breadcrumb__wrapper {
 | 
			
		||||
  height: var(--header-height);
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.drawer-breadcrumb {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  margin: 0 var(--sp-extra-tight);
 | 
			
		||||
 | 
			
		||||
  &::before,
 | 
			
		||||
  &::after {
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    z-index: 99;
 | 
			
		||||
 | 
			
		||||
    content: '';
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    min-width: 8px;
 | 
			
		||||
    width: 8px;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    background-image: linear-gradient(
 | 
			
		||||
      to right,
 | 
			
		||||
      var(--bg-surface-low-transparent),
 | 
			
		||||
      var(--bg-surface-low)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  &::before {
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: unset;
 | 
			
		||||
    background-image: linear-gradient(
 | 
			
		||||
      to left,
 | 
			
		||||
      var(--bg-surface-low-transparent),
 | 
			
		||||
      var(--bg-surface-low)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & > * {
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & > .btn-surface {
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
    padding: var(--sp-extra-tight) 10px;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    box-shadow: none;
 | 
			
		||||
    & p {
 | 
			
		||||
      @extend .cp-txt__ellipsis;
 | 
			
		||||
      max-width: 86px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & .notification-badge {
 | 
			
		||||
      @include dir.side(margin, var(--sp-extra-tight), 0);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__btn--selected {
 | 
			
		||||
    box-shadow: var(--bs-surface-border) !important;
 | 
			
		||||
    background-color: var(--bg-surface);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,159 +0,0 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './DrawerHeader.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import {
 | 
			
		||||
  openPublicRooms, openCreateRoom, openSpaceManage, openJoinAlias,
 | 
			
		||||
  openSpaceAddExisting, openInviteUser, openReusableContextMenu,
 | 
			
		||||
} from '../../../client/action/navigation';
 | 
			
		||||
import { getEventCords } from '../../../util/common';
 | 
			
		||||
 | 
			
		||||
import { blurOnBubbling } from '../../atoms/button/script';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import RawIcon from '../../atoms/system-icons/RawIcon';
 | 
			
		||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu';
 | 
			
		||||
import SpaceOptions from '../../molecules/space-options/SpaceOptions';
 | 
			
		||||
 | 
			
		||||
import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
 | 
			
		||||
import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg';
 | 
			
		||||
import HashGlobeIC from '../../../../public/res/ic/outlined/hash-globe.svg';
 | 
			
		||||
import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg';
 | 
			
		||||
import SpacePlusIC from '../../../../public/res/ic/outlined/space-plus.svg';
 | 
			
		||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
 | 
			
		||||
 | 
			
		||||
export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const room = mx.getRoom(spaceId);
 | 
			
		||||
  const canManage = room
 | 
			
		||||
    ? room.currentState.maySendStateEvent('m.space.child', mx.getUserId())
 | 
			
		||||
    : true;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <MenuHeader>Add rooms or spaces</MenuHeader>
 | 
			
		||||
      <MenuItem
 | 
			
		||||
        iconSrc={SpacePlusIC}
 | 
			
		||||
        onClick={() => { afterOptionSelect(); openCreateRoom(true, spaceId); }}
 | 
			
		||||
        disabled={!canManage}
 | 
			
		||||
      >
 | 
			
		||||
        Create new space
 | 
			
		||||
      </MenuItem>
 | 
			
		||||
      <MenuItem
 | 
			
		||||
        iconSrc={HashPlusIC}
 | 
			
		||||
        onClick={() => { afterOptionSelect(); openCreateRoom(false, spaceId); }}
 | 
			
		||||
        disabled={!canManage}
 | 
			
		||||
      >
 | 
			
		||||
        Create new room
 | 
			
		||||
      </MenuItem>
 | 
			
		||||
      { !spaceId && (
 | 
			
		||||
        <MenuItem
 | 
			
		||||
          iconSrc={HashGlobeIC}
 | 
			
		||||
          onClick={() => { afterOptionSelect(); openPublicRooms(); }}
 | 
			
		||||
        >
 | 
			
		||||
          Explore public rooms
 | 
			
		||||
        </MenuItem>
 | 
			
		||||
      )}
 | 
			
		||||
      { !spaceId && (
 | 
			
		||||
        <MenuItem
 | 
			
		||||
          iconSrc={PlusIC}
 | 
			
		||||
          onClick={() => { afterOptionSelect(); openJoinAlias(); }}
 | 
			
		||||
        >
 | 
			
		||||
          Join with address
 | 
			
		||||
        </MenuItem>
 | 
			
		||||
      )}
 | 
			
		||||
      { spaceId && (
 | 
			
		||||
        <MenuItem
 | 
			
		||||
          iconSrc={PlusIC}
 | 
			
		||||
          onClick={() => { afterOptionSelect(); openSpaceAddExisting(spaceId); }}
 | 
			
		||||
          disabled={!canManage}
 | 
			
		||||
        >
 | 
			
		||||
          Add existing
 | 
			
		||||
        </MenuItem>
 | 
			
		||||
      )}
 | 
			
		||||
      { spaceId && (
 | 
			
		||||
        <MenuItem
 | 
			
		||||
          onClick={() => { afterOptionSelect(); openSpaceManage(spaceId); }}
 | 
			
		||||
          iconSrc={HashSearchIC}
 | 
			
		||||
        >
 | 
			
		||||
          Manage rooms
 | 
			
		||||
        </MenuItem>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
HomeSpaceOptions.defaultProps = {
 | 
			
		||||
  spaceId: null,
 | 
			
		||||
};
 | 
			
		||||
HomeSpaceOptions.propTypes = {
 | 
			
		||||
  spaceId: PropTypes.string,
 | 
			
		||||
  afterOptionSelect: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function DrawerHeader({ selectedTab, spaceId }) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const tabName = selectedTab !== cons.tabs.DIRECTS ? 'Home' : 'Direct messages';
 | 
			
		||||
 | 
			
		||||
  const isDMTab = selectedTab === cons.tabs.DIRECTS;
 | 
			
		||||
  const room = mx.getRoom(spaceId);
 | 
			
		||||
  const spaceName = isDMTab ? null : (room?.name || null);
 | 
			
		||||
 | 
			
		||||
  const openSpaceOptions = (e) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    openReusableContextMenu(
 | 
			
		||||
      'bottom',
 | 
			
		||||
      getEventCords(e, '.header'),
 | 
			
		||||
      (closeMenu) => <SpaceOptions roomId={spaceId} afterOptionSelect={closeMenu} />,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const openHomeSpaceOptions = (e) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    openReusableContextMenu(
 | 
			
		||||
      'right',
 | 
			
		||||
      getEventCords(e, '.ic-btn'),
 | 
			
		||||
      (closeMenu) => <HomeSpaceOptions spaceId={spaceId} afterOptionSelect={closeMenu} />,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Header>
 | 
			
		||||
      {spaceName ? (
 | 
			
		||||
        <button
 | 
			
		||||
          className="drawer-header__btn"
 | 
			
		||||
          onClick={openSpaceOptions}
 | 
			
		||||
          type="button"
 | 
			
		||||
          onMouseUp={(e) => blurOnBubbling(e, '.drawer-header__btn')}
 | 
			
		||||
        >
 | 
			
		||||
          <TitleWrapper>
 | 
			
		||||
            <Text variant="s1" weight="medium" primary>{twemojify(spaceName)}</Text>
 | 
			
		||||
          </TitleWrapper>
 | 
			
		||||
          <RawIcon size="small" src={ChevronBottomIC} />
 | 
			
		||||
        </button>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <TitleWrapper>
 | 
			
		||||
          <Text variant="s1" weight="medium" primary>{tabName}</Text>
 | 
			
		||||
        </TitleWrapper>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      { isDMTab && <IconButton onClick={() => openInviteUser()} tooltip="Start DM" src={PlusIC} size="small" /> }
 | 
			
		||||
      { !isDMTab && <IconButton onClick={openHomeSpaceOptions} tooltip="Add rooms/spaces" src={PlusIC} size="small" /> }
 | 
			
		||||
    </Header>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DrawerHeader.defaultProps = {
 | 
			
		||||
  spaceId: null,
 | 
			
		||||
};
 | 
			
		||||
DrawerHeader.propTypes = {
 | 
			
		||||
  selectedTab: PropTypes.string.isRequired,
 | 
			
		||||
  spaceId: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DrawerHeader;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +0,0 @@
 | 
			
		|||
@use '../../partials/flex';
 | 
			
		||||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.drawer-header__btn {
 | 
			
		||||
  min-width: 0;
 | 
			
		||||
  @extend .cp-fx__row--s-c;
 | 
			
		||||
  @include dir.side(margin, 0, auto);
 | 
			
		||||
  padding: var(--sp-ultra-tight);
 | 
			
		||||
  border-radius: calc(var(--bo-radius) / 2);
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  
 | 
			
		||||
  & .header__title-wrapper {
 | 
			
		||||
    @include dir.side(margin, 0, var(--sp-extra-tight));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @media (hover:hover) {
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: var(--bg-surface-hover);
 | 
			
		||||
      box-shadow: var(--bs-surface-outline);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &:focus,
 | 
			
		||||
  &:active {
 | 
			
		||||
    background-color: var(--bg-surface-active);
 | 
			
		||||
    box-shadow: var(--bs-surface-outline);
 | 
			
		||||
    outline: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,112 +0,0 @@
 | 
			
		|||
import React, { useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import Postie from '../../../util/Postie';
 | 
			
		||||
import { roomIdByActivity, roomIdByAtoZ } from '../../../util/sort';
 | 
			
		||||
 | 
			
		||||
import RoomsCategory from './RoomsCategory';
 | 
			
		||||
 | 
			
		||||
import { useCategorizedSpaces } from '../../hooks/useCategorizedSpaces';
 | 
			
		||||
 | 
			
		||||
const drawerPostie = new Postie();
 | 
			
		||||
function Home({ spaceId }) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const { roomList, notifications, accountData } = initMatrix;
 | 
			
		||||
  const { spaces, rooms, directs } = roomList;
 | 
			
		||||
  useCategorizedSpaces();
 | 
			
		||||
  const isCategorized = accountData.categorizedSpaces.has(spaceId);
 | 
			
		||||
 | 
			
		||||
  let categories = null;
 | 
			
		||||
  let spaceIds = [];
 | 
			
		||||
  let roomIds = [];
 | 
			
		||||
  let directIds = [];
 | 
			
		||||
 | 
			
		||||
  if (spaceId) {
 | 
			
		||||
    const spaceChildIds = roomList.getSpaceChildren(spaceId) ?? [];
 | 
			
		||||
    spaceIds = spaceChildIds.filter((roomId) => spaces.has(roomId));
 | 
			
		||||
    roomIds = spaceChildIds.filter((roomId) => rooms.has(roomId));
 | 
			
		||||
    directIds = spaceChildIds.filter((roomId) => directs.has(roomId));
 | 
			
		||||
  } else {
 | 
			
		||||
    spaceIds = roomList.getOrphanSpaces().filter((id) => !accountData.spaceShortcut.has(id));
 | 
			
		||||
    roomIds = roomList.getOrphanRooms();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (isCategorized) {
 | 
			
		||||
    categories = roomList.getCategorizedSpaces(spaceIds);
 | 
			
		||||
    categories.delete(spaceId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const selectorChanged = (selectedRoomId, prevSelectedRoomId) => {
 | 
			
		||||
      if (!drawerPostie.hasTopic('selector-change')) return;
 | 
			
		||||
      const addresses = [];
 | 
			
		||||
      if (drawerPostie.hasSubscriber('selector-change', selectedRoomId)) addresses.push(selectedRoomId);
 | 
			
		||||
      if (drawerPostie.hasSubscriber('selector-change', prevSelectedRoomId)) addresses.push(prevSelectedRoomId);
 | 
			
		||||
      if (addresses.length === 0) return;
 | 
			
		||||
      drawerPostie.post('selector-change', addresses, selectedRoomId);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const notiChanged = (roomId, total, prevTotal) => {
 | 
			
		||||
      if (total === prevTotal) return;
 | 
			
		||||
      if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) {
 | 
			
		||||
        drawerPostie.post('unread-change', roomId);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
 | 
			
		||||
    notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
 | 
			
		||||
    notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged);
 | 
			
		||||
    return () => {
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
 | 
			
		||||
      notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
 | 
			
		||||
      notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      { !isCategorized && spaceIds.length !== 0 && (
 | 
			
		||||
        <RoomsCategory name="Spaces" roomIds={spaceIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      { roomIds.length !== 0 && (
 | 
			
		||||
        <RoomsCategory name="Rooms" roomIds={roomIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      { directIds.length !== 0 && (
 | 
			
		||||
        <RoomsCategory name="People" roomIds={directIds.sort(roomIdByActivity)} drawerPostie={drawerPostie} />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      { isCategorized && [...categories.keys()].sort(roomIdByAtoZ).map((catId) => {
 | 
			
		||||
        const rms = [];
 | 
			
		||||
        const dms = [];
 | 
			
		||||
        categories.get(catId).forEach((id) => {
 | 
			
		||||
          if (directs.has(id)) dms.push(id);
 | 
			
		||||
          else rms.push(id);
 | 
			
		||||
        });
 | 
			
		||||
        rms.sort(roomIdByAtoZ);
 | 
			
		||||
        dms.sort(roomIdByActivity);
 | 
			
		||||
        return (
 | 
			
		||||
          <RoomsCategory
 | 
			
		||||
            key={catId}
 | 
			
		||||
            spaceId={catId}
 | 
			
		||||
            name={mx.getRoom(catId).name}
 | 
			
		||||
            roomIds={rms.concat(dms)}
 | 
			
		||||
            drawerPostie={drawerPostie}
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      })}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
Home.defaultProps = {
 | 
			
		||||
  spaceId: null,
 | 
			
		||||
};
 | 
			
		||||
Home.propTypes = {
 | 
			
		||||
  spaceId: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Home;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import './Navigation.scss';
 | 
			
		||||
 | 
			
		||||
import SideBar from './SideBar';
 | 
			
		||||
import Drawer from './Drawer';
 | 
			
		||||
 | 
			
		||||
function Navigation() {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="navigation">
 | 
			
		||||
      <SideBar />
 | 
			
		||||
      <Drawer />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Navigation;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +0,0 @@
 | 
			
		|||
.navigation {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  background-color: var(--bg-surface-low);
 | 
			
		||||
 | 
			
		||||
  display: flex;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,92 +0,0 @@
 | 
			
		|||
import React, { useState } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './RoomsCategory.scss';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import { selectSpace, selectRoom, openReusableContextMenu } from '../../../client/action/navigation';
 | 
			
		||||
import { getEventCords } from '../../../util/common';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import RawIcon from '../../atoms/system-icons/RawIcon';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import Selector from './Selector';
 | 
			
		||||
import SpaceOptions from '../../molecules/space-options/SpaceOptions';
 | 
			
		||||
import { HomeSpaceOptions } from './DrawerHeader';
 | 
			
		||||
 | 
			
		||||
import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
 | 
			
		||||
import HorizontalMenuIC from '../../../../public/res/ic/outlined/horizontal-menu.svg';
 | 
			
		||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
 | 
			
		||||
import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg';
 | 
			
		||||
 | 
			
		||||
function RoomsCategory({
 | 
			
		||||
  spaceId, name, hideHeader, roomIds, drawerPostie,
 | 
			
		||||
}) {
 | 
			
		||||
  const { spaces, directs } = initMatrix.roomList;
 | 
			
		||||
  const [isOpen, setIsOpen] = useState(true);
 | 
			
		||||
 | 
			
		||||
  const openSpaceOptions = (e) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    openReusableContextMenu(
 | 
			
		||||
      'bottom',
 | 
			
		||||
      getEventCords(e, '.header'),
 | 
			
		||||
      (closeMenu) => <SpaceOptions roomId={spaceId} afterOptionSelect={closeMenu} />,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const openHomeSpaceOptions = (e) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    openReusableContextMenu(
 | 
			
		||||
      'right',
 | 
			
		||||
      getEventCords(e, '.ic-btn'),
 | 
			
		||||
      (closeMenu) => <HomeSpaceOptions spaceId={spaceId} afterOptionSelect={closeMenu} />,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const renderSelector = (roomId) => {
 | 
			
		||||
    const isSpace = spaces.has(roomId);
 | 
			
		||||
    const isDM = directs.has(roomId);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Selector
 | 
			
		||||
        key={roomId}
 | 
			
		||||
        roomId={roomId}
 | 
			
		||||
        isDM={isDM}
 | 
			
		||||
        drawerPostie={drawerPostie}
 | 
			
		||||
        onClick={() => (isSpace ? selectSpace(roomId) : selectRoom(roomId))}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="room-category">
 | 
			
		||||
      {!hideHeader && (
 | 
			
		||||
        <div className="room-category__header">
 | 
			
		||||
          <button className="room-category__toggle" onClick={() => setIsOpen(!isOpen)} type="button">
 | 
			
		||||
            <RawIcon src={isOpen ? ChevronBottomIC : ChevronRightIC} size="extra-small" />
 | 
			
		||||
            <Text className="cat-header" variant="b3" weight="medium">{name}</Text>
 | 
			
		||||
          </button>
 | 
			
		||||
          {spaceId && <IconButton onClick={openSpaceOptions} tooltip="Space options" src={HorizontalMenuIC} size="extra-small" />}
 | 
			
		||||
          {spaceId && <IconButton onClick={openHomeSpaceOptions} tooltip="Add rooms/spaces" src={PlusIC} size="extra-small" />}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      {(isOpen || hideHeader) && (
 | 
			
		||||
        <div className="room-category__content">
 | 
			
		||||
          {roomIds.map(renderSelector)}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
RoomsCategory.defaultProps = {
 | 
			
		||||
  spaceId: null,
 | 
			
		||||
  hideHeader: false,
 | 
			
		||||
};
 | 
			
		||||
RoomsCategory.propTypes = {
 | 
			
		||||
  spaceId: PropTypes.string,
 | 
			
		||||
  name: PropTypes.string.isRequired,
 | 
			
		||||
  hideHeader: PropTypes.bool,
 | 
			
		||||
  roomIds: PropTypes.arrayOf(PropTypes.string).isRequired,
 | 
			
		||||
  drawerPostie: PropTypes.shape({}).isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default RoomsCategory;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,54 +0,0 @@
 | 
			
		|||
@use '../../partials/flex';
 | 
			
		||||
@use '../../partials/dir';
 | 
			
		||||
@use '../../partials/text';
 | 
			
		||||
 | 
			
		||||
.room-category {
 | 
			
		||||
  &__header,
 | 
			
		||||
  &__toggle {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
  &__header {
 | 
			
		||||
    margin-top: var(--sp-extra-tight);
 | 
			
		||||
 | 
			
		||||
    & .ic-btn {
 | 
			
		||||
      padding: var(--sp-ultra-tight);
 | 
			
		||||
      border-radius: 4px;
 | 
			
		||||
      @include dir.side(margin, 0, 5px);
 | 
			
		||||
      & .ic-raw {
 | 
			
		||||
        width: 16px;
 | 
			
		||||
        height: 16px;
 | 
			
		||||
        background-color: var(--ic-surface-low);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &__toggle {
 | 
			
		||||
    @extend .cp-fx__item-one;
 | 
			
		||||
    padding: var(--sp-extra-tight) var(--sp-tight);
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    
 | 
			
		||||
    & .ic-raw {
 | 
			
		||||
      flex-shrink: 0;
 | 
			
		||||
      width: 12px;
 | 
			
		||||
      height: 12px;
 | 
			
		||||
      background-color: var(--ic-surface-low);
 | 
			
		||||
      @include dir.side(margin, 0, var(--sp-ultra-tight));
 | 
			
		||||
    }
 | 
			
		||||
    & .text {
 | 
			
		||||
      text-transform: uppercase;
 | 
			
		||||
      @extend .cp-txt__ellipsis;
 | 
			
		||||
    }
 | 
			
		||||
    &:hover .text {
 | 
			
		||||
      color: var(--tc-surface-normal);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__content:first-child {
 | 
			
		||||
    margin-top: var(--sp-extra-tight);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  & .room-selector {
 | 
			
		||||
    width: calc(100% - var(--sp-extra-tight));
 | 
			
		||||
    @include dir.side(margin, auto, 0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,93 +0,0 @@
 | 
			
		|||
/* eslint-disable react/prop-types */
 | 
			
		||||
import React, { useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import { openReusableContextMenu } from '../../../client/action/navigation';
 | 
			
		||||
import { getEventCords, abbreviateNumber } from '../../../util/common';
 | 
			
		||||
import { joinRuleToIconSrc } from '../../../util/matrixUtil';
 | 
			
		||||
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import RoomSelector from '../../molecules/room-selector/RoomSelector';
 | 
			
		||||
import RoomOptions from '../../molecules/room-options/RoomOptions';
 | 
			
		||||
import SpaceOptions from '../../molecules/space-options/SpaceOptions';
 | 
			
		||||
 | 
			
		||||
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
 | 
			
		||||
 | 
			
		||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
 | 
			
		||||
 | 
			
		||||
function Selector({
 | 
			
		||||
  roomId, isDM, drawerPostie, onClick,
 | 
			
		||||
}) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const noti = initMatrix.notifications;
 | 
			
		||||
  const room = mx.getRoom(roomId);
 | 
			
		||||
 | 
			
		||||
  let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
 | 
			
		||||
  if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
 | 
			
		||||
 | 
			
		||||
  const isMuted = noti.getNotiType(roomId) === cons.notifs.MUTE;
 | 
			
		||||
 | 
			
		||||
  const [, forceUpdate] = useForceUpdate();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const unSub1 = drawerPostie.subscribe('selector-change', roomId, forceUpdate);
 | 
			
		||||
    const unSub2 = drawerPostie.subscribe('unread-change', roomId, forceUpdate);
 | 
			
		||||
    return () => {
 | 
			
		||||
      unSub1();
 | 
			
		||||
      unSub2();
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const openOptions = (e) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    openReusableContextMenu(
 | 
			
		||||
      'right',
 | 
			
		||||
      getEventCords(e, '.room-selector'),
 | 
			
		||||
      room.isSpaceRoom()
 | 
			
		||||
        ? (closeMenu) => <SpaceOptions roomId={roomId} afterOptionSelect={closeMenu} />
 | 
			
		||||
        : (closeMenu) => <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <RoomSelector
 | 
			
		||||
      key={roomId}
 | 
			
		||||
      name={room.name}
 | 
			
		||||
      roomId={roomId}
 | 
			
		||||
      imageSrc={isDM ? imageSrc : null}
 | 
			
		||||
      iconSrc={isDM ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())}
 | 
			
		||||
      isSelected={navigation.selectedRoomId === roomId}
 | 
			
		||||
      isMuted={isMuted}
 | 
			
		||||
      isUnread={!isMuted && noti.hasNoti(roomId)}
 | 
			
		||||
      notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
 | 
			
		||||
      isAlert={noti.getHighlightNoti(roomId) !== 0}
 | 
			
		||||
      onClick={onClick}
 | 
			
		||||
      onContextMenu={openOptions}
 | 
			
		||||
      options={(
 | 
			
		||||
        <IconButton
 | 
			
		||||
          size="extra-small"
 | 
			
		||||
          tooltip="Options"
 | 
			
		||||
          tooltipPlacement="right"
 | 
			
		||||
          src={VerticalMenuIC}
 | 
			
		||||
          onClick={openOptions}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Selector.defaultProps = {
 | 
			
		||||
  isDM: true,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Selector.propTypes = {
 | 
			
		||||
  roomId: PropTypes.string.isRequired,
 | 
			
		||||
  isDM: PropTypes.bool,
 | 
			
		||||
  drawerPostie: PropTypes.shape({}).isRequired,
 | 
			
		||||
  onClick: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Selector;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,390 +0,0 @@
 | 
			
		|||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './SideBar.scss';
 | 
			
		||||
 | 
			
		||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
 | 
			
		||||
import { HTML5Backend } from 'react-dnd-html5-backend';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import colorMXID from '../../../util/colorMXID';
 | 
			
		||||
import {
 | 
			
		||||
  selectTab, openShortcutSpaces, openInviteList,
 | 
			
		||||
  openSearch, openSettings, openReusableContextMenu,
 | 
			
		||||
} from '../../../client/action/navigation';
 | 
			
		||||
import { moveSpaceShortcut } from '../../../client/action/accountData';
 | 
			
		||||
import { abbreviateNumber, getEventCords } from '../../../util/common';
 | 
			
		||||
import { isCrossVerified } from '../../../util/matrixUtil';
 | 
			
		||||
 | 
			
		||||
import Avatar from '../../atoms/avatar/Avatar';
 | 
			
		||||
import NotificationBadge from '../../atoms/badge/NotificationBadge';
 | 
			
		||||
import ScrollView from '../../atoms/scroll/ScrollView';
 | 
			
		||||
import SidebarAvatar from '../../molecules/sidebar-avatar/SidebarAvatar';
 | 
			
		||||
import SpaceOptions from '../../molecules/space-options/SpaceOptions';
 | 
			
		||||
 | 
			
		||||
import HomeIC from '../../../../public/res/ic/outlined/home.svg';
 | 
			
		||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
 | 
			
		||||
import AddPinIC from '../../../../public/res/ic/outlined/add-pin.svg';
 | 
			
		||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
 | 
			
		||||
import InviteIC from '../../../../public/res/ic/outlined/invite.svg';
 | 
			
		||||
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
 | 
			
		||||
 | 
			
		||||
import { useSelectedTab } from '../../hooks/useSelectedTab';
 | 
			
		||||
import { useDeviceList } from '../../hooks/useDeviceList';
 | 
			
		||||
 | 
			
		||||
import { tabText as settingTabText } from '../settings/Settings';
 | 
			
		||||
 | 
			
		||||
function useNotificationUpdate() {
 | 
			
		||||
  const { notifications } = initMatrix;
 | 
			
		||||
  const [, forceUpdate] = useState({});
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    function onNotificationChanged(roomId, total, prevTotal) {
 | 
			
		||||
      if (total === prevTotal) return;
 | 
			
		||||
      forceUpdate({});
 | 
			
		||||
    }
 | 
			
		||||
    notifications.on(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
 | 
			
		||||
    return () => {
 | 
			
		||||
      notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ProfileAvatarMenu() {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const [profile, setProfile] = useState({
 | 
			
		||||
    avatarUrl: null,
 | 
			
		||||
    displayName: mx.getUser(mx.getUserId()).displayName,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const user = mx.getUser(mx.getUserId());
 | 
			
		||||
    const setNewProfile = (avatarUrl, displayName) => setProfile({
 | 
			
		||||
      avatarUrl: avatarUrl || null,
 | 
			
		||||
      displayName: displayName || profile.displayName,
 | 
			
		||||
    });
 | 
			
		||||
    const onAvatarChange = (event, myUser) => {
 | 
			
		||||
      setNewProfile(myUser.avatarUrl, myUser.displayName);
 | 
			
		||||
    };
 | 
			
		||||
    mx.getProfileInfo(mx.getUserId()).then((info) => {
 | 
			
		||||
      setNewProfile(info.avatar_url, info.displayname);
 | 
			
		||||
    });
 | 
			
		||||
    user.on('User.avatarUrl', onAvatarChange);
 | 
			
		||||
    return () => {
 | 
			
		||||
      user.removeListener('User.avatarUrl', onAvatarChange);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SidebarAvatar
 | 
			
		||||
      onClick={openSettings}
 | 
			
		||||
      tooltip="Settings"
 | 
			
		||||
      avatar={(
 | 
			
		||||
        <Avatar
 | 
			
		||||
          text={profile.displayName}
 | 
			
		||||
          bgColor={colorMXID(mx.getUserId())}
 | 
			
		||||
          size="normal"
 | 
			
		||||
          imageSrc={profile.avatarUrl !== null ? mx.mxcUrlToHttp(profile.avatarUrl, 42, 42, 'crop') : null}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CrossSigninAlert() {
 | 
			
		||||
  const deviceList = useDeviceList();
 | 
			
		||||
  const unverified = deviceList?.filter((device) => isCrossVerified(device.device_id) === false);
 | 
			
		||||
 | 
			
		||||
  if (!unverified?.length) return null;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SidebarAvatar
 | 
			
		||||
      className="sidebar__cross-signin-alert"
 | 
			
		||||
      tooltip={`${unverified.length} unverified sessions`}
 | 
			
		||||
      onClick={() => openSettings(settingTabText.SECURITY)}
 | 
			
		||||
      avatar={<Avatar iconSrc={ShieldUserIC} iconColor="var(--ic-danger-normal)" size="normal" />}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function FeaturedTab() {
 | 
			
		||||
  const { roomList, accountData, notifications } = initMatrix;
 | 
			
		||||
  const [selectedTab] = useSelectedTab();
 | 
			
		||||
  useNotificationUpdate();
 | 
			
		||||
 | 
			
		||||
  function getHomeNoti() {
 | 
			
		||||
    const orphans = roomList.getOrphans();
 | 
			
		||||
    let noti = null;
 | 
			
		||||
 | 
			
		||||
    orphans.forEach((roomId) => {
 | 
			
		||||
      if (accountData.spaceShortcut.has(roomId)) return;
 | 
			
		||||
      if (!notifications.hasNoti(roomId)) return;
 | 
			
		||||
      if (noti === null) noti = { total: 0, highlight: 0 };
 | 
			
		||||
      const childNoti = notifications.getNoti(roomId);
 | 
			
		||||
      noti.total += childNoti.total;
 | 
			
		||||
      noti.highlight += childNoti.highlight;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return noti;
 | 
			
		||||
  }
 | 
			
		||||
  function getDMsNoti() {
 | 
			
		||||
    if (roomList.directs.size === 0) return null;
 | 
			
		||||
    let noti = null;
 | 
			
		||||
 | 
			
		||||
    [...roomList.directs].forEach((roomId) => {
 | 
			
		||||
      if (!notifications.hasNoti(roomId)) return;
 | 
			
		||||
      if (noti === null) noti = { total: 0, highlight: 0 };
 | 
			
		||||
      const childNoti = notifications.getNoti(roomId);
 | 
			
		||||
      noti.total += childNoti.total;
 | 
			
		||||
      noti.highlight += childNoti.highlight;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return noti;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const dmsNoti = getDMsNoti();
 | 
			
		||||
  const homeNoti = getHomeNoti();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <SidebarAvatar
 | 
			
		||||
        tooltip="Home"
 | 
			
		||||
        active={selectedTab === cons.tabs.HOME}
 | 
			
		||||
        onClick={() => selectTab(cons.tabs.HOME)}
 | 
			
		||||
        avatar={<Avatar iconSrc={HomeIC} size="normal" />}
 | 
			
		||||
        notificationBadge={homeNoti ? (
 | 
			
		||||
          <NotificationBadge
 | 
			
		||||
            alert={homeNoti?.highlight > 0}
 | 
			
		||||
            content={abbreviateNumber(homeNoti.total) || null}
 | 
			
		||||
          />
 | 
			
		||||
        ) : null}
 | 
			
		||||
      />
 | 
			
		||||
      <SidebarAvatar
 | 
			
		||||
        tooltip="People"
 | 
			
		||||
        active={selectedTab === cons.tabs.DIRECTS}
 | 
			
		||||
        onClick={() => selectTab(cons.tabs.DIRECTS)}
 | 
			
		||||
        avatar={<Avatar iconSrc={UserIC} size="normal" />}
 | 
			
		||||
        notificationBadge={dmsNoti ? (
 | 
			
		||||
          <NotificationBadge
 | 
			
		||||
            alert={dmsNoti?.highlight > 0}
 | 
			
		||||
            content={abbreviateNumber(dmsNoti.total) || null}
 | 
			
		||||
          />
 | 
			
		||||
        ) : null}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DraggableSpaceShortcut({
 | 
			
		||||
  isActive, spaceId, index, moveShortcut, onDrop,
 | 
			
		||||
}) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const { notifications } = initMatrix;
 | 
			
		||||
  const room = mx.getRoom(spaceId);
 | 
			
		||||
  const shortcutRef = useRef(null);
 | 
			
		||||
  const avatarRef = useRef(null);
 | 
			
		||||
 | 
			
		||||
  const openSpaceOptions = (e, sId) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    openReusableContextMenu(
 | 
			
		||||
      'right',
 | 
			
		||||
      getEventCords(e, '.sidebar-avatar'),
 | 
			
		||||
      (closeMenu) => <SpaceOptions roomId={sId} afterOptionSelect={closeMenu} />,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const [, drop] = useDrop({
 | 
			
		||||
    accept: 'SPACE_SHORTCUT',
 | 
			
		||||
    collect(monitor) {
 | 
			
		||||
      return {
 | 
			
		||||
        handlerId: monitor.getHandlerId(),
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    drop(item) {
 | 
			
		||||
      onDrop(item.index, item.spaceId);
 | 
			
		||||
    },
 | 
			
		||||
    hover(item, monitor) {
 | 
			
		||||
      if (!shortcutRef.current) return;
 | 
			
		||||
 | 
			
		||||
      const dragIndex = item.index;
 | 
			
		||||
      const hoverIndex = index;
 | 
			
		||||
      if (dragIndex === hoverIndex) return;
 | 
			
		||||
 | 
			
		||||
      const hoverBoundingRect = shortcutRef.current?.getBoundingClientRect();
 | 
			
		||||
      const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
 | 
			
		||||
      const clientOffset = monitor.getClientOffset();
 | 
			
		||||
      const hoverClientY = clientOffset.y - hoverBoundingRect.top;
 | 
			
		||||
 | 
			
		||||
      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      moveShortcut(dragIndex, hoverIndex);
 | 
			
		||||
      // eslint-disable-next-line no-param-reassign
 | 
			
		||||
      item.index = hoverIndex;
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const [{ isDragging }, drag] = useDrag({
 | 
			
		||||
    type: 'SPACE_SHORTCUT',
 | 
			
		||||
    item: () => ({ spaceId, index }),
 | 
			
		||||
    collect: (monitor) => ({
 | 
			
		||||
      isDragging: monitor.isDragging(),
 | 
			
		||||
    }),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  drag(avatarRef);
 | 
			
		||||
  drop(shortcutRef);
 | 
			
		||||
 | 
			
		||||
  if (shortcutRef.current) {
 | 
			
		||||
    if (isDragging) shortcutRef.current.style.opacity = 0;
 | 
			
		||||
    else shortcutRef.current.style.opacity = 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SidebarAvatar
 | 
			
		||||
      ref={shortcutRef}
 | 
			
		||||
      active={isActive}
 | 
			
		||||
      tooltip={room.name}
 | 
			
		||||
      onClick={() => selectTab(spaceId)}
 | 
			
		||||
      onContextMenu={(e) => openSpaceOptions(e, spaceId)}
 | 
			
		||||
      avatar={(
 | 
			
		||||
        <Avatar
 | 
			
		||||
          ref={avatarRef}
 | 
			
		||||
          text={room.name}
 | 
			
		||||
          bgColor={colorMXID(room.roomId)}
 | 
			
		||||
          size="normal"
 | 
			
		||||
          imageSrc={room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop') || null}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      notificationBadge={notifications.hasNoti(spaceId) ? (
 | 
			
		||||
        <NotificationBadge
 | 
			
		||||
          alert={notifications.getHighlightNoti(spaceId) > 0}
 | 
			
		||||
          content={abbreviateNumber(notifications.getTotalNoti(spaceId)) || null}
 | 
			
		||||
        />
 | 
			
		||||
      ) : null}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DraggableSpaceShortcut.propTypes = {
 | 
			
		||||
  spaceId: PropTypes.string.isRequired,
 | 
			
		||||
  isActive: PropTypes.bool.isRequired,
 | 
			
		||||
  index: PropTypes.number.isRequired,
 | 
			
		||||
  moveShortcut: PropTypes.func.isRequired,
 | 
			
		||||
  onDrop: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function SpaceShortcut() {
 | 
			
		||||
  const { accountData } = initMatrix;
 | 
			
		||||
  const [selectedTab] = useSelectedTab();
 | 
			
		||||
  useNotificationUpdate();
 | 
			
		||||
  const [spaceShortcut, setSpaceShortcut] = useState([...accountData.spaceShortcut]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleShortcut = () => setSpaceShortcut([...accountData.spaceShortcut]);
 | 
			
		||||
    accountData.on(cons.events.accountData.SPACE_SHORTCUT_UPDATED, handleShortcut);
 | 
			
		||||
    return () => {
 | 
			
		||||
      accountData.removeListener(cons.events.accountData.SPACE_SHORTCUT_UPDATED, handleShortcut);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const moveShortcut = (dragIndex, hoverIndex) => {
 | 
			
		||||
    const dragSpaceId = spaceShortcut[dragIndex];
 | 
			
		||||
    const newShortcuts = [...spaceShortcut];
 | 
			
		||||
    newShortcuts.splice(dragIndex, 1);
 | 
			
		||||
    newShortcuts.splice(hoverIndex, 0, dragSpaceId);
 | 
			
		||||
    setSpaceShortcut(newShortcuts);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleDrop = (dragIndex, dragSpaceId) => {
 | 
			
		||||
    if ([...accountData.spaceShortcut][dragIndex] === dragSpaceId) return;
 | 
			
		||||
    moveSpaceShortcut(dragSpaceId, dragIndex);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <DndProvider backend={HTML5Backend}>
 | 
			
		||||
      {
 | 
			
		||||
        spaceShortcut.map((shortcut, index) => (
 | 
			
		||||
          <DraggableSpaceShortcut
 | 
			
		||||
            key={shortcut}
 | 
			
		||||
            index={index}
 | 
			
		||||
            spaceId={shortcut}
 | 
			
		||||
            isActive={selectedTab === shortcut}
 | 
			
		||||
            moveShortcut={moveShortcut}
 | 
			
		||||
            onDrop={handleDrop}
 | 
			
		||||
          />
 | 
			
		||||
        ))
 | 
			
		||||
      }
 | 
			
		||||
    </DndProvider>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useTotalInvites() {
 | 
			
		||||
  const { roomList } = initMatrix;
 | 
			
		||||
  const totalInviteCount = () => roomList.inviteRooms.size
 | 
			
		||||
    + roomList.inviteSpaces.size
 | 
			
		||||
    + roomList.inviteDirects.size;
 | 
			
		||||
  const [totalInvites, updateTotalInvites] = useState(totalInviteCount());
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const onInviteListChange = () => {
 | 
			
		||||
      updateTotalInvites(totalInviteCount());
 | 
			
		||||
    };
 | 
			
		||||
    roomList.on(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
 | 
			
		||||
    return () => {
 | 
			
		||||
      roomList.removeListener(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return [totalInvites];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SideBar() {
 | 
			
		||||
  const [totalInvites] = useTotalInvites();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="sidebar">
 | 
			
		||||
      <div className="sidebar__scrollable">
 | 
			
		||||
        <ScrollView invisible>
 | 
			
		||||
          <div className="scrollable-content">
 | 
			
		||||
            <div className="featured-container">
 | 
			
		||||
              <FeaturedTab />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="sidebar-divider" />
 | 
			
		||||
            <div className="space-container">
 | 
			
		||||
              <SpaceShortcut />
 | 
			
		||||
              <SidebarAvatar
 | 
			
		||||
                tooltip="Pin spaces"
 | 
			
		||||
                onClick={() => openShortcutSpaces()}
 | 
			
		||||
                avatar={<Avatar iconSrc={AddPinIC} size="normal" />}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </ScrollView>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="sidebar__sticky">
 | 
			
		||||
        <div className="sidebar-divider" />
 | 
			
		||||
        <div className="sticky-container">
 | 
			
		||||
          <SidebarAvatar
 | 
			
		||||
            tooltip="Search"
 | 
			
		||||
            onClick={() => openSearch()}
 | 
			
		||||
            avatar={<Avatar iconSrc={SearchIC} size="normal" />}
 | 
			
		||||
          />
 | 
			
		||||
          { totalInvites !== 0 && (
 | 
			
		||||
            <SidebarAvatar
 | 
			
		||||
              tooltip="Invites"
 | 
			
		||||
              onClick={() => openInviteList()}
 | 
			
		||||
              avatar={<Avatar iconSrc={InviteIC} size="normal" />}
 | 
			
		||||
              notificationBadge={<NotificationBadge alert content={totalInvites} />}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          <CrossSigninAlert />
 | 
			
		||||
          <ProfileAvatarMenu />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default SideBar;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,75 +0,0 @@
 | 
			
		|||
@use '../../partials/flex';
 | 
			
		||||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.sidebar {
 | 
			
		||||
  @extend .cp-fx__column;
 | 
			
		||||
 | 
			
		||||
  width: var(--navigation-sidebar-width);
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  background-color: var(--bg-surface-extra-low);
 | 
			
		||||
  @include dir.side(border, none, 1px solid var(--bg-surface-border));
 | 
			
		||||
 | 
			
		||||
  &__scrollable,
 | 
			
		||||
  &__sticky {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__scrollable {
 | 
			
		||||
    @extend .cp-fx__item-one;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scrollable-content {
 | 
			
		||||
  &::after {
 | 
			
		||||
    content: '';
 | 
			
		||||
    display: block;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 8px;
 | 
			
		||||
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    background-image: linear-gradient(
 | 
			
		||||
      to top,
 | 
			
		||||
      var(--bg-surface-extra-low),
 | 
			
		||||
      var(--bg-surface-extra-low-transparent)
 | 
			
		||||
    );
 | 
			
		||||
    position: sticky;
 | 
			
		||||
    bottom: -1px;
 | 
			
		||||
    left: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.featured-container,
 | 
			
		||||
.space-container,
 | 
			
		||||
.sticky-container {
 | 
			
		||||
  @extend .cp-fx__column--c-c;
 | 
			
		||||
 | 
			
		||||
  padding: var(--sp-ultra-tight) 0;
 | 
			
		||||
 | 
			
		||||
  & > .sidebar-avatar,
 | 
			
		||||
  & > .avatar-container {
 | 
			
		||||
    margin: calc(var(--sp-tight) / 2) 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.sidebar-divider {
 | 
			
		||||
  margin: auto;
 | 
			
		||||
  width: 24px;
 | 
			
		||||
  height: 1px;
 | 
			
		||||
  background-color: var(--bg-surface-border);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sidebar__cross-signin-alert .avatar-container {
 | 
			
		||||
  box-shadow: var(--bs-danger-border);
 | 
			
		||||
  animation-name: pushRight;
 | 
			
		||||
  animation-duration: 400ms;
 | 
			
		||||
  animation-iteration-count: 30;
 | 
			
		||||
  animation-direction: alternate;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes pushRight {
 | 
			
		||||
  from {
 | 
			
		||||
    transform: translateX(4px) scale(1);
 | 
			
		||||
  }
 | 
			
		||||
  to {
 | 
			
		||||
    transform: translateX(0) scale(1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import colorMXID from '../../../util/colorMXID';
 | 
			
		||||
| 
						 | 
				
			
			@ -22,7 +21,9 @@ function ProfileEditor({ userId }) {
 | 
			
		|||
  const user = mx.getUser(mx.getUserId());
 | 
			
		||||
 | 
			
		||||
  const displayNameRef = useRef(null);
 | 
			
		||||
  const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null);
 | 
			
		||||
  const [avatarSrc, setAvatarSrc] = useState(
 | 
			
		||||
    user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null
 | 
			
		||||
  );
 | 
			
		||||
  const [username, setUsername] = useState(user.displayName);
 | 
			
		||||
  const [disabled, setDisabled] = useState(true);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +45,7 @@ function ProfileEditor({ userId }) {
 | 
			
		|||
        'Remove avatar',
 | 
			
		||||
        'Are you sure that you want to remove avatar?',
 | 
			
		||||
        'Remove',
 | 
			
		||||
        'caution',
 | 
			
		||||
        'caution'
 | 
			
		||||
      );
 | 
			
		||||
      if (isConfirmed) {
 | 
			
		||||
        mx.setAvatarUrl('');
 | 
			
		||||
| 
						 | 
				
			
			@ -79,7 +80,10 @@ function ProfileEditor({ userId }) {
 | 
			
		|||
    <form
 | 
			
		||||
      className="profile-editor__form"
 | 
			
		||||
      style={{ marginBottom: avatarSrc ? '24px' : '0' }}
 | 
			
		||||
      onSubmit={(e) => { e.preventDefault(); saveDisplayName(); }}
 | 
			
		||||
      onSubmit={(e) => {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        saveDisplayName();
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Input
 | 
			
		||||
        label={`Display name of ${mx.getUserId()}`}
 | 
			
		||||
| 
						 | 
				
			
			@ -87,7 +91,9 @@ function ProfileEditor({ userId }) {
 | 
			
		|||
        value={mx.getUser(mx.getUserId()).displayName}
 | 
			
		||||
        forwardRef={displayNameRef}
 | 
			
		||||
      />
 | 
			
		||||
      <Button variant="primary" type="submit" disabled={disabled}>Save</Button>
 | 
			
		||||
      <Button variant="primary" type="submit" disabled={disabled}>
 | 
			
		||||
        Save
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button onClick={cancelDisplayNameChanges}>Cancel</Button>
 | 
			
		||||
    </form>
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			@ -95,7 +101,9 @@ function ProfileEditor({ userId }) {
 | 
			
		|||
  const renderInfo = () => (
 | 
			
		||||
    <div className="profile-editor__info" style={{ marginBottom: avatarSrc ? '24px' : '0' }}>
 | 
			
		||||
      <div>
 | 
			
		||||
        <Text variant="h2" primary weight="medium">{twemojify(username) ?? userId}</Text>
 | 
			
		||||
        <Text variant="h2" primary weight="medium">
 | 
			
		||||
          {username ?? userId}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <IconButton
 | 
			
		||||
          src={PencilIC}
 | 
			
		||||
          size="extra-small"
 | 
			
		||||
| 
						 | 
				
			
			@ -116,9 +124,7 @@ function ProfileEditor({ userId }) {
 | 
			
		|||
        onUpload={handleAvatarUpload}
 | 
			
		||||
        onRequestRemove={() => handleAvatarUpload(null)}
 | 
			
		||||
      />
 | 
			
		||||
      {
 | 
			
		||||
        isEditing ? renderForm() : renderInfo()
 | 
			
		||||
      }
 | 
			
		||||
      {isEditing ? renderForm() : renderInfo()}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,16 +2,17 @@ import React, { useState, useEffect, useRef } from 'react';
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import './ProfileViewer.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import { selectRoom, openReusableContextMenu } from '../../../client/action/navigation';
 | 
			
		||||
import { openReusableContextMenu } from '../../../client/action/navigation';
 | 
			
		||||
import * as roomActions from '../../../client/action/room';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  getUsername, getUsernameOfRoomMember, getPowerLabel, hasDMWith, hasDevices,
 | 
			
		||||
  getUsername,
 | 
			
		||||
  getUsernameOfRoomMember,
 | 
			
		||||
  getPowerLabel,
 | 
			
		||||
  hasDevices,
 | 
			
		||||
} from '../../../util/matrixUtil';
 | 
			
		||||
import { getEventCords } from '../../../util/common';
 | 
			
		||||
import colorMXID from '../../../util/colorMXID';
 | 
			
		||||
| 
						 | 
				
			
			@ -33,26 +34,24 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 | 
			
		|||
 | 
			
		||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
 | 
			
		||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
 | 
			
		||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
 | 
			
		||||
import { getDMRoomFor } from '../../utils/matrix';
 | 
			
		||||
 | 
			
		||||
function ModerationTools({
 | 
			
		||||
  roomId, userId,
 | 
			
		||||
}) {
 | 
			
		||||
function ModerationTools({ roomId, userId }) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const room = mx.getRoom(roomId);
 | 
			
		||||
  const roomMember = room.getMember(userId);
 | 
			
		||||
 | 
			
		||||
  const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
 | 
			
		||||
  const powerLevel = roomMember?.powerLevel || 0;
 | 
			
		||||
  const canIKick = (
 | 
			
		||||
    roomMember?.membership === 'join'
 | 
			
		||||
    && room.currentState.hasSufficientPowerLevelFor('kick', myPowerLevel)
 | 
			
		||||
    && powerLevel < myPowerLevel
 | 
			
		||||
  );
 | 
			
		||||
  const canIBan = (
 | 
			
		||||
    ['join', 'leave'].includes(roomMember?.membership)
 | 
			
		||||
    && room.currentState.hasSufficientPowerLevelFor('ban', myPowerLevel)
 | 
			
		||||
    && powerLevel < myPowerLevel
 | 
			
		||||
  );
 | 
			
		||||
  const canIKick =
 | 
			
		||||
    roomMember?.membership === 'join' &&
 | 
			
		||||
    room.currentState.hasSufficientPowerLevelFor('kick', myPowerLevel) &&
 | 
			
		||||
    powerLevel < myPowerLevel;
 | 
			
		||||
  const canIBan =
 | 
			
		||||
    ['join', 'leave'].includes(roomMember?.membership) &&
 | 
			
		||||
    room.currentState.hasSufficientPowerLevelFor('ban', myPowerLevel) &&
 | 
			
		||||
    powerLevel < myPowerLevel;
 | 
			
		||||
 | 
			
		||||
  const handleKick = (e) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
| 
						 | 
				
			
			@ -120,13 +119,14 @@ function SessionInfo({ userId }) {
 | 
			
		|||
      <div className="session-info__chips">
 | 
			
		||||
        {devices === null && <Text variant="b2">Loading sessions...</Text>}
 | 
			
		||||
        {devices?.length === 0 && <Text variant="b2">No session found.</Text>}
 | 
			
		||||
        {devices !== null && (devices.map((device) => (
 | 
			
		||||
          <Chip
 | 
			
		||||
            key={device.deviceId}
 | 
			
		||||
            iconSrc={ShieldEmptyIC}
 | 
			
		||||
            text={device.getDisplayName() || device.deviceId}
 | 
			
		||||
          />
 | 
			
		||||
        )))}
 | 
			
		||||
        {devices !== null &&
 | 
			
		||||
          devices.map((device) => (
 | 
			
		||||
            <Chip
 | 
			
		||||
              key={device.deviceId}
 | 
			
		||||
              iconSrc={ShieldEmptyIC}
 | 
			
		||||
              text={device.getDisplayName() || device.deviceId}
 | 
			
		||||
            />
 | 
			
		||||
          ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -137,7 +137,11 @@ function SessionInfo({ userId }) {
 | 
			
		|||
        onClick={() => setIsVisible(!isVisible)}
 | 
			
		||||
        iconSrc={isVisible ? ChevronBottomIC : ChevronRightIC}
 | 
			
		||||
      >
 | 
			
		||||
        <Text variant="b2">{`View ${devices?.length > 0 ? `${devices.length} ${devices.length == 1 ? 'session' : 'sessions'}` : 'sessions'}`}</Text>
 | 
			
		||||
        <Text variant="b2">{`View ${
 | 
			
		||||
          devices?.length > 0
 | 
			
		||||
            ? `${devices.length} ${devices.length == 1 ? 'session' : 'sessions'}`
 | 
			
		||||
            : 'sessions'
 | 
			
		||||
        }`}</Text>
 | 
			
		||||
      </MenuItem>
 | 
			
		||||
      {renderSessionChips()}
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -155,6 +159,7 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
 | 
			
		|||
 | 
			
		||||
  const isMountedRef = useRef(true);
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const { navigateRoom } = useRoomNavigate();
 | 
			
		||||
  const room = mx.getRoom(roomId);
 | 
			
		||||
  const member = room.getMember(userId);
 | 
			
		||||
  const isInvitable = member?.membership !== 'join' && member?.membership !== 'ban';
 | 
			
		||||
| 
						 | 
				
			
			@ -164,25 +169,18 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
 | 
			
		|||
 | 
			
		||||
  const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
 | 
			
		||||
  const userPL = room.getMember(userId)?.powerLevel || 0;
 | 
			
		||||
  const canIKick = room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
 | 
			
		||||
  const canIKick =
 | 
			
		||||
    room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
 | 
			
		||||
 | 
			
		||||
  const isBanned = member?.membership === 'ban';
 | 
			
		||||
 | 
			
		||||
  const onCreated = (dmRoomId) => {
 | 
			
		||||
    if (isMountedRef.current === false) return;
 | 
			
		||||
    setIsCreatingDM(false);
 | 
			
		||||
    selectRoom(dmRoomId);
 | 
			
		||||
    navigateRoom(dmRoomId);
 | 
			
		||||
    onRequestClose();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const { roomList } = initMatrix;
 | 
			
		||||
    roomList.on(cons.events.roomList.ROOM_CREATED, onCreated);
 | 
			
		||||
    return () => {
 | 
			
		||||
      isMountedRef.current = false;
 | 
			
		||||
      roomList.removeListener(cons.events.roomList.ROOM_CREATED, onCreated);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setIsUserIgnored(initMatrix.matrixClient.isUserIgnored(userId));
 | 
			
		||||
    setIsIgnoring(false);
 | 
			
		||||
| 
						 | 
				
			
			@ -191,9 +189,9 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
 | 
			
		|||
 | 
			
		||||
  const openDM = async () => {
 | 
			
		||||
    // Check and open if user already have a DM with userId.
 | 
			
		||||
    const dmRoomId = hasDMWith(userId);
 | 
			
		||||
    const dmRoomId = getDMRoomFor(mx, userId)?.roomId;
 | 
			
		||||
    if (dmRoomId) {
 | 
			
		||||
      selectRoom(dmRoomId);
 | 
			
		||||
      navigateRoom(dmRoomId);
 | 
			
		||||
      onRequestClose();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -201,7 +199,8 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
 | 
			
		|||
    // Create new DM
 | 
			
		||||
    try {
 | 
			
		||||
      setIsCreatingDM(true);
 | 
			
		||||
      await roomActions.createDM(userId, await hasDevices(userId));
 | 
			
		||||
      const result = await roomActions.createDM(userId, await hasDevices(userId));
 | 
			
		||||
      onCreated(result.room_id);
 | 
			
		||||
    } catch {
 | 
			
		||||
      if (isMountedRef.current === false) return;
 | 
			
		||||
      setIsCreatingDM(false);
 | 
			
		||||
| 
						 | 
				
			
			@ -246,31 +245,19 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
 | 
			
		|||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="profile-viewer__buttons">
 | 
			
		||||
      <Button
 | 
			
		||||
        variant="primary"
 | 
			
		||||
        onClick={openDM}
 | 
			
		||||
        disabled={isCreatingDM}
 | 
			
		||||
      >
 | 
			
		||||
      <Button variant="primary" onClick={openDM} disabled={isCreatingDM}>
 | 
			
		||||
        {isCreatingDM ? 'Creating room...' : 'Message'}
 | 
			
		||||
      </Button>
 | 
			
		||||
      { isBanned && canIKick && (
 | 
			
		||||
        <Button
 | 
			
		||||
          variant="positive"
 | 
			
		||||
          onClick={() => roomActions.unban(roomId, userId)}
 | 
			
		||||
        >
 | 
			
		||||
      {isBanned && canIKick && (
 | 
			
		||||
        <Button variant="positive" onClick={() => roomActions.unban(roomId, userId)}>
 | 
			
		||||
          Unban
 | 
			
		||||
        </Button>
 | 
			
		||||
      )}
 | 
			
		||||
      { (isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
 | 
			
		||||
        <Button
 | 
			
		||||
          onClick={toggleInvite}
 | 
			
		||||
          disabled={isInviting}
 | 
			
		||||
        >
 | 
			
		||||
          {
 | 
			
		||||
            isInvited
 | 
			
		||||
              ? `${isInviting ? 'Disinviting...' : 'Disinvite'}`
 | 
			
		||||
              : `${isInviting ? 'Inviting...' : 'Invite'}`
 | 
			
		||||
          }
 | 
			
		||||
      {(isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
 | 
			
		||||
        <Button onClick={toggleInvite} disabled={isInviting}>
 | 
			
		||||
          {isInvited
 | 
			
		||||
            ? `${isInviting ? 'Disinviting...' : 'Disinvite'}`
 | 
			
		||||
            : `${isInviting ? 'Inviting...' : 'Invite'}`}
 | 
			
		||||
        </Button>
 | 
			
		||||
      )}
 | 
			
		||||
      <Button
 | 
			
		||||
| 
						 | 
				
			
			@ -278,11 +265,9 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
 | 
			
		|||
        onClick={toggleIgnore}
 | 
			
		||||
        disabled={isIgnoring}
 | 
			
		||||
      >
 | 
			
		||||
        {
 | 
			
		||||
          isUserIgnored
 | 
			
		||||
            ? `${isIgnoring ? 'Unignoring...' : 'Unignore'}`
 | 
			
		||||
            : `${isIgnoring ? 'Ignoring...' : 'Ignore'}`
 | 
			
		||||
        }
 | 
			
		||||
        {isUserIgnored
 | 
			
		||||
          ? `${isIgnoring ? 'Unignoring...' : 'Unignore'}`
 | 
			
		||||
          : `${isIgnoring ? 'Ignoring...' : 'Ignore'}`}
 | 
			
		||||
      </Button>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			@ -326,8 +311,8 @@ function useRerenderOnProfileChange(roomId, userId) {
 | 
			
		|||
  useEffect(() => {
 | 
			
		||||
    const handleProfileChange = (mEvent, member) => {
 | 
			
		||||
      if (
 | 
			
		||||
        mEvent.getRoomId() === roomId
 | 
			
		||||
        && (member.userId === userId || member.userId === mx.getUserId())
 | 
			
		||||
        mEvent.getRoomId() === roomId &&
 | 
			
		||||
        (member.userId === userId || member.userId === mx.getUserId())
 | 
			
		||||
      ) {
 | 
			
		||||
        forceUpdate();
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -352,20 +337,22 @@ function ProfileViewer() {
 | 
			
		|||
    const roomMember = room.getMember(userId);
 | 
			
		||||
    const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(userId);
 | 
			
		||||
    const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl;
 | 
			
		||||
    const avatarUrl = (avatarMxc && avatarMxc !== 'null') ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null;
 | 
			
		||||
    const avatarUrl =
 | 
			
		||||
      avatarMxc && avatarMxc !== 'null' ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null;
 | 
			
		||||
 | 
			
		||||
    const powerLevel = roomMember?.powerLevel || 0;
 | 
			
		||||
    const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
 | 
			
		||||
 | 
			
		||||
    const canChangeRole = (
 | 
			
		||||
      room.currentState.maySendEvent('m.room.power_levels', mx.getUserId())
 | 
			
		||||
      && (powerLevel < myPowerLevel || userId === mx.getUserId())
 | 
			
		||||
    );
 | 
			
		||||
    const canChangeRole =
 | 
			
		||||
      room.currentState.maySendEvent('m.room.power_levels', mx.getUserId()) &&
 | 
			
		||||
      (powerLevel < myPowerLevel || userId === mx.getUserId());
 | 
			
		||||
 | 
			
		||||
    const handleChangePowerLevel = async (newPowerLevel) => {
 | 
			
		||||
      if (newPowerLevel === powerLevel) return;
 | 
			
		||||
      const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
 | 
			
		||||
      const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?';
 | 
			
		||||
      const SHARED_POWER_MSG =
 | 
			
		||||
        'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
 | 
			
		||||
      const DEMOTING_MYSELF_MSG =
 | 
			
		||||
        'You will not be able to undo this change as you are demoting yourself. Are you sure?';
 | 
			
		||||
 | 
			
		||||
      const isSharedPower = newPowerLevel === myPowerLevel;
 | 
			
		||||
      const isDemotingMyself = userId === mx.getUserId();
 | 
			
		||||
| 
						 | 
				
			
			@ -374,7 +361,7 @@ function ProfileViewer() {
 | 
			
		|||
          'Change power level',
 | 
			
		||||
          isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG,
 | 
			
		||||
          'Change',
 | 
			
		||||
          'caution',
 | 
			
		||||
          'caution'
 | 
			
		||||
        );
 | 
			
		||||
        if (!isConfirmed) return;
 | 
			
		||||
        roomActions.setPowerLevel(roomId, userId, newPowerLevel);
 | 
			
		||||
| 
						 | 
				
			
			@ -384,20 +371,16 @@ function ProfileViewer() {
 | 
			
		|||
    };
 | 
			
		||||
 | 
			
		||||
    const handlePowerSelector = (e) => {
 | 
			
		||||
      openReusableContextMenu(
 | 
			
		||||
        'bottom',
 | 
			
		||||
        getEventCords(e, '.btn-surface'),
 | 
			
		||||
        (closeMenu) => (
 | 
			
		||||
          <PowerLevelSelector
 | 
			
		||||
            value={powerLevel}
 | 
			
		||||
            max={myPowerLevel}
 | 
			
		||||
            onSelect={(pl) => {
 | 
			
		||||
              closeMenu();
 | 
			
		||||
              handleChangePowerLevel(pl);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
      openReusableContextMenu('bottom', getEventCords(e, '.btn-surface'), (closeMenu) => (
 | 
			
		||||
        <PowerLevelSelector
 | 
			
		||||
          value={powerLevel}
 | 
			
		||||
          max={myPowerLevel}
 | 
			
		||||
          onSelect={(pl) => {
 | 
			
		||||
            closeMenu();
 | 
			
		||||
            handleChangePowerLevel(pl);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      ));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
| 
						 | 
				
			
			@ -405,8 +388,10 @@ function ProfileViewer() {
 | 
			
		|||
        <div className="profile-viewer__user">
 | 
			
		||||
          <Avatar imageSrc={avatarUrl} text={username} bgColor={colorMXID(userId)} size="large" />
 | 
			
		||||
          <div className="profile-viewer__user__info">
 | 
			
		||||
            <Text variant="s1" weight="medium">{twemojify(username)}</Text>
 | 
			
		||||
            <Text variant="b2">{twemojify(userId)}</Text>
 | 
			
		||||
            <Text variant="s1" weight="medium">
 | 
			
		||||
              {username}
 | 
			
		||||
            </Text>
 | 
			
		||||
            <Text variant="b2">{userId}</Text>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="profile-viewer__user__role">
 | 
			
		||||
            <Text variant="b3">Role</Text>
 | 
			
		||||
| 
						 | 
				
			
			@ -420,7 +405,7 @@ function ProfileViewer() {
 | 
			
		|||
        </div>
 | 
			
		||||
        <ModerationTools roomId={roomId} userId={userId} />
 | 
			
		||||
        <SessionInfo userId={userId} />
 | 
			
		||||
        { userId !== mx.getUserId() && (
 | 
			
		||||
        {userId !== mx.getUserId() && (
 | 
			
		||||
          <ProfileFooter roomId={roomId} userId={userId} onRequestClose={closeDialog} />
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,295 +0,0 @@
 | 
			
		|||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './PublicRooms.scss';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import { selectRoom, selectTab } from '../../../client/action/navigation';
 | 
			
		||||
import * as roomActions from '../../../client/action/room';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import Button from '../../atoms/button/Button';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import Spinner from '../../atoms/spinner/Spinner';
 | 
			
		||||
import Input from '../../atoms/input/Input';
 | 
			
		||||
import PopupWindow from '../../molecules/popup-window/PopupWindow';
 | 
			
		||||
import RoomTile from '../../molecules/room-tile/RoomTile';
 | 
			
		||||
 | 
			
		||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 | 
			
		||||
import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg';
 | 
			
		||||
 | 
			
		||||
const SEARCH_LIMIT = 20;
 | 
			
		||||
 | 
			
		||||
function TryJoinWithAlias({ alias, onRequestClose }) {
 | 
			
		||||
  const [status, setStatus] = useState({
 | 
			
		||||
    isJoining: false,
 | 
			
		||||
    error: null,
 | 
			
		||||
    roomId: null,
 | 
			
		||||
    tempRoomId: null,
 | 
			
		||||
  });
 | 
			
		||||
  function handleOnRoomAdded(roomId) {
 | 
			
		||||
    if (status.tempRoomId !== null && status.tempRoomId !== roomId) return;
 | 
			
		||||
    setStatus({
 | 
			
		||||
      isJoining: false, error: null, roomId, tempRoomId: null,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    initMatrix.roomList.on(cons.events.roomList.ROOM_JOINED, handleOnRoomAdded);
 | 
			
		||||
    return () => {
 | 
			
		||||
      initMatrix.roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleOnRoomAdded);
 | 
			
		||||
    };
 | 
			
		||||
  }, [status]);
 | 
			
		||||
 | 
			
		||||
  async function joinWithAlias() {
 | 
			
		||||
    setStatus({
 | 
			
		||||
      isJoining: true, error: null, roomId: null, tempRoomId: null,
 | 
			
		||||
    });
 | 
			
		||||
    try {
 | 
			
		||||
      const roomId = await roomActions.join(alias, false);
 | 
			
		||||
      setStatus({
 | 
			
		||||
        isJoining: true, error: null, roomId: null, tempRoomId: roomId,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      setStatus({
 | 
			
		||||
        isJoining: false,
 | 
			
		||||
        error: `Unable to join ${alias}. Either room is private or doesn't exist.`,
 | 
			
		||||
        roomId: null,
 | 
			
		||||
        tempRoomId: null,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="try-join-with-alias">
 | 
			
		||||
      {status.roomId === null && !status.isJoining && status.error === null && (
 | 
			
		||||
        <Button onClick={() => joinWithAlias()}>{`Try joining ${alias}`}</Button>
 | 
			
		||||
      )}
 | 
			
		||||
      {status.isJoining && (
 | 
			
		||||
        <>
 | 
			
		||||
          <Spinner size="small" />
 | 
			
		||||
          <Text>{`Joining ${alias}...`}</Text>
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
      {status.roomId !== null && (
 | 
			
		||||
        <Button onClick={() => { onRequestClose(); selectRoom(status.roomId); }}>Open</Button>
 | 
			
		||||
      )}
 | 
			
		||||
      {status.error !== null && <Text variant="b2"><span style={{ color: 'var(--bg-danger)' }}>{status.error}</span></Text>}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TryJoinWithAlias.propTypes = {
 | 
			
		||||
  alias: PropTypes.string.isRequired,
 | 
			
		||||
  onRequestClose: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
 | 
			
		||||
  const [isSearching, updateIsSearching] = useState(false);
 | 
			
		||||
  const [isViewMore, updateIsViewMore] = useState(false);
 | 
			
		||||
  const [publicRooms, updatePublicRooms] = useState([]);
 | 
			
		||||
  const [nextBatch, updateNextBatch] = useState(undefined);
 | 
			
		||||
  const [searchQuery, updateSearchQuery] = useState({});
 | 
			
		||||
  const [joiningRooms, updateJoiningRooms] = useState(new Set());
 | 
			
		||||
 | 
			
		||||
  const roomNameRef = useRef(null);
 | 
			
		||||
  const hsRef = useRef(null);
 | 
			
		||||
  const userId = initMatrix.matrixClient.getUserId();
 | 
			
		||||
 | 
			
		||||
  async function searchRooms(viewMore) {
 | 
			
		||||
    let inputRoomName = roomNameRef?.current?.value || searchTerm;
 | 
			
		||||
    let isInputAlias = false;
 | 
			
		||||
    if (typeof inputRoomName === 'string') {
 | 
			
		||||
      isInputAlias = inputRoomName[0] === '#' && inputRoomName.indexOf(':') > 1;
 | 
			
		||||
    }
 | 
			
		||||
    const hsFromAlias = (isInputAlias) ? inputRoomName.slice(inputRoomName.indexOf(':') + 1) : null;
 | 
			
		||||
    let inputHs = hsFromAlias || hsRef?.current?.value;
 | 
			
		||||
 | 
			
		||||
    if (typeof inputHs !== 'string') inputHs = userId.slice(userId.indexOf(':') + 1);
 | 
			
		||||
    if (typeof inputRoomName !== 'string') inputRoomName = '';
 | 
			
		||||
 | 
			
		||||
    if (isSearching) return;
 | 
			
		||||
    if (viewMore !== true
 | 
			
		||||
      && inputRoomName === searchQuery.name
 | 
			
		||||
      && inputHs === searchQuery.homeserver
 | 
			
		||||
    ) return;
 | 
			
		||||
 | 
			
		||||
    updateSearchQuery({
 | 
			
		||||
      name: inputRoomName,
 | 
			
		||||
      homeserver: inputHs,
 | 
			
		||||
    });
 | 
			
		||||
    if (isViewMore !== viewMore) updateIsViewMore(viewMore);
 | 
			
		||||
    updateIsSearching(true);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await initMatrix.matrixClient.publicRooms({
 | 
			
		||||
        server: inputHs,
 | 
			
		||||
        limit: SEARCH_LIMIT,
 | 
			
		||||
        since: viewMore ? nextBatch : undefined,
 | 
			
		||||
        include_all_networks: true,
 | 
			
		||||
        filter: {
 | 
			
		||||
          generic_search_term: inputRoomName,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const totalRooms = viewMore ? publicRooms.concat(result.chunk) : result.chunk;
 | 
			
		||||
      updatePublicRooms(totalRooms);
 | 
			
		||||
      updateNextBatch(result.next_batch);
 | 
			
		||||
      updateIsSearching(false);
 | 
			
		||||
      updateIsViewMore(false);
 | 
			
		||||
      if (totalRooms.length === 0) {
 | 
			
		||||
        updateSearchQuery({
 | 
			
		||||
          error: inputRoomName === ''
 | 
			
		||||
            ? `No public rooms on ${inputHs}`
 | 
			
		||||
            : `No result found for "${inputRoomName}" on ${inputHs}`,
 | 
			
		||||
          alias: isInputAlias ? inputRoomName : null,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      updatePublicRooms([]);
 | 
			
		||||
      let err = 'Something went wrong!';
 | 
			
		||||
      if (e?.httpStatus >= 400 && e?.httpStatus < 500) {
 | 
			
		||||
        err = e.message;
 | 
			
		||||
      }
 | 
			
		||||
      updateSearchQuery({
 | 
			
		||||
        error: err,
 | 
			
		||||
        alias: isInputAlias ? inputRoomName : null,
 | 
			
		||||
      });
 | 
			
		||||
      updateIsSearching(false);
 | 
			
		||||
      updateNextBatch(undefined);
 | 
			
		||||
      updateIsViewMore(false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isOpen) searchRooms();
 | 
			
		||||
  }, [isOpen]);
 | 
			
		||||
 | 
			
		||||
  function handleOnRoomAdded(roomId) {
 | 
			
		||||
    if (joiningRooms.has(roomId)) {
 | 
			
		||||
      joiningRooms.delete(roomId);
 | 
			
		||||
      updateJoiningRooms(new Set(Array.from(joiningRooms)));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    initMatrix.roomList.on(cons.events.roomList.ROOM_JOINED, handleOnRoomAdded);
 | 
			
		||||
    return () => {
 | 
			
		||||
      initMatrix.roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleOnRoomAdded);
 | 
			
		||||
    };
 | 
			
		||||
  }, [joiningRooms]);
 | 
			
		||||
 | 
			
		||||
  function handleViewRoom(roomId) {
 | 
			
		||||
    const room = initMatrix.matrixClient.getRoom(roomId);
 | 
			
		||||
    if (room.isSpaceRoom()) selectTab(roomId);
 | 
			
		||||
    else selectRoom(roomId);
 | 
			
		||||
    onRequestClose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function joinRoom(roomIdOrAlias) {
 | 
			
		||||
    joiningRooms.add(roomIdOrAlias);
 | 
			
		||||
    updateJoiningRooms(new Set(Array.from(joiningRooms)));
 | 
			
		||||
    roomActions.join(roomIdOrAlias, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function renderRoomList(rooms) {
 | 
			
		||||
    return rooms.map((room) => {
 | 
			
		||||
      const alias = typeof room.canonical_alias === 'string' ? room.canonical_alias : room.room_id;
 | 
			
		||||
      const name = typeof room.name === 'string' ? room.name : alias;
 | 
			
		||||
      const isJoined = initMatrix.matrixClient.getRoom(room.room_id)?.getMyMembership() === 'join';
 | 
			
		||||
      return (
 | 
			
		||||
        <RoomTile
 | 
			
		||||
          key={room.room_id}
 | 
			
		||||
          avatarSrc={typeof room.avatar_url === 'string' ? initMatrix.matrixClient.mxcUrlToHttp(room.avatar_url, 42, 42, 'crop') : null}
 | 
			
		||||
          name={name}
 | 
			
		||||
          id={alias}
 | 
			
		||||
          memberCount={room.num_joined_members}
 | 
			
		||||
          desc={typeof room.topic === 'string' ? room.topic : null}
 | 
			
		||||
          options={(
 | 
			
		||||
            <>
 | 
			
		||||
              {isJoined && <Button onClick={() => handleViewRoom(room.room_id)}>Open</Button>}
 | 
			
		||||
              {!isJoined && (joiningRooms.has(room.room_id) ? <Spinner size="small" /> : <Button onClick={() => joinRoom(room.aliases?.[0] || room.room_id)} variant="primary">Join</Button>)}
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <PopupWindow
 | 
			
		||||
      isOpen={isOpen}
 | 
			
		||||
      title="Public rooms"
 | 
			
		||||
      contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
 | 
			
		||||
      onRequestClose={onRequestClose}
 | 
			
		||||
    >
 | 
			
		||||
      <div className="public-rooms">
 | 
			
		||||
        <form className="public-rooms__form" onSubmit={(e) => { e.preventDefault(); searchRooms(); }}>
 | 
			
		||||
          <div className="public-rooms__input-wrapper">
 | 
			
		||||
            <Input value={searchTerm} forwardRef={roomNameRef} label="Room name or alias" />
 | 
			
		||||
            <Input forwardRef={hsRef} value={userId.slice(userId.indexOf(':') + 1)} label="Homeserver" required />
 | 
			
		||||
          </div>
 | 
			
		||||
          <Button disabled={isSearching} iconSrc={HashSearchIC} variant="primary" type="submit">Search</Button>
 | 
			
		||||
        </form>
 | 
			
		||||
        <div className="public-rooms__search-status">
 | 
			
		||||
          {
 | 
			
		||||
            typeof searchQuery.name !== 'undefined' && isSearching && (
 | 
			
		||||
              searchQuery.name === ''
 | 
			
		||||
                ? (
 | 
			
		||||
                  <div className="flex--center">
 | 
			
		||||
                    <Spinner size="small" />
 | 
			
		||||
                    <Text variant="b2">{`Loading public rooms from ${searchQuery.homeserver}...`}</Text>
 | 
			
		||||
                  </div>
 | 
			
		||||
                )
 | 
			
		||||
                : (
 | 
			
		||||
                  <div className="flex--center">
 | 
			
		||||
                    <Spinner size="small" />
 | 
			
		||||
                    <Text variant="b2">{`Searching for "${searchQuery.name}" on ${searchQuery.homeserver}...`}</Text>
 | 
			
		||||
                  </div>
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            typeof searchQuery.name !== 'undefined' && !isSearching && (
 | 
			
		||||
              searchQuery.name === ''
 | 
			
		||||
                ? <Text variant="b2">{`Public rooms on ${searchQuery.homeserver}.`}</Text>
 | 
			
		||||
                : <Text variant="b2">{`Search result for "${searchQuery.name}" on ${searchQuery.homeserver}.`}</Text>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          { searchQuery.error && (
 | 
			
		||||
            <>
 | 
			
		||||
              <Text className="public-rooms__search-error" variant="b2">{searchQuery.error}</Text>
 | 
			
		||||
              {typeof searchQuery.alias === 'string' && (
 | 
			
		||||
                <TryJoinWithAlias onRequestClose={onRequestClose} alias={searchQuery.alias} />
 | 
			
		||||
              )}
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
        { publicRooms.length !== 0 && (
 | 
			
		||||
          <div className="public-rooms__content">
 | 
			
		||||
            { renderRoomList(publicRooms) }
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        { publicRooms.length !== 0 && publicRooms.length % SEARCH_LIMIT === 0 && (
 | 
			
		||||
          <div className="public-rooms__view-more">
 | 
			
		||||
            { isViewMore !== true && (
 | 
			
		||||
              <Button onClick={() => searchRooms(true)}>View more</Button>
 | 
			
		||||
            )}
 | 
			
		||||
            { isViewMore && <Spinner /> }
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </PopupWindow>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PublicRooms.defaultProps = {
 | 
			
		||||
  searchTerm: undefined,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
PublicRooms.propTypes = {
 | 
			
		||||
  isOpen: PropTypes.bool.isRequired,
 | 
			
		||||
  searchTerm: PropTypes.string,
 | 
			
		||||
  onRequestClose: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default PublicRooms;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,85 +0,0 @@
 | 
			
		|||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.public-rooms {
 | 
			
		||||
  @include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
 | 
			
		||||
  margin-top: var(--sp-extra-tight);
 | 
			
		||||
 | 
			
		||||
  &__form {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: flex-end;
 | 
			
		||||
 | 
			
		||||
    & .btn-primary {
 | 
			
		||||
      padding: {
 | 
			
		||||
        top: 11px;
 | 
			
		||||
        bottom: 11px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &__input-wrapper {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
 | 
			
		||||
    display: flex;
 | 
			
		||||
    @include dir.side(margin, 0, var(--sp-normal));
 | 
			
		||||
 | 
			
		||||
    & > div:first-child {
 | 
			
		||||
      flex: 1;
 | 
			
		||||
      min-width: 0;
 | 
			
		||||
 | 
			
		||||
      & .input {
 | 
			
		||||
        @include dir.prop(border-radius,
 | 
			
		||||
          var(--bo-radius) 0 0 var(--bo-radius),
 | 
			
		||||
          0 var(--bo-radius) var(--bo-radius) 0,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & > div:last-child .input {
 | 
			
		||||
      width: 120px;
 | 
			
		||||
      @include dir.prop(border-left-width, 0, 1px);
 | 
			
		||||
      @include dir.prop(border-right-width, 1px, 0);
 | 
			
		||||
      @include dir.prop(border-radius,
 | 
			
		||||
        0 var(--bo-radius) var(--bo-radius) 0,
 | 
			
		||||
        var(--bo-radius) 0 0 var(--bo-radius),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__search-status {
 | 
			
		||||
    margin-top: var(--sp-extra-loose);
 | 
			
		||||
    margin-bottom: var(--sp-tight);
 | 
			
		||||
    & .donut-spinner {
 | 
			
		||||
      margin: 0 var(--sp-tight);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .try-join-with-alias {
 | 
			
		||||
      margin-top: var(--sp-normal);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &__search-error {
 | 
			
		||||
    color: var(--bg-danger);
 | 
			
		||||
  }
 | 
			
		||||
  &__content {
 | 
			
		||||
    border-top: 1px solid var(--bg-surface-border);
 | 
			
		||||
  }
 | 
			
		||||
  &__view-more {
 | 
			
		||||
    margin-top: var(--sp-loose);
 | 
			
		||||
    @include dir.side(margin, calc(var(--av-normal) + var(--sp-normal)), 0);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  & .room-tile {
 | 
			
		||||
    margin-top: var(--sp-normal);
 | 
			
		||||
    &__options {
 | 
			
		||||
      align-self: flex-end;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.try-join-with-alias {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  
 | 
			
		||||
  & >.text:nth-child(2) {
 | 
			
		||||
    margin: 0 var(--sp-normal);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +1,8 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import ReadReceipts from '../read-receipts/ReadReceipts';
 | 
			
		||||
import ProfileViewer from '../profile-viewer/ProfileViewer';
 | 
			
		||||
import ShortcutSpaces from '../shortcut-spaces/ShortcutSpaces';
 | 
			
		||||
import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExisting';
 | 
			
		||||
import Search from '../search/Search';
 | 
			
		||||
import ViewSource from '../view-source/ViewSource';
 | 
			
		||||
import CreateRoom from '../create-room/CreateRoom';
 | 
			
		||||
import JoinAlias from '../join-alias/JoinAlias';
 | 
			
		||||
import EmojiVerification from '../emoji-verification/EmojiVerification';
 | 
			
		||||
| 
						 | 
				
			
			@ -15,10 +12,7 @@ import ReusableDialog from '../../molecules/dialog/ReusableDialog';
 | 
			
		|||
function Dialogs() {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <ReadReceipts />
 | 
			
		||||
      <ViewSource />
 | 
			
		||||
      <ProfileViewer />
 | 
			
		||||
      <ShortcutSpaces />
 | 
			
		||||
      <CreateRoom />
 | 
			
		||||
      <JoinAlias />
 | 
			
		||||
      <SpaceAddExisting />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,35 +3,18 @@ import React, { useState, useEffect } from 'react';
 | 
			
		|||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
 | 
			
		||||
import InviteList from '../invite-list/InviteList';
 | 
			
		||||
import PublicRooms from '../public-rooms/PublicRooms';
 | 
			
		||||
import InviteUser from '../invite-user/InviteUser';
 | 
			
		||||
import Settings from '../settings/Settings';
 | 
			
		||||
import SpaceSettings from '../space-settings/SpaceSettings';
 | 
			
		||||
import SpaceManage from '../space-manage/SpaceManage';
 | 
			
		||||
import RoomSettings from '../room/RoomSettings';
 | 
			
		||||
 | 
			
		||||
function Windows() {
 | 
			
		||||
  const [isInviteList, changeInviteList] = useState(false);
 | 
			
		||||
  const [publicRooms, changePublicRooms] = useState({
 | 
			
		||||
    isOpen: false,
 | 
			
		||||
    searchTerm: undefined,
 | 
			
		||||
  });
 | 
			
		||||
  const [inviteUser, changeInviteUser] = useState({
 | 
			
		||||
    isOpen: false,
 | 
			
		||||
    roomId: undefined,
 | 
			
		||||
    term: undefined,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function openInviteList() {
 | 
			
		||||
    changeInviteList(true);
 | 
			
		||||
  }
 | 
			
		||||
  function openPublicRooms(searchTerm) {
 | 
			
		||||
    changePublicRooms({
 | 
			
		||||
      isOpen: true,
 | 
			
		||||
      searchTerm,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  function openInviteUser(roomId, searchTerm) {
 | 
			
		||||
    changeInviteUser({
 | 
			
		||||
      isOpen: true,
 | 
			
		||||
| 
						 | 
				
			
			@ -41,24 +24,14 @@ function Windows() {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
 | 
			
		||||
    navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
 | 
			
		||||
    navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
 | 
			
		||||
    return () => {
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <InviteList isOpen={isInviteList} onRequestClose={() => changeInviteList(false)} />
 | 
			
		||||
      <PublicRooms
 | 
			
		||||
        isOpen={publicRooms.isOpen}
 | 
			
		||||
        searchTerm={publicRooms.searchTerm}
 | 
			
		||||
        onRequestClose={() => changePublicRooms({ isOpen: false, searchTerm: undefined })}
 | 
			
		||||
      />
 | 
			
		||||
      <InviteUser
 | 
			
		||||
        isOpen={inviteUser.isOpen}
 | 
			
		||||
        roomId={inviteUser.roomId}
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +41,6 @@ function Windows() {
 | 
			
		|||
      <Settings />
 | 
			
		||||
      <SpaceSettings />
 | 
			
		||||
      <RoomSettings />
 | 
			
		||||
      <SpaceManage />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,76 +0,0 @@
 | 
			
		|||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
 | 
			
		||||
import colorMXID from '../../../util/colorMXID';
 | 
			
		||||
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import PeopleSelector from '../../molecules/people-selector/PeopleSelector';
 | 
			
		||||
import Dialog from '../../molecules/dialog/Dialog';
 | 
			
		||||
 | 
			
		||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 | 
			
		||||
import { openProfileViewer } from '../../../client/action/navigation';
 | 
			
		||||
 | 
			
		||||
function ReadReceipts() {
 | 
			
		||||
  const [isOpen, setIsOpen] = useState(false);
 | 
			
		||||
  const [readers, setReaders] = useState([]);
 | 
			
		||||
  const [roomId, setRoomId] = useState(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const loadReadReceipts = (rId, userIds) => {
 | 
			
		||||
      setReaders(userIds);
 | 
			
		||||
      setRoomId(rId);
 | 
			
		||||
      setIsOpen(true);
 | 
			
		||||
    };
 | 
			
		||||
    navigation.on(cons.events.navigation.READRECEIPTS_OPENED, loadReadReceipts);
 | 
			
		||||
    return () => {
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.READRECEIPTS_OPENED, loadReadReceipts);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const handleAfterClose = () => {
 | 
			
		||||
    setReaders([]);
 | 
			
		||||
    setRoomId(null);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function renderPeople(userId) {
 | 
			
		||||
    const room = initMatrix.matrixClient.getRoom(roomId);
 | 
			
		||||
    const member = room.getMember(userId);
 | 
			
		||||
    const getUserDisplayName = () => {
 | 
			
		||||
      if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
 | 
			
		||||
      return getUsername(userId);
 | 
			
		||||
    };
 | 
			
		||||
    return (
 | 
			
		||||
      <PeopleSelector
 | 
			
		||||
        key={userId}
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          setIsOpen(false);
 | 
			
		||||
          openProfileViewer(userId, roomId);
 | 
			
		||||
        }}
 | 
			
		||||
        avatarSrc={member?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 24, 24, 'crop')}
 | 
			
		||||
        name={getUserDisplayName(userId)}
 | 
			
		||||
        color={colorMXID(userId)}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog
 | 
			
		||||
      isOpen={isOpen}
 | 
			
		||||
      title="Seen by"
 | 
			
		||||
      onAfterClose={handleAfterClose}
 | 
			
		||||
      onRequestClose={() => setIsOpen(false)}
 | 
			
		||||
      contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
 | 
			
		||||
    >
 | 
			
		||||
      <div style={{ marginTop: 'var(--sp-tight)', marginBottom: 'var(--sp-extra-loose)' }}>
 | 
			
		||||
        {
 | 
			
		||||
          readers.map(renderPeople)
 | 
			
		||||
        }
 | 
			
		||||
      </div>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ReadReceipts;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,35 +0,0 @@
 | 
			
		|||
class EventLimit {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this._from = 0;
 | 
			
		||||
 | 
			
		||||
    this.SMALLEST_EVT_HEIGHT = 32;
 | 
			
		||||
    this.PAGES_COUNT = 4;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get maxEvents() {
 | 
			
		||||
    return Math.round(document.body.clientHeight / this.SMALLEST_EVT_HEIGHT) * this.PAGES_COUNT;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get from() {
 | 
			
		||||
    return this._from;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get length() {
 | 
			
		||||
    return this._from + this.maxEvents;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setFrom(from) {
 | 
			
		||||
    this._from = from < 0 ? 0 : from;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  paginate(backwards, limit, timelineLength) {
 | 
			
		||||
    this._from = backwards ? this._from - limit : this._from + limit;
 | 
			
		||||
 | 
			
		||||
    if (!backwards && this.length > timelineLength) {
 | 
			
		||||
      this._from = timelineLength - this.maxEvents;
 | 
			
		||||
    }
 | 
			
		||||
    if (this._from < 0) this._from = 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default EventLimit;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,215 +0,0 @@
 | 
			
		|||
import React, {
 | 
			
		||||
  useState, useEffect, useCallback, useRef,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './PeopleDrawer.scss';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import { getPowerLabel, getUsernameOfRoomMember } from '../../../util/matrixUtil';
 | 
			
		||||
import colorMXID from '../../../util/colorMXID';
 | 
			
		||||
import { openInviteUser, openProfileViewer } from '../../../client/action/navigation';
 | 
			
		||||
import AsyncSearch from '../../../util/AsyncSearch';
 | 
			
		||||
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
 | 
			
		||||
import RawIcon from '../../atoms/system-icons/RawIcon';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import Button from '../../atoms/button/Button';
 | 
			
		||||
import ScrollView from '../../atoms/scroll/ScrollView';
 | 
			
		||||
import Input from '../../atoms/input/Input';
 | 
			
		||||
import SegmentedControl from '../../atoms/segmented-controls/SegmentedControls';
 | 
			
		||||
import PeopleSelector from '../../molecules/people-selector/PeopleSelector';
 | 
			
		||||
 | 
			
		||||
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
 | 
			
		||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
 | 
			
		||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 | 
			
		||||
 | 
			
		||||
function simplyfiMembers(members) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  return members.map((member) => ({
 | 
			
		||||
    userId: member.userId,
 | 
			
		||||
    name: getUsernameOfRoomMember(member),
 | 
			
		||||
    username: member.userId.slice(1, member.userId.indexOf(':')),
 | 
			
		||||
    avatarSrc: member.getAvatarUrl(mx.baseUrl, 24, 24, 'crop'),
 | 
			
		||||
    peopleRole: getPowerLabel(member.powerLevel),
 | 
			
		||||
    powerLevel: members.powerLevel,
 | 
			
		||||
  }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const asyncSearch = new AsyncSearch();
 | 
			
		||||
function PeopleDrawer({ roomId }) {
 | 
			
		||||
  const PER_PAGE_MEMBER = 50;
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const room = mx.getRoom(roomId);
 | 
			
		||||
  const canInvite = room?.canInvite(mx.getUserId());
 | 
			
		||||
 | 
			
		||||
  const [itemCount, setItemCount] = useState(PER_PAGE_MEMBER);
 | 
			
		||||
  const [membership, setMembership] = useState('join');
 | 
			
		||||
  const [memberList, setMemberList] = useState([]);
 | 
			
		||||
  const [searchedMembers, setSearchedMembers] = useState(null);
 | 
			
		||||
  const searchRef = useRef(null);
 | 
			
		||||
 | 
			
		||||
  const getMembersWithMembership = useCallback(
 | 
			
		||||
    (mship) => room.getMembersWithMembership(mship),
 | 
			
		||||
    [roomId, membership],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  function loadMorePeople() {
 | 
			
		||||
    setItemCount(itemCount + PER_PAGE_MEMBER);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleSearchData(data) {
 | 
			
		||||
    // NOTICE: data is passed as object property
 | 
			
		||||
    // because react sucks at handling state update with array.
 | 
			
		||||
    setSearchedMembers({ data });
 | 
			
		||||
    setItemCount(PER_PAGE_MEMBER);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleSearch(e) {
 | 
			
		||||
    const term = e.target.value;
 | 
			
		||||
    if (term === '' || term === undefined) {
 | 
			
		||||
      searchRef.current.value = '';
 | 
			
		||||
      searchRef.current.focus();
 | 
			
		||||
      setSearchedMembers(null);
 | 
			
		||||
      setItemCount(PER_PAGE_MEMBER);
 | 
			
		||||
    } else asyncSearch.search(term);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    asyncSearch.setup(memberList, {
 | 
			
		||||
      keys: ['name', 'username', 'userId'],
 | 
			
		||||
      limit: PER_PAGE_MEMBER,
 | 
			
		||||
    });
 | 
			
		||||
  }, [memberList]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let isLoadingMembers = false;
 | 
			
		||||
    let isRoomChanged = false;
 | 
			
		||||
    const updateMemberList = (event) => {
 | 
			
		||||
      if (isLoadingMembers) return;
 | 
			
		||||
      if (event && event?.getRoomId() !== roomId) return;
 | 
			
		||||
      setMemberList(
 | 
			
		||||
        simplyfiMembers(
 | 
			
		||||
          getMembersWithMembership(membership)
 | 
			
		||||
            .sort(memberByAtoZ).sort(memberByPowerLevel),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
    searchRef.current.value = '';
 | 
			
		||||
    updateMemberList();
 | 
			
		||||
    isLoadingMembers = true;
 | 
			
		||||
    room.loadMembersIfNeeded().then(() => {
 | 
			
		||||
      isLoadingMembers = false;
 | 
			
		||||
      if (isRoomChanged) return;
 | 
			
		||||
      updateMemberList();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    asyncSearch.on(asyncSearch.RESULT_SENT, handleSearchData);
 | 
			
		||||
    mx.on('RoomMember.membership', updateMemberList);
 | 
			
		||||
    mx.on('RoomMember.powerLevel', updateMemberList);
 | 
			
		||||
    return () => {
 | 
			
		||||
      isRoomChanged = true;
 | 
			
		||||
      setMemberList([]);
 | 
			
		||||
      setSearchedMembers(null);
 | 
			
		||||
      setItemCount(PER_PAGE_MEMBER);
 | 
			
		||||
      asyncSearch.removeListener(asyncSearch.RESULT_SENT, handleSearchData);
 | 
			
		||||
      mx.removeListener('RoomMember.membership', updateMemberList);
 | 
			
		||||
      mx.removeListener('RoomMember.powerLevel', updateMemberList);
 | 
			
		||||
    };
 | 
			
		||||
  }, [roomId, membership]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setMembership('join');
 | 
			
		||||
  }, [roomId]);
 | 
			
		||||
 | 
			
		||||
  const mList = searchedMembers !== null ? searchedMembers.data : memberList.slice(0, itemCount);
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="people-drawer">
 | 
			
		||||
      <Header>
 | 
			
		||||
        <TitleWrapper>
 | 
			
		||||
          <Text variant="s1" primary>
 | 
			
		||||
            People
 | 
			
		||||
            <Text className="people-drawer__member-count" variant="b3">{`${room.getJoinedMemberCount()} members`}</Text>
 | 
			
		||||
          </Text>
 | 
			
		||||
        </TitleWrapper>
 | 
			
		||||
        <IconButton onClick={() => openInviteUser(roomId)} tooltip="Invite" src={AddUserIC} disabled={!canInvite} />
 | 
			
		||||
      </Header>
 | 
			
		||||
      <div className="people-drawer__content-wrapper">
 | 
			
		||||
        <div className="people-drawer__scrollable">
 | 
			
		||||
          <ScrollView autoHide>
 | 
			
		||||
            <div className="people-drawer__content">
 | 
			
		||||
              <SegmentedControl
 | 
			
		||||
                selected={
 | 
			
		||||
                  (() => {
 | 
			
		||||
                    const getSegmentIndex = {
 | 
			
		||||
                      join: 0,
 | 
			
		||||
                      invite: 1,
 | 
			
		||||
                      ban: 2,
 | 
			
		||||
                    };
 | 
			
		||||
                    return getSegmentIndex[membership];
 | 
			
		||||
                  })()
 | 
			
		||||
                }
 | 
			
		||||
                segments={[{ text: 'Joined' }, { text: 'Invited' }, { text: 'Banned' }]}
 | 
			
		||||
                onSelect={(index) => {
 | 
			
		||||
                  const selectSegment = [
 | 
			
		||||
                    () => setMembership('join'),
 | 
			
		||||
                    () => setMembership('invite'),
 | 
			
		||||
                    () => setMembership('ban'),
 | 
			
		||||
                  ];
 | 
			
		||||
                  selectSegment[index]?.();
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
              {
 | 
			
		||||
                mList.map((member) => (
 | 
			
		||||
                  <PeopleSelector
 | 
			
		||||
                    key={member.userId}
 | 
			
		||||
                    onClick={() => openProfileViewer(member.userId, roomId)}
 | 
			
		||||
                    avatarSrc={member.avatarSrc}
 | 
			
		||||
                    name={member.name}
 | 
			
		||||
                    color={colorMXID(member.userId)}
 | 
			
		||||
                    peopleRole={member.peopleRole}
 | 
			
		||||
                  />
 | 
			
		||||
                ))
 | 
			
		||||
              }
 | 
			
		||||
              {
 | 
			
		||||
                (searchedMembers?.data.length === 0 || memberList.length === 0)
 | 
			
		||||
                && (
 | 
			
		||||
                  <div className="people-drawer__noresult">
 | 
			
		||||
                    <Text variant="b2">No results found!</Text>
 | 
			
		||||
                  </div>
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
              <div className="people-drawer__load-more">
 | 
			
		||||
                {
 | 
			
		||||
                  mList.length !== 0
 | 
			
		||||
                  && memberList.length > itemCount
 | 
			
		||||
                  && searchedMembers === null
 | 
			
		||||
                  && (
 | 
			
		||||
                    <Button onClick={loadMorePeople}>View more</Button>
 | 
			
		||||
                  )
 | 
			
		||||
                }
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </ScrollView>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="people-drawer__sticky">
 | 
			
		||||
          <form onSubmit={(e) => e.preventDefault()} className="people-search">
 | 
			
		||||
            <RawIcon size="small" src={SearchIC} />
 | 
			
		||||
            <Input forwardRef={searchRef} type="text" onChange={handleSearch} placeholder="Search" required />
 | 
			
		||||
            {
 | 
			
		||||
              searchedMembers !== null
 | 
			
		||||
              && <IconButton onClick={handleSearch} size="small" src={CrossIC} />
 | 
			
		||||
            }
 | 
			
		||||
          </form>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PeopleDrawer.propTypes = {
 | 
			
		||||
  roomId: PropTypes.string.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default PeopleDrawer;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,93 +0,0 @@
 | 
			
		|||
@use '../../partials/flex';
 | 
			
		||||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.people-drawer {
 | 
			
		||||
  @extend .cp-fx__column;
 | 
			
		||||
  width: var(--people-drawer-width);
 | 
			
		||||
  background-color: var(--bg-surface-low);
 | 
			
		||||
  @include dir.side(border, 1px solid var(--bg-surface-border), none);
 | 
			
		||||
 | 
			
		||||
  &__member-count {
 | 
			
		||||
    color: var(--tc-surface-low);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__content-wrapper {
 | 
			
		||||
    @extend .cp-fx__item-one;
 | 
			
		||||
    @extend .cp-fx__column;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__scrollable {
 | 
			
		||||
    @extend .cp-fx__item-one;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__noresult {
 | 
			
		||||
    padding: var(--sp-extra-tight) var(--sp-normal);
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__sticky {
 | 
			
		||||
    & .people-search {
 | 
			
		||||
      --search-input-height: 40px;
 | 
			
		||||
      min-height: var(--search-input-height);
 | 
			
		||||
  
 | 
			
		||||
      margin: 0 var(--sp-extra-tight);
 | 
			
		||||
 | 
			
		||||
      position: relative;
 | 
			
		||||
      bottom: var(--sp-normal);
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
 | 
			
		||||
      & > .ic-raw,
 | 
			
		||||
      & > .ic-btn {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        z-index: 99;
 | 
			
		||||
      }
 | 
			
		||||
      & > .ic-raw {
 | 
			
		||||
        @include dir.prop(left, var(--sp-tight), unset);
 | 
			
		||||
        @include dir.prop(right, unset, var(--sp-tight));
 | 
			
		||||
      }
 | 
			
		||||
      & > .ic-btn {
 | 
			
		||||
        @include dir.prop(right, 2px, unset);
 | 
			
		||||
        @include dir.prop(left, unset, 2px);
 | 
			
		||||
      }
 | 
			
		||||
      & .input-container {
 | 
			
		||||
        flex: 1;
 | 
			
		||||
      }
 | 
			
		||||
      & .input {
 | 
			
		||||
        padding: 0 44px;
 | 
			
		||||
        height: var(--search-input-height);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.people-drawer__content {
 | 
			
		||||
  padding-top: var(--sp-extra-tight);
 | 
			
		||||
  padding-bottom: calc(2 * var(--sp-normal));
 | 
			
		||||
  
 | 
			
		||||
  & .people-selector {
 | 
			
		||||
    padding: var(--sp-extra-tight);
 | 
			
		||||
    border-radius: var(--bo-radius);
 | 
			
		||||
    &__container {
 | 
			
		||||
      @include dir.side(margin, var(--sp-extra-tight), 0);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  & .segmented-controls {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    margin-bottom: var(--sp-extra-tight);
 | 
			
		||||
    @include dir.side(margin, var(--sp-extra-tight), 0);
 | 
			
		||||
  }
 | 
			
		||||
  & .segment-btn {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    padding: var(--sp-ultra-tight) 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.people-drawer__load-more {
 | 
			
		||||
  padding: var(--sp-normal) 0 0;
 | 
			
		||||
  @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
 | 
			
		||||
 | 
			
		||||
  & .btn-surface {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,20 +0,0 @@
 | 
			
		|||
@use '../../partials/flex';
 | 
			
		||||
@use '../../partials/screen';
 | 
			
		||||
 | 
			
		||||
.room {
 | 
			
		||||
  @extend .cp-fx__row;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  flex-grow: 1;
 | 
			
		||||
 | 
			
		||||
  &__content {
 | 
			
		||||
    @extend .cp-fx__item-one;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.room .people-drawer {
 | 
			
		||||
  @include screen.smallerThan(tabletBreakpoint) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -5,7 +5,6 @@ import './RoomSettings.scss';
 | 
			
		|||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import * as roomActions from '../../../client/action/room';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import Tabs from '../../atoms/tabs/Tabs';
 | 
			
		||||
| 
						 | 
				
			
			@ -86,7 +85,7 @@ function GeneralSettings({ roomId }) {
 | 
			
		|||
              'danger'
 | 
			
		||||
            );
 | 
			
		||||
            if (!isConfirmed) return;
 | 
			
		||||
            roomActions.leave(roomId);
 | 
			
		||||
            mx.leave(roomId);
 | 
			
		||||
          }}
 | 
			
		||||
          iconSrc={LeaveArrowIC}
 | 
			
		||||
        >
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,46 +0,0 @@
 | 
			
		|||
@use '../../partials/flex';
 | 
			
		||||
@use '../../partials/screen';
 | 
			
		||||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.room-view {
 | 
			
		||||
  @extend .cp-fx__column;
 | 
			
		||||
  background-color: var(--bg-surface);
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  z-index: 999;
 | 
			
		||||
  box-shadow: none;
 | 
			
		||||
 | 
			
		||||
  transition: transform 200ms var(--fluid-slide-down);
 | 
			
		||||
 | 
			
		||||
  &--dropped {
 | 
			
		||||
    transform: translateY(calc(100% - var(--header-height)));
 | 
			
		||||
    border-radius: var(--bo-radius) var(--bo-radius) 0 0;
 | 
			
		||||
    box-shadow: var(--bs-popup);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & .header {
 | 
			
		||||
    @include screen.smallerThan(mobileBreakpoint) {
 | 
			
		||||
      padding: 0 var(--sp-tight);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__content-wrapper {
 | 
			
		||||
    @extend .cp-fx__item-one;
 | 
			
		||||
    @extend .cp-fx__column;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__scrollable {
 | 
			
		||||
    @extend .cp-fx__item-one;
 | 
			
		||||
    position: relative;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__sticky {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    background: var(--bg-surface);
 | 
			
		||||
  }
 | 
			
		||||
  &__editor {
 | 
			
		||||
    padding: 0 var(--sp-normal);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,297 +0,0 @@
 | 
			
		|||
/* eslint-disable react/prop-types */
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './RoomViewCmdBar.scss';
 | 
			
		||||
import parse from 'html-react-parser';
 | 
			
		||||
import twemoji from 'twemoji';
 | 
			
		||||
 | 
			
		||||
import { twemojify, TWEMOJI_BASE_URL } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import { getEmojiForCompletion } from '../emoji-board/custom-emoji';
 | 
			
		||||
import AsyncSearch from '../../../util/AsyncSearch';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import ScrollView from '../../atoms/scroll/ScrollView';
 | 
			
		||||
import FollowingMembers from '../../molecules/following-members/FollowingMembers';
 | 
			
		||||
import { addRecentEmoji, getRecentEmojis } from '../emoji-board/recent';
 | 
			
		||||
import commands from './commands';
 | 
			
		||||
 | 
			
		||||
function CmdItem({ onClick, children }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <button className="cmd-item" onClick={onClick} type="button">
 | 
			
		||||
      {children}
 | 
			
		||||
    </button>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
CmdItem.propTypes = {
 | 
			
		||||
  onClick: PropTypes.func.isRequired,
 | 
			
		||||
  children: PropTypes.node.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
 | 
			
		||||
  function renderCmdSuggestions(cmdPrefix, cmds) {
 | 
			
		||||
    const cmdOptString = typeof option === 'string' ? `/${option}` : '/?';
 | 
			
		||||
    return cmds.map((cmd) => (
 | 
			
		||||
      <CmdItem
 | 
			
		||||
        key={cmd}
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          fireCmd({
 | 
			
		||||
            prefix: cmdPrefix,
 | 
			
		||||
            option,
 | 
			
		||||
            result: commands[cmd],
 | 
			
		||||
          });
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Text variant="b2">{`${cmd}${cmd.isOptions ? cmdOptString : ''}`}</Text>
 | 
			
		||||
      </CmdItem>
 | 
			
		||||
    ));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function renderEmojiSuggestion(emPrefix, emos) {
 | 
			
		||||
    const mx = initMatrix.matrixClient;
 | 
			
		||||
 | 
			
		||||
    // Renders a small Twemoji
 | 
			
		||||
    function renderTwemoji(emoji) {
 | 
			
		||||
      return parse(
 | 
			
		||||
        twemoji.parse(emoji.unicode, {
 | 
			
		||||
          attributes: () => ({
 | 
			
		||||
            unicode: emoji.unicode,
 | 
			
		||||
            shortcodes: emoji.shortcodes?.toString(),
 | 
			
		||||
          }),
 | 
			
		||||
          base: TWEMOJI_BASE_URL,
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Render a custom emoji
 | 
			
		||||
    function renderCustomEmoji(emoji) {
 | 
			
		||||
      return (
 | 
			
		||||
        <img
 | 
			
		||||
          className="emoji"
 | 
			
		||||
          src={mx.mxcUrlToHttp(emoji.mxc)}
 | 
			
		||||
          data-mx-emoticon=""
 | 
			
		||||
          alt={`:${emoji.shortcode}:`}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Dynamically render either a custom emoji or twemoji based on what the input is
 | 
			
		||||
    function renderEmoji(emoji) {
 | 
			
		||||
      if (emoji.mxc) {
 | 
			
		||||
        return renderCustomEmoji(emoji);
 | 
			
		||||
      }
 | 
			
		||||
      return renderTwemoji(emoji);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return emos.map((emoji) => (
 | 
			
		||||
      <CmdItem
 | 
			
		||||
        key={emoji.shortcode}
 | 
			
		||||
        onClick={() =>
 | 
			
		||||
          fireCmd({
 | 
			
		||||
            prefix: emPrefix,
 | 
			
		||||
            result: emoji,
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        <Text variant="b1">{renderEmoji(emoji)}</Text>
 | 
			
		||||
        <Text variant="b2">{`:${emoji.shortcode}:`}</Text>
 | 
			
		||||
      </CmdItem>
 | 
			
		||||
    ));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function renderNameSuggestion(namePrefix, members) {
 | 
			
		||||
    return members.map((member) => (
 | 
			
		||||
      <CmdItem
 | 
			
		||||
        key={member.userId}
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          fireCmd({
 | 
			
		||||
            prefix: namePrefix,
 | 
			
		||||
            result: member,
 | 
			
		||||
          });
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Text variant="b2">{twemojify(member.name)}</Text>
 | 
			
		||||
      </CmdItem>
 | 
			
		||||
    ));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const cmd = {
 | 
			
		||||
    '/': (cmds) => renderCmdSuggestions(prefix, cmds),
 | 
			
		||||
    ':': (emos) => renderEmojiSuggestion(prefix, emos),
 | 
			
		||||
    '@': (members) => renderNameSuggestion(prefix, members),
 | 
			
		||||
  };
 | 
			
		||||
  return cmd[prefix]?.(suggestions);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const asyncSearch = new AsyncSearch();
 | 
			
		||||
let cmdPrefix;
 | 
			
		||||
let cmdOption;
 | 
			
		||||
function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
 | 
			
		||||
  const [cmd, setCmd] = useState(null);
 | 
			
		||||
 | 
			
		||||
  function displaySuggestions(suggestions) {
 | 
			
		||||
    if (suggestions.length === 0) {
 | 
			
		||||
      setCmd({ prefix: cmd?.prefix || cmdPrefix, error: 'No suggestion found.' });
 | 
			
		||||
      viewEvent.emit('cmd_error');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setCmd({ prefix: cmd?.prefix || cmdPrefix, suggestions, option: cmdOption });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function processCmd(prefix, slug) {
 | 
			
		||||
    let searchTerm = slug;
 | 
			
		||||
    cmdOption = undefined;
 | 
			
		||||
    cmdPrefix = prefix;
 | 
			
		||||
    if (prefix === '/') {
 | 
			
		||||
      const cmdSlugParts = slug.split('/');
 | 
			
		||||
      [searchTerm, cmdOption] = cmdSlugParts;
 | 
			
		||||
    }
 | 
			
		||||
    if (prefix === ':') {
 | 
			
		||||
      if (searchTerm.length <= 3) {
 | 
			
		||||
        if (searchTerm.match(/^[-]?(\))$/)) searchTerm = 'smile';
 | 
			
		||||
        else if (searchTerm.match(/^[-]?(s|S)$/)) searchTerm = 'confused';
 | 
			
		||||
        else if (searchTerm.match(/^[-]?(o|O|0)$/)) searchTerm = 'astonished';
 | 
			
		||||
        else if (searchTerm.match(/^[-]?(\|)$/)) searchTerm = 'neutral_face';
 | 
			
		||||
        else if (searchTerm.match(/^[-]?(d|D)$/)) searchTerm = 'grin';
 | 
			
		||||
        else if (searchTerm.match(/^[-]?(\/)$/)) searchTerm = 'frown';
 | 
			
		||||
        else if (searchTerm.match(/^[-]?(p|P)$/)) searchTerm = 'stuck_out_tongue';
 | 
			
		||||
        else if (searchTerm.match(/^'[-]?(\()$/)) searchTerm = 'cry';
 | 
			
		||||
        else if (searchTerm.match(/^[-]?(x|X)$/)) searchTerm = 'dizzy_face';
 | 
			
		||||
        else if (searchTerm.match(/^[-]?(\()$/)) searchTerm = 'pleading_face';
 | 
			
		||||
        else if (searchTerm.match(/^[-]?(\$)$/)) searchTerm = 'money';
 | 
			
		||||
        else if (searchTerm.match(/^(<3)$/)) searchTerm = 'heart';
 | 
			
		||||
        else if (searchTerm.match(/^(c|ca|cat)$/)) searchTerm = '_cat';
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    asyncSearch.search(searchTerm);
 | 
			
		||||
  }
 | 
			
		||||
  function activateCmd(prefix) {
 | 
			
		||||
    cmdPrefix = prefix;
 | 
			
		||||
    cmdPrefix = undefined;
 | 
			
		||||
 | 
			
		||||
    const mx = initMatrix.matrixClient;
 | 
			
		||||
    const setupSearch = {
 | 
			
		||||
      '/': () => {
 | 
			
		||||
        asyncSearch.setup(Object.keys(commands), { isContain: true });
 | 
			
		||||
        setCmd({ prefix, suggestions: Object.keys(commands) });
 | 
			
		||||
      },
 | 
			
		||||
      ':': () => {
 | 
			
		||||
        const parentIds = initMatrix.roomList.getAllParentSpaces(roomId);
 | 
			
		||||
        const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
 | 
			
		||||
        const emojis = getEmojiForCompletion(mx, [mx.getRoom(roomId), ...parentRooms]);
 | 
			
		||||
        const recentEmoji = getRecentEmojis(20);
 | 
			
		||||
        asyncSearch.setup(emojis, { keys: ['shortcode'], isContain: true, limit: 20 });
 | 
			
		||||
        setCmd({
 | 
			
		||||
          prefix,
 | 
			
		||||
          suggestions: recentEmoji.length > 0 ? recentEmoji : emojis.slice(26, 46),
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
      '@': () => {
 | 
			
		||||
        const members = mx
 | 
			
		||||
          .getRoom(roomId)
 | 
			
		||||
          .getJoinedMembers()
 | 
			
		||||
          .map((member) => ({
 | 
			
		||||
            name: member.name,
 | 
			
		||||
            userId: member.userId.slice(1),
 | 
			
		||||
          }));
 | 
			
		||||
        asyncSearch.setup(members, { keys: ['name', 'userId'], limit: 20 });
 | 
			
		||||
        const endIndex = members.length > 20 ? 20 : members.length;
 | 
			
		||||
        setCmd({ prefix, suggestions: members.slice(0, endIndex) });
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
    setupSearch[prefix]?.();
 | 
			
		||||
  }
 | 
			
		||||
  function deactivateCmd() {
 | 
			
		||||
    setCmd(null);
 | 
			
		||||
    cmdOption = undefined;
 | 
			
		||||
    cmdPrefix = undefined;
 | 
			
		||||
  }
 | 
			
		||||
  function fireCmd(myCmd) {
 | 
			
		||||
    if (myCmd.prefix === '/') {
 | 
			
		||||
      viewEvent.emit('cmd_fired', {
 | 
			
		||||
        replace: `/${myCmd.result.name}`,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (myCmd.prefix === ':') {
 | 
			
		||||
      if (!myCmd.result.mxc) addRecentEmoji(myCmd.result.unicode);
 | 
			
		||||
      viewEvent.emit('cmd_fired', {
 | 
			
		||||
        replace: myCmd.result.mxc ? `:${myCmd.result.shortcode}: ` : myCmd.result.unicode,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (myCmd.prefix === '@') {
 | 
			
		||||
      viewEvent.emit('cmd_fired', {
 | 
			
		||||
        replace: `@${myCmd.result.userId}`,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    deactivateCmd();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function listenKeyboard(event) {
 | 
			
		||||
    const { activeElement } = document;
 | 
			
		||||
    const lastCmdItem = document.activeElement.parentNode.lastElementChild;
 | 
			
		||||
    if (event.key === 'Escape') {
 | 
			
		||||
      if (activeElement.className !== 'cmd-item') return;
 | 
			
		||||
      viewEvent.emit('focus_msg_input');
 | 
			
		||||
    }
 | 
			
		||||
    if (event.key === 'Tab') {
 | 
			
		||||
      if (lastCmdItem.className !== 'cmd-item') return;
 | 
			
		||||
      if (lastCmdItem !== activeElement) return;
 | 
			
		||||
      if (event.shiftKey) return;
 | 
			
		||||
      viewEvent.emit('focus_msg_input');
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    viewEvent.on('cmd_activate', activateCmd);
 | 
			
		||||
    viewEvent.on('cmd_deactivate', deactivateCmd);
 | 
			
		||||
    return () => {
 | 
			
		||||
      deactivateCmd();
 | 
			
		||||
      viewEvent.removeListener('cmd_activate', activateCmd);
 | 
			
		||||
      viewEvent.removeListener('cmd_deactivate', deactivateCmd);
 | 
			
		||||
    };
 | 
			
		||||
  }, [roomId]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (cmd !== null) document.body.addEventListener('keydown', listenKeyboard);
 | 
			
		||||
    viewEvent.on('cmd_process', processCmd);
 | 
			
		||||
    asyncSearch.on(asyncSearch.RESULT_SENT, displaySuggestions);
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (cmd !== null) document.body.removeEventListener('keydown', listenKeyboard);
 | 
			
		||||
 | 
			
		||||
      viewEvent.removeListener('cmd_process', processCmd);
 | 
			
		||||
      asyncSearch.removeListener(asyncSearch.RESULT_SENT, displaySuggestions);
 | 
			
		||||
    };
 | 
			
		||||
  }, [cmd]);
 | 
			
		||||
 | 
			
		||||
  const isError = typeof cmd?.error === 'string';
 | 
			
		||||
  if (cmd === null || isError) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="cmd-bar">
 | 
			
		||||
        <FollowingMembers roomTimeline={roomTimeline} />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="cmd-bar">
 | 
			
		||||
      <div className="cmd-bar__info">
 | 
			
		||||
        <Text variant="b3">TAB</Text>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="cmd-bar__content">
 | 
			
		||||
        <ScrollView horizontal vertical={false} invisible>
 | 
			
		||||
          <div className="cmd-bar__content-suggestions">{renderSuggestions(cmd, fireCmd)}</div>
 | 
			
		||||
        </ScrollView>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
RoomViewCmdBar.propTypes = {
 | 
			
		||||
  roomId: PropTypes.string.isRequired,
 | 
			
		||||
  roomTimeline: PropTypes.shape({}).isRequired,
 | 
			
		||||
  viewEvent: PropTypes.shape({}).isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default RoomViewCmdBar;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,57 +0,0 @@
 | 
			
		|||
@use '../../partials/flex';
 | 
			
		||||
@use '../../partials/text';
 | 
			
		||||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.cmd-bar {
 | 
			
		||||
  --cmd-bar-height: 28px;
 | 
			
		||||
  min-height: var(--cmd-bar-height);
 | 
			
		||||
  display: flex;
 | 
			
		||||
 | 
			
		||||
  &__info {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    width: 40px;
 | 
			
		||||
    @include dir.side(margin, 14px, 10px);
 | 
			
		||||
 | 
			
		||||
    & > * {
 | 
			
		||||
      margin: auto;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__content {
 | 
			
		||||
    @extend .cp-fx__item-one;
 | 
			
		||||
    display: flex;
 | 
			
		||||
 | 
			
		||||
    &-suggestions {
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
 | 
			
		||||
      & > .text {
 | 
			
		||||
        @extend .cp-txt__ellipsis;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.cmd-item {
 | 
			
		||||
  --cmd-item-bar: inset 0 -2px 0 0 var(--bg-caution);
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  @include dir.side(margin, 0, var(--sp-extra-tight));
 | 
			
		||||
  padding: 0 var(--sp-extra-tight);
 | 
			
		||||
  border-radius: var(--bo-radius) var(--bo-radius) 0 0;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background-color: var(--bg-caution-hover);
 | 
			
		||||
  }
 | 
			
		||||
  &:focus {
 | 
			
		||||
    background-color: var(--bg-caution-active);
 | 
			
		||||
    box-shadow: var(--cmd-item-bar);
 | 
			
		||||
    border-bottom: 2px solid transparent;
 | 
			
		||||
    outline: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,644 +0,0 @@
 | 
			
		|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
 | 
			
		||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
 | 
			
		||||
/* eslint-disable react/prop-types */
 | 
			
		||||
import React, {
 | 
			
		||||
  useState, useEffect, useLayoutEffect, useCallback, useRef,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './RoomViewContent.scss';
 | 
			
		||||
 | 
			
		||||
import dateFormat from 'dateformat';
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import { openProfileViewer } from '../../../client/action/navigation';
 | 
			
		||||
import { diffMinutes, isInSameDay, Throttle } from '../../../util/common';
 | 
			
		||||
import { markAsRead } from '../../../client/action/notifications';
 | 
			
		||||
 | 
			
		||||
import Divider from '../../atoms/divider/Divider';
 | 
			
		||||
import ScrollView from '../../atoms/scroll/ScrollView';
 | 
			
		||||
import { Message, PlaceholderMessage } from '../../molecules/message/Message';
 | 
			
		||||
import RoomIntro from '../../molecules/room-intro/RoomIntro';
 | 
			
		||||
import TimelineChange from '../../molecules/message/TimelineChange';
 | 
			
		||||
 | 
			
		||||
import { useStore } from '../../hooks/useStore';
 | 
			
		||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
 | 
			
		||||
import { parseTimelineChange } from './common';
 | 
			
		||||
import TimelineScroll from './TimelineScroll';
 | 
			
		||||
import EventLimit from './EventLimit';
 | 
			
		||||
import { getResizeObserverEntry, useResizeObserver } from '../../hooks/useResizeObserver';
 | 
			
		||||
 | 
			
		||||
const PAG_LIMIT = 30;
 | 
			
		||||
const MAX_MSG_DIFF_MINUTES = 5;
 | 
			
		||||
const PLACEHOLDER_COUNT = 2;
 | 
			
		||||
const PLACEHOLDERS_HEIGHT = 96 * PLACEHOLDER_COUNT;
 | 
			
		||||
const SCROLL_TRIGGER_POS = PLACEHOLDERS_HEIGHT * 4;
 | 
			
		||||
 | 
			
		||||
function loadingMsgPlaceholders(key, count = 2) {
 | 
			
		||||
  const pl = [];
 | 
			
		||||
  const genPlaceholders = () => {
 | 
			
		||||
    for (let i = 0; i < count; i += 1) {
 | 
			
		||||
      pl.push(<PlaceholderMessage key={`placeholder-${i}${key}`} />);
 | 
			
		||||
    }
 | 
			
		||||
    return pl;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <React.Fragment key={`placeholder-container${key}`}>
 | 
			
		||||
      {genPlaceholders()}
 | 
			
		||||
    </React.Fragment>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function RoomIntroContainer({ event, timeline }) {
 | 
			
		||||
  const [, nameForceUpdate] = useForceUpdate();
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const { roomList } = initMatrix;
 | 
			
		||||
  const { room } = timeline;
 | 
			
		||||
  const roomTopic = room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
 | 
			
		||||
  const isDM = roomList.directs.has(timeline.roomId);
 | 
			
		||||
  let avatarSrc = room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop');
 | 
			
		||||
  avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc;
 | 
			
		||||
 | 
			
		||||
  const heading = isDM ? room.name : `Welcome to ${room.name}`;
 | 
			
		||||
  const topic = twemojify(roomTopic || '', undefined, true);
 | 
			
		||||
  const nameJsx = twemojify(room.name);
 | 
			
		||||
  const desc = isDM
 | 
			
		||||
    ? (
 | 
			
		||||
      <>
 | 
			
		||||
        This is the beginning of your direct message history with @
 | 
			
		||||
        <b>{nameJsx}</b>
 | 
			
		||||
        {'. '}
 | 
			
		||||
        {topic}
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
    : (
 | 
			
		||||
      <>
 | 
			
		||||
        {'This is the beginning of the '}
 | 
			
		||||
        <b>{nameJsx}</b>
 | 
			
		||||
        {' room. '}
 | 
			
		||||
        {topic}
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleUpdate = () => nameForceUpdate();
 | 
			
		||||
 | 
			
		||||
    roomList.on(cons.events.roomList.ROOM_PROFILE_UPDATED, handleUpdate);
 | 
			
		||||
    return () => {
 | 
			
		||||
      roomList.removeListener(cons.events.roomList.ROOM_PROFILE_UPDATED, handleUpdate);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <RoomIntro
 | 
			
		||||
      roomId={timeline.roomId}
 | 
			
		||||
      avatarSrc={avatarSrc}
 | 
			
		||||
      name={room.name}
 | 
			
		||||
      heading={twemojify(heading)}
 | 
			
		||||
      desc={desc}
 | 
			
		||||
      time={event ? `Created at ${dateFormat(event.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleOnClickCapture(e) {
 | 
			
		||||
  const { target, nativeEvent } = e;
 | 
			
		||||
 | 
			
		||||
  const userId = target.getAttribute('data-mx-pill');
 | 
			
		||||
  if (userId) {
 | 
			
		||||
    const roomId = navigation.selectedRoomId;
 | 
			
		||||
    openProfileViewer(userId, roomId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const spoiler = nativeEvent.composedPath().find((el) => el?.hasAttribute?.('data-mx-spoiler'));
 | 
			
		||||
  if (spoiler) {
 | 
			
		||||
    if (!spoiler.classList.contains('data-mx-spoiler--visible')) e.preventDefault();
 | 
			
		||||
    spoiler.classList.toggle('data-mx-spoiler--visible');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderEvent(
 | 
			
		||||
  roomTimeline,
 | 
			
		||||
  mEvent,
 | 
			
		||||
  prevMEvent,
 | 
			
		||||
  isFocus,
 | 
			
		||||
  isEdit,
 | 
			
		||||
  setEdit,
 | 
			
		||||
  cancelEdit,
 | 
			
		||||
) {
 | 
			
		||||
  const isBodyOnly = (prevMEvent !== null
 | 
			
		||||
    && prevMEvent.getSender() === mEvent.getSender()
 | 
			
		||||
    && prevMEvent.getType() !== 'm.room.member'
 | 
			
		||||
    && prevMEvent.getType() !== 'm.room.create'
 | 
			
		||||
    && diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES
 | 
			
		||||
  );
 | 
			
		||||
  const timestamp = mEvent.getTs();
 | 
			
		||||
 | 
			
		||||
  if (mEvent.getType() === 'm.room.member') {
 | 
			
		||||
    const timelineChange = parseTimelineChange(mEvent);
 | 
			
		||||
    if (timelineChange === null) return <div key={mEvent.getId()} />;
 | 
			
		||||
    return (
 | 
			
		||||
      <TimelineChange
 | 
			
		||||
        key={mEvent.getId()}
 | 
			
		||||
        variant={timelineChange.variant}
 | 
			
		||||
        content={timelineChange.content}
 | 
			
		||||
        timestamp={timestamp}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <Message
 | 
			
		||||
      key={mEvent.getId()}
 | 
			
		||||
      mEvent={mEvent}
 | 
			
		||||
      isBodyOnly={isBodyOnly}
 | 
			
		||||
      roomTimeline={roomTimeline}
 | 
			
		||||
      focus={isFocus}
 | 
			
		||||
      fullTime={false}
 | 
			
		||||
      isEdit={isEdit}
 | 
			
		||||
      setEdit={setEdit}
 | 
			
		||||
      cancelEdit={cancelEdit}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef) {
 | 
			
		||||
  const [timelineInfo, setTimelineInfo] = useState(null);
 | 
			
		||||
 | 
			
		||||
  const setEventTimeline = async (eId) => {
 | 
			
		||||
    if (typeof eId === 'string') {
 | 
			
		||||
      const isLoaded = await roomTimeline.loadEventTimeline(eId);
 | 
			
		||||
      if (isLoaded) return;
 | 
			
		||||
      // if eventTimeline failed to load,
 | 
			
		||||
      // we will load live timeline as fallback.
 | 
			
		||||
    }
 | 
			
		||||
    roomTimeline.loadLiveTimeline();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const limit = eventLimitRef.current;
 | 
			
		||||
    const initTimeline = (eId) => {
 | 
			
		||||
      // NOTICE: eId can be id of readUpto, reply or specific event.
 | 
			
		||||
      // readUpTo: when user click jump to unread message button.
 | 
			
		||||
      // reply: when user click reply from timeline.
 | 
			
		||||
      // specific event when user open a link of event. behave same as ^^^^
 | 
			
		||||
      const readUpToId = roomTimeline.getReadUpToEventId();
 | 
			
		||||
      let focusEventIndex = -1;
 | 
			
		||||
      const isSpecificEvent = eId && eId !== readUpToId;
 | 
			
		||||
 | 
			
		||||
      if (isSpecificEvent) {
 | 
			
		||||
        focusEventIndex = roomTimeline.getEventIndex(eId);
 | 
			
		||||
      }
 | 
			
		||||
      if (!readUptoEvtStore.getItem() && roomTimeline.hasEventInTimeline(readUpToId)) {
 | 
			
		||||
        // either opening live timeline or jump to unread.
 | 
			
		||||
        readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
 | 
			
		||||
      }
 | 
			
		||||
      if (readUptoEvtStore.getItem() && !isSpecificEvent) {
 | 
			
		||||
        focusEventIndex = roomTimeline.getUnreadEventIndex(readUptoEvtStore.getItem().getId());
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (focusEventIndex > -1) {
 | 
			
		||||
        limit.setFrom(focusEventIndex - Math.round(limit.maxEvents / 2));
 | 
			
		||||
      } else {
 | 
			
		||||
        limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
 | 
			
		||||
      }
 | 
			
		||||
      setTimelineInfo({ focusEventId: isSpecificEvent ? eId : null });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    roomTimeline.on(cons.events.roomTimeline.READY, initTimeline);
 | 
			
		||||
    setEventTimeline(eventId);
 | 
			
		||||
    return () => {
 | 
			
		||||
      roomTimeline.removeListener(cons.events.roomTimeline.READY, initTimeline);
 | 
			
		||||
      limit.setFrom(0);
 | 
			
		||||
    };
 | 
			
		||||
  }, [roomTimeline, eventId]);
 | 
			
		||||
 | 
			
		||||
  return timelineInfo;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function usePaginate(
 | 
			
		||||
  roomTimeline,
 | 
			
		||||
  readUptoEvtStore,
 | 
			
		||||
  forceUpdateLimit,
 | 
			
		||||
  timelineScrollRef,
 | 
			
		||||
  eventLimitRef,
 | 
			
		||||
) {
 | 
			
		||||
  const [info, setInfo] = useState(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handlePaginatedFromServer = (backwards, loaded) => {
 | 
			
		||||
      const limit = eventLimitRef.current;
 | 
			
		||||
      if (loaded === 0) return;
 | 
			
		||||
      if (!readUptoEvtStore.getItem()) {
 | 
			
		||||
        const readUpToId = roomTimeline.getReadUpToEventId();
 | 
			
		||||
        readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
 | 
			
		||||
      }
 | 
			
		||||
      limit.paginate(backwards, PAG_LIMIT, roomTimeline.timeline.length);
 | 
			
		||||
      setTimeout(() => setInfo({
 | 
			
		||||
        backwards,
 | 
			
		||||
        loaded,
 | 
			
		||||
      }));
 | 
			
		||||
    };
 | 
			
		||||
    roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
 | 
			
		||||
    return () => {
 | 
			
		||||
      roomTimeline.removeListener(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
 | 
			
		||||
    };
 | 
			
		||||
  }, [roomTimeline]);
 | 
			
		||||
 | 
			
		||||
  const autoPaginate = useCallback(async () => {
 | 
			
		||||
    const timelineScroll = timelineScrollRef.current;
 | 
			
		||||
    const limit = eventLimitRef.current;
 | 
			
		||||
    if (roomTimeline.isOngoingPagination) return;
 | 
			
		||||
    const tLength = roomTimeline.timeline.length;
 | 
			
		||||
 | 
			
		||||
    if (timelineScroll.bottom < SCROLL_TRIGGER_POS) {
 | 
			
		||||
      if (limit.length < tLength) {
 | 
			
		||||
        // paginate from memory
 | 
			
		||||
        limit.paginate(false, PAG_LIMIT, tLength);
 | 
			
		||||
        forceUpdateLimit();
 | 
			
		||||
      } else if (roomTimeline.canPaginateForward()) {
 | 
			
		||||
        // paginate from server.
 | 
			
		||||
        await roomTimeline.paginateTimeline(false, PAG_LIMIT);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (timelineScroll.top < SCROLL_TRIGGER_POS) {
 | 
			
		||||
      if (limit.from > 0) {
 | 
			
		||||
        // paginate from memory
 | 
			
		||||
        limit.paginate(true, PAG_LIMIT, tLength);
 | 
			
		||||
        forceUpdateLimit();
 | 
			
		||||
      } else if (roomTimeline.canPaginateBackward()) {
 | 
			
		||||
        // paginate from server.
 | 
			
		||||
        await roomTimeline.paginateTimeline(true, PAG_LIMIT);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [roomTimeline]);
 | 
			
		||||
 | 
			
		||||
  return [info, autoPaginate];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useHandleScroll(
 | 
			
		||||
  roomTimeline,
 | 
			
		||||
  autoPaginate,
 | 
			
		||||
  readUptoEvtStore,
 | 
			
		||||
  forceUpdateLimit,
 | 
			
		||||
  timelineScrollRef,
 | 
			
		||||
  eventLimitRef,
 | 
			
		||||
) {
 | 
			
		||||
  const handleScroll = useCallback(() => {
 | 
			
		||||
    const timelineScroll = timelineScrollRef.current;
 | 
			
		||||
    const limit = eventLimitRef.current;
 | 
			
		||||
    requestAnimationFrame(() => {
 | 
			
		||||
      // emit event to toggle scrollToBottom button visibility
 | 
			
		||||
      const isAtBottom = (
 | 
			
		||||
        timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()
 | 
			
		||||
        && limit.length >= roomTimeline.timeline.length
 | 
			
		||||
      );
 | 
			
		||||
      roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
 | 
			
		||||
      if (isAtBottom && readUptoEvtStore.getItem()) {
 | 
			
		||||
        requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    autoPaginate();
 | 
			
		||||
  }, [roomTimeline]);
 | 
			
		||||
 | 
			
		||||
  const handleScrollToLive = useCallback(() => {
 | 
			
		||||
    const timelineScroll = timelineScrollRef.current;
 | 
			
		||||
    const limit = eventLimitRef.current;
 | 
			
		||||
    if (readUptoEvtStore.getItem()) {
 | 
			
		||||
      requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
 | 
			
		||||
    }
 | 
			
		||||
    if (roomTimeline.isServingLiveTimeline()) {
 | 
			
		||||
      limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
 | 
			
		||||
      timelineScroll.scrollToBottom();
 | 
			
		||||
      forceUpdateLimit();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    roomTimeline.loadLiveTimeline();
 | 
			
		||||
  }, [roomTimeline]);
 | 
			
		||||
 | 
			
		||||
  return [handleScroll, handleScrollToLive];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef) {
 | 
			
		||||
  const myUserId = initMatrix.matrixClient.getUserId();
 | 
			
		||||
  const [newEvent, setEvent] = useState(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const timelineScroll = timelineScrollRef.current;
 | 
			
		||||
    const limit = eventLimitRef.current;
 | 
			
		||||
    const trySendReadReceipt = (event) => {
 | 
			
		||||
      if (myUserId === event.getSender()) {
 | 
			
		||||
        requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const readUpToEvent = readUptoEvtStore.getItem();
 | 
			
		||||
      const readUpToId = roomTimeline.getReadUpToEventId();
 | 
			
		||||
      const isUnread = readUpToEvent ? readUpToEvent?.getId() === readUpToId : true;
 | 
			
		||||
 | 
			
		||||
      if (isUnread === false) {
 | 
			
		||||
        if (document.visibilityState === 'visible' && timelineScroll.bottom < 16) {
 | 
			
		||||
          requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
 | 
			
		||||
        } else {
 | 
			
		||||
          readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
 | 
			
		||||
        }
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const { timeline } = roomTimeline;
 | 
			
		||||
      const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToId;
 | 
			
		||||
      if (unreadMsgIsLast) {
 | 
			
		||||
        requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleEvent = (event) => {
 | 
			
		||||
      const tLength = roomTimeline.timeline.length;
 | 
			
		||||
      const isViewingLive = roomTimeline.isServingLiveTimeline() && limit.length >= tLength - 1;
 | 
			
		||||
      const isAttached = timelineScroll.bottom < SCROLL_TRIGGER_POS;
 | 
			
		||||
 | 
			
		||||
      if (isViewingLive && isAttached && document.hasFocus()) {
 | 
			
		||||
        limit.setFrom(tLength - limit.maxEvents);
 | 
			
		||||
        trySendReadReceipt(event);
 | 
			
		||||
        setEvent(event);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const isRelates = (event.getType() === 'm.reaction' || event.getRelation()?.rel_type === 'm.replace');
 | 
			
		||||
      if (isRelates) {
 | 
			
		||||
        setEvent(event);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (isViewingLive) {
 | 
			
		||||
        // This stateUpdate will help to put the
 | 
			
		||||
        // loading msg placeholder at bottom
 | 
			
		||||
        setEvent(event);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleEventRedact = (event) => setEvent(event);
 | 
			
		||||
 | 
			
		||||
    roomTimeline.on(cons.events.roomTimeline.EVENT, handleEvent);
 | 
			
		||||
    roomTimeline.on(cons.events.roomTimeline.EVENT_REDACTED, handleEventRedact);
 | 
			
		||||
    return () => {
 | 
			
		||||
      roomTimeline.removeListener(cons.events.roomTimeline.EVENT, handleEvent);
 | 
			
		||||
      roomTimeline.removeListener(cons.events.roomTimeline.EVENT_REDACTED, handleEventRedact);
 | 
			
		||||
    };
 | 
			
		||||
  }, [roomTimeline]);
 | 
			
		||||
 | 
			
		||||
  return newEvent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let jumpToItemIndex = -1;
 | 
			
		||||
 | 
			
		||||
function RoomViewContent({ roomInputRef, eventId, roomTimeline }) {
 | 
			
		||||
  const [throttle] = useState(new Throttle());
 | 
			
		||||
 | 
			
		||||
  const timelineSVRef = useRef(null);
 | 
			
		||||
  const timelineScrollRef = useRef(null);
 | 
			
		||||
  const eventLimitRef = useRef(null);
 | 
			
		||||
  const [editEventId, setEditEventId] = useState(null);
 | 
			
		||||
  const cancelEdit = () => setEditEventId(null);
 | 
			
		||||
 | 
			
		||||
  const readUptoEvtStore = useStore(roomTimeline);
 | 
			
		||||
  const [onLimitUpdate, forceUpdateLimit] = useForceUpdate();
 | 
			
		||||
 | 
			
		||||
  const timelineInfo = useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef);
 | 
			
		||||
  const [paginateInfo, autoPaginate] = usePaginate(
 | 
			
		||||
    roomTimeline,
 | 
			
		||||
    readUptoEvtStore,
 | 
			
		||||
    forceUpdateLimit,
 | 
			
		||||
    timelineScrollRef,
 | 
			
		||||
    eventLimitRef,
 | 
			
		||||
  );
 | 
			
		||||
  const [handleScroll, handleScrollToLive] = useHandleScroll(
 | 
			
		||||
    roomTimeline,
 | 
			
		||||
    autoPaginate,
 | 
			
		||||
    readUptoEvtStore,
 | 
			
		||||
    forceUpdateLimit,
 | 
			
		||||
    timelineScrollRef,
 | 
			
		||||
    eventLimitRef,
 | 
			
		||||
  );
 | 
			
		||||
  const newEvent = useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef);
 | 
			
		||||
 | 
			
		||||
  const { timeline } = roomTimeline;
 | 
			
		||||
 | 
			
		||||
  useLayoutEffect(() => {
 | 
			
		||||
    if (!roomTimeline.initialized) {
 | 
			
		||||
      timelineScrollRef.current = new TimelineScroll(timelineSVRef.current);
 | 
			
		||||
      eventLimitRef.current = new EventLimit();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // when active timeline changes
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!roomTimeline.initialized) return undefined;
 | 
			
		||||
    const timelineScroll = timelineScrollRef.current;
 | 
			
		||||
 | 
			
		||||
    if (timeline.length > 0) {
 | 
			
		||||
      if (jumpToItemIndex === -1) {
 | 
			
		||||
        timelineScroll.scrollToBottom();
 | 
			
		||||
      } else {
 | 
			
		||||
        timelineScroll.scrollToIndex(jumpToItemIndex, 80);
 | 
			
		||||
      }
 | 
			
		||||
      if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) {
 | 
			
		||||
        const readUpToId = roomTimeline.getReadUpToEventId();
 | 
			
		||||
        if (readUptoEvtStore.getItem()?.getId() === readUpToId || readUpToId === null) {
 | 
			
		||||
          requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      jumpToItemIndex = -1;
 | 
			
		||||
    }
 | 
			
		||||
    autoPaginate();
 | 
			
		||||
 | 
			
		||||
    roomTimeline.on(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (timelineSVRef.current === null) return;
 | 
			
		||||
      roomTimeline.removeListener(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
 | 
			
		||||
    };
 | 
			
		||||
  }, [timelineInfo]);
 | 
			
		||||
 | 
			
		||||
  // when paginating from server
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!roomTimeline.initialized) return;
 | 
			
		||||
    const timelineScroll = timelineScrollRef.current;
 | 
			
		||||
    timelineScroll.tryRestoringScroll();
 | 
			
		||||
    autoPaginate();
 | 
			
		||||
  }, [paginateInfo]);
 | 
			
		||||
 | 
			
		||||
  // when paginating locally
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!roomTimeline.initialized) return;
 | 
			
		||||
    const timelineScroll = timelineScrollRef.current;
 | 
			
		||||
    timelineScroll.tryRestoringScroll();
 | 
			
		||||
  }, [onLimitUpdate]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const timelineScroll = timelineScrollRef.current;
 | 
			
		||||
    if (!roomTimeline.initialized) return;
 | 
			
		||||
    if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
 | 
			
		||||
      timelineScroll.scrollToBottom();
 | 
			
		||||
    } else {
 | 
			
		||||
      timelineScroll.tryRestoringScroll();
 | 
			
		||||
    }
 | 
			
		||||
  }, [newEvent]);
 | 
			
		||||
 | 
			
		||||
  useResizeObserver(
 | 
			
		||||
    useCallback((entries) => {
 | 
			
		||||
      if (!roomInputRef.current) return;
 | 
			
		||||
      const editorBaseEntry = getResizeObserverEntry(roomInputRef.current, entries);
 | 
			
		||||
      if (!editorBaseEntry) return;
 | 
			
		||||
 | 
			
		||||
      const timelineScroll = timelineScrollRef.current;
 | 
			
		||||
      if (!roomTimeline.initialized) return;
 | 
			
		||||
      if (timelineScroll.bottom < 40 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
 | 
			
		||||
        timelineScroll.scrollToBottom();
 | 
			
		||||
      }
 | 
			
		||||
    }, [roomInputRef]),
 | 
			
		||||
    useCallback(() => roomInputRef.current, [roomInputRef]),
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
  const listenKeyboard = useCallback((event) => {
 | 
			
		||||
    if (event.ctrlKey || event.altKey || event.metaKey) return;
 | 
			
		||||
    if (event.key !== 'ArrowUp') return;
 | 
			
		||||
    if (navigation.isRawModalVisible) return;
 | 
			
		||||
 | 
			
		||||
    if (document.activeElement.id !== 'message-textarea') return;
 | 
			
		||||
    if (document.activeElement.value !== '') return;
 | 
			
		||||
 | 
			
		||||
    const {
 | 
			
		||||
      timeline: tl, activeTimeline, liveTimeline, matrixClient: mx,
 | 
			
		||||
    } = roomTimeline;
 | 
			
		||||
    const limit = eventLimitRef.current;
 | 
			
		||||
    if (activeTimeline !== liveTimeline) return;
 | 
			
		||||
    if (tl.length > limit.length) return;
 | 
			
		||||
 | 
			
		||||
    const mTypes = ['m.text'];
 | 
			
		||||
    for (let i = tl.length - 1; i >= 0; i -= 1) {
 | 
			
		||||
      const mE = tl[i];
 | 
			
		||||
      if (
 | 
			
		||||
        mE.getSender() === mx.getUserId()
 | 
			
		||||
        && mE.getType() === 'm.room.message'
 | 
			
		||||
        && mTypes.includes(mE.getContent()?.msgtype)
 | 
			
		||||
      ) {
 | 
			
		||||
        setEditEventId(mE.getId());
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [roomTimeline]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.body.addEventListener('keydown', listenKeyboard);
 | 
			
		||||
    return () => {
 | 
			
		||||
      document.body.removeEventListener('keydown', listenKeyboard);
 | 
			
		||||
    };
 | 
			
		||||
  }, [listenKeyboard]);
 | 
			
		||||
 | 
			
		||||
  const handleTimelineScroll = (event) => {
 | 
			
		||||
    const timelineScroll = timelineScrollRef.current;
 | 
			
		||||
    if (!event.target) return;
 | 
			
		||||
 | 
			
		||||
    throttle._(() => {
 | 
			
		||||
      const backwards = timelineScroll?.calcScroll();
 | 
			
		||||
      if (typeof backwards !== 'boolean') return;
 | 
			
		||||
      handleScroll(backwards);
 | 
			
		||||
    }, 200)();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const renderTimeline = () => {
 | 
			
		||||
    const tl = [];
 | 
			
		||||
    const limit = eventLimitRef.current;
 | 
			
		||||
 | 
			
		||||
    let itemCountIndex = 0;
 | 
			
		||||
    jumpToItemIndex = -1;
 | 
			
		||||
    const readUptoEvent = readUptoEvtStore.getItem();
 | 
			
		||||
    let unreadDivider = false;
 | 
			
		||||
 | 
			
		||||
    if (roomTimeline.canPaginateBackward() || limit.from > 0) {
 | 
			
		||||
      tl.push(loadingMsgPlaceholders(1, PLACEHOLDER_COUNT));
 | 
			
		||||
      itemCountIndex += PLACEHOLDER_COUNT;
 | 
			
		||||
    }
 | 
			
		||||
    for (let i = limit.from; i < limit.length; i += 1) {
 | 
			
		||||
      if (i >= timeline.length) break;
 | 
			
		||||
      const mEvent = timeline[i];
 | 
			
		||||
      const prevMEvent = timeline[i - 1] ?? null;
 | 
			
		||||
 | 
			
		||||
      if (i === 0 && !roomTimeline.canPaginateBackward()) {
 | 
			
		||||
        if (mEvent.getType() === 'm.room.create') {
 | 
			
		||||
          tl.push(
 | 
			
		||||
            <RoomIntroContainer key={mEvent.getId()} event={mEvent} timeline={roomTimeline} />,
 | 
			
		||||
          );
 | 
			
		||||
          itemCountIndex += 1;
 | 
			
		||||
          // eslint-disable-next-line no-continue
 | 
			
		||||
          continue;
 | 
			
		||||
        } else {
 | 
			
		||||
          tl.push(<RoomIntroContainer key="room-intro" event={null} timeline={roomTimeline} />);
 | 
			
		||||
          itemCountIndex += 1;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let isNewEvent = false;
 | 
			
		||||
      if (!unreadDivider) {
 | 
			
		||||
        unreadDivider = (readUptoEvent
 | 
			
		||||
          && prevMEvent?.getTs() <= readUptoEvent.getTs()
 | 
			
		||||
          && readUptoEvent.getTs() < mEvent.getTs());
 | 
			
		||||
        if (unreadDivider) {
 | 
			
		||||
          isNewEvent = true;
 | 
			
		||||
          tl.push(<Divider key={`new-${mEvent.getId()}`} variant="positive" text="New messages" />);
 | 
			
		||||
          itemCountIndex += 1;
 | 
			
		||||
          if (jumpToItemIndex === -1) jumpToItemIndex = itemCountIndex;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      const dayDivider = prevMEvent && !isInSameDay(mEvent.getDate(), prevMEvent.getDate());
 | 
			
		||||
      if (dayDivider) {
 | 
			
		||||
        tl.push(<Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />);
 | 
			
		||||
        itemCountIndex += 1;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const focusId = timelineInfo.focusEventId;
 | 
			
		||||
      const isFocus = focusId === mEvent.getId();
 | 
			
		||||
      if (isFocus) jumpToItemIndex = itemCountIndex;
 | 
			
		||||
 | 
			
		||||
      tl.push(renderEvent(
 | 
			
		||||
        roomTimeline,
 | 
			
		||||
        mEvent,
 | 
			
		||||
        isNewEvent ? null : prevMEvent,
 | 
			
		||||
        isFocus,
 | 
			
		||||
        editEventId === mEvent.getId(),
 | 
			
		||||
        setEditEventId,
 | 
			
		||||
        cancelEdit,
 | 
			
		||||
      ));
 | 
			
		||||
      itemCountIndex += 1;
 | 
			
		||||
    }
 | 
			
		||||
    if (roomTimeline.canPaginateForward() || limit.length < timeline.length) {
 | 
			
		||||
      tl.push(loadingMsgPlaceholders(2, PLACEHOLDER_COUNT));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return tl;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ScrollView onScroll={handleTimelineScroll} ref={timelineSVRef} autoHide>
 | 
			
		||||
      <div className="room-view__content" onClick={handleOnClickCapture}>
 | 
			
		||||
        <div className="timeline__wrapper">
 | 
			
		||||
          { roomTimeline.initialized ? renderTimeline() : loadingMsgPlaceholders('loading', 3) }
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ScrollView>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RoomViewContent.defaultProps = {
 | 
			
		||||
  eventId: null,
 | 
			
		||||
};
 | 
			
		||||
RoomViewContent.propTypes = {
 | 
			
		||||
  eventId: PropTypes.string,
 | 
			
		||||
  roomTimeline: PropTypes.shape({}).isRequired,
 | 
			
		||||
  roomInputRef: PropTypes.shape({
 | 
			
		||||
    current: PropTypes.shape({})
 | 
			
		||||
  }).isRequired
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default RoomViewContent;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,30 +0,0 @@
 | 
			
		|||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.room-view__content {
 | 
			
		||||
  min-height: 100%;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  justify-content: flex-end;
 | 
			
		||||
 | 
			
		||||
  & .timeline__wrapper {
 | 
			
		||||
    --typing-noti-height: 28px;
 | 
			
		||||
    min-height: 0;
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
    padding-bottom: var(--typing-noti-height);
 | 
			
		||||
 | 
			
		||||
    & .message,
 | 
			
		||||
    & .ph-msg,
 | 
			
		||||
    & .timeline-change {
 | 
			
		||||
      @include dir.prop(border-radius,
 | 
			
		||||
      0 var(--bo-radius) var(--bo-radius) 0,
 | 
			
		||||
      var(--bo-radius) 0 0 var(--bo-radius),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    & > .divider {
 | 
			
		||||
      margin: var(--sp-extra-tight);
 | 
			
		||||
      @include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
 | 
			
		||||
      @include dir.side(padding, calc(var(--av-small) + var(--sp-tight)), 0);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,125 +0,0 @@
 | 
			
		|||
/* eslint-disable react/prop-types */
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './RoomViewFloating.scss';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import { markAsRead } from '../../../client/action/notifications';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import Button from '../../atoms/button/Button';
 | 
			
		||||
 | 
			
		||||
import MessageIC from '../../../../public/res/ic/outlined/message.svg';
 | 
			
		||||
import MessageUnreadIC from '../../../../public/res/ic/outlined/message-unread.svg';
 | 
			
		||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
 | 
			
		||||
 | 
			
		||||
import { getUsersActionJsx } from './common';
 | 
			
		||||
 | 
			
		||||
function useJumpToEvent(roomTimeline) {
 | 
			
		||||
  const [eventId, setEventId] = useState(null);
 | 
			
		||||
 | 
			
		||||
  const jumpToEvent = () => {
 | 
			
		||||
    roomTimeline.loadEventTimeline(eventId);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const cancelJumpToEvent = () => {
 | 
			
		||||
    markAsRead(roomTimeline.roomId);
 | 
			
		||||
    setEventId(null);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const readEventId = roomTimeline.getReadUpToEventId();
 | 
			
		||||
    // we only show "Jump to unread" btn only if the event is not in timeline.
 | 
			
		||||
    // if event is in timeline
 | 
			
		||||
    // we will automatically open the timeline from that event position
 | 
			
		||||
    if (!readEventId?.startsWith('~') && !roomTimeline.hasEventInTimeline(readEventId)) {
 | 
			
		||||
      setEventId(readEventId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { notifications } = initMatrix;
 | 
			
		||||
    const handleMarkAsRead = () => setEventId(null);
 | 
			
		||||
    notifications.on(cons.events.notifications.FULL_READ, handleMarkAsRead);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      notifications.removeListener(cons.events.notifications.FULL_READ, handleMarkAsRead);
 | 
			
		||||
      setEventId(null);
 | 
			
		||||
    };
 | 
			
		||||
  }, [roomTimeline]);
 | 
			
		||||
 | 
			
		||||
  return [!!eventId, jumpToEvent, cancelJumpToEvent];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useTypingMembers(roomTimeline) {
 | 
			
		||||
  const [typingMembers, setTypingMembers] = useState(new Set());
 | 
			
		||||
 | 
			
		||||
  const updateTyping = (members) => {
 | 
			
		||||
    const mx = initMatrix.matrixClient;
 | 
			
		||||
    members.delete(mx.getUserId());
 | 
			
		||||
    setTypingMembers(members);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setTypingMembers(new Set());
 | 
			
		||||
    roomTimeline.on(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, updateTyping);
 | 
			
		||||
    return () => {
 | 
			
		||||
      roomTimeline?.removeListener(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, updateTyping);
 | 
			
		||||
    };
 | 
			
		||||
  }, [roomTimeline]);
 | 
			
		||||
 | 
			
		||||
  return [typingMembers];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useScrollToBottom(roomTimeline) {
 | 
			
		||||
  const [isAtBottom, setIsAtBottom] = useState(true);
 | 
			
		||||
  const handleAtBottom = (atBottom) => setIsAtBottom(atBottom);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setIsAtBottom(true);
 | 
			
		||||
    roomTimeline.on(cons.events.roomTimeline.AT_BOTTOM, handleAtBottom);
 | 
			
		||||
    return () => roomTimeline.removeListener(cons.events.roomTimeline.AT_BOTTOM, handleAtBottom);
 | 
			
		||||
  }, [roomTimeline]);
 | 
			
		||||
 | 
			
		||||
  return [isAtBottom, setIsAtBottom];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function RoomViewFloating({
 | 
			
		||||
  roomId, roomTimeline,
 | 
			
		||||
}) {
 | 
			
		||||
  const [isJumpToEvent, jumpToEvent, cancelJumpToEvent] = useJumpToEvent(roomTimeline);
 | 
			
		||||
  const [typingMembers] = useTypingMembers(roomTimeline);
 | 
			
		||||
  const [isAtBottom, setIsAtBottom] = useScrollToBottom(roomTimeline);
 | 
			
		||||
 | 
			
		||||
  const handleScrollToBottom = () => {
 | 
			
		||||
    roomTimeline.emit(cons.events.roomTimeline.SCROLL_TO_LIVE);
 | 
			
		||||
    setIsAtBottom(true);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}>
 | 
			
		||||
        <Button iconSrc={MessageUnreadIC} onClick={jumpToEvent} variant="primary">
 | 
			
		||||
          <Text variant="b3" weight="medium">Jump to unread messages</Text>
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button iconSrc={TickMarkIC} onClick={cancelJumpToEvent} variant="primary">
 | 
			
		||||
          <Text variant="b3" weight="bold">Mark as read</Text>
 | 
			
		||||
        </Button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className={`room-view__typing${typingMembers.size > 0 ? ' room-view__typing--open' : ''}`}>
 | 
			
		||||
        <div className="bouncing-loader"><div /></div>
 | 
			
		||||
        <Text variant="b2">{getUsersActionJsx(roomId, [...typingMembers], 'typing...')}</Text>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className={`room-view__STB${isAtBottom ? '' : ' room-view__STB--open'}`}>
 | 
			
		||||
        <Button iconSrc={MessageIC} onClick={handleScrollToBottom}>
 | 
			
		||||
          <Text variant="b3" weight="medium">Jump to latest</Text>
 | 
			
		||||
        </Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
RoomViewFloating.propTypes = {
 | 
			
		||||
  roomId: PropTypes.string.isRequired,
 | 
			
		||||
  roomTimeline: PropTypes.shape({}).isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default RoomViewFloating;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,125 +0,0 @@
 | 
			
		|||
 @use '../../partials/flex';
 | 
			
		||||
@use '../../partials/text';
 | 
			
		||||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.room-view {
 | 
			
		||||
  &__typing {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    padding: var(--sp-ultra-tight) var(--sp-normal);
 | 
			
		||||
    background: var(--bg-surface);
 | 
			
		||||
    transition: transform 200ms ease-in-out;
 | 
			
		||||
 | 
			
		||||
    & b {
 | 
			
		||||
      color: var(--tc-surface-high);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & .text {
 | 
			
		||||
      @extend .cp-txt__ellipsis;
 | 
			
		||||
      @extend .cp-fx__item-one;
 | 
			
		||||
 | 
			
		||||
      margin: 0 var(--sp-tight);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--open {
 | 
			
		||||
      transform: translateY(-99%);
 | 
			
		||||
      box-shadow: 0 4px 0 0 var(--bg-surface);
 | 
			
		||||
      & .bouncing-loader {
 | 
			
		||||
        & > *,
 | 
			
		||||
        &::after,
 | 
			
		||||
        &::before {
 | 
			
		||||
          animation: bouncing-loader 0.6s infinite alternate;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .bouncing-loader {
 | 
			
		||||
    transform: translateY(2px);
 | 
			
		||||
    margin: 0 calc(var(--sp-ultra-tight) / 2);
 | 
			
		||||
  }
 | 
			
		||||
  .bouncing-loader > div,
 | 
			
		||||
  .bouncing-loader::before,
 | 
			
		||||
  .bouncing-loader::after {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    width: 8px;
 | 
			
		||||
    height: 8px;
 | 
			
		||||
    background: var(--tc-surface-high);
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  
 | 
			
		||||
  .bouncing-loader::before,
 | 
			
		||||
  .bouncing-loader::after {
 | 
			
		||||
    content: "";
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .bouncing-loader > div {
 | 
			
		||||
    margin: 0 4px;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .bouncing-loader > div {
 | 
			
		||||
    animation-delay: 0.2s;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .bouncing-loader::after {
 | 
			
		||||
    animation-delay: 0.4s;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  @keyframes bouncing-loader {
 | 
			
		||||
    to {
 | 
			
		||||
      opacity: 0.1;
 | 
			
		||||
      transform: translate3d(0, -4px, 0);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__STB,
 | 
			
		||||
  &__unread {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    background-color: var(--bg-surface-low);
 | 
			
		||||
    border-radius: var(--bo-radius);
 | 
			
		||||
 | 
			
		||||
    & button {
 | 
			
		||||
      justify-content: flex-start;
 | 
			
		||||
      border-radius: 0;
 | 
			
		||||
      box-shadow: none;
 | 
			
		||||
      padding: 6px var(--sp-tight);
 | 
			
		||||
      & .ic-raw {
 | 
			
		||||
        width: 16px;
 | 
			
		||||
        height: 16px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__STB {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    @include dir.prop(left, 50%, unset);
 | 
			
		||||
    @include dir.prop(right, unset, 50%);
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    box-shadow: var(--bs-surface-border);
 | 
			
		||||
    transition: transform 200ms ease-in-out;
 | 
			
		||||
    transform: translate(-50%, 100%);
 | 
			
		||||
 | 
			
		||||
    &--open {
 | 
			
		||||
      transform: translate(-50%, -28px);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__unread {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: var(--sp-extra-tight);
 | 
			
		||||
    @include dir.prop(left, var(--sp-normal), unset);
 | 
			
		||||
    @include dir.prop(right, unset, var(--sp-normal));
 | 
			
		||||
    z-index: 999;
 | 
			
		||||
 | 
			
		||||
    display: none;
 | 
			
		||||
    width: calc(100% - var(--sp-extra-loose));
 | 
			
		||||
    box-shadow: 0 0 2px 0 rgba(0, 0, 0, 20%);
 | 
			
		||||
 | 
			
		||||
    &--open {
 | 
			
		||||
      display: flex;
 | 
			
		||||
    }
 | 
			
		||||
    & button:first-child {
 | 
			
		||||
      @extend .cp-fx__item-one;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,132 +0,0 @@
 | 
			
		|||
import React, { useEffect, useRef } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './RoomViewHeader.scss';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
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 colorMXID from '../../../util/colorMXID';
 | 
			
		||||
import { getEventCords } from '../../../util/common';
 | 
			
		||||
 | 
			
		||||
import { tabText } from './RoomSettings';
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import RawIcon from '../../atoms/system-icons/RawIcon';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
 | 
			
		||||
import Avatar from '../../atoms/avatar/Avatar';
 | 
			
		||||
import RoomOptions from '../../molecules/room-options/RoomOptions';
 | 
			
		||||
 | 
			
		||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
 | 
			
		||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
 | 
			
		||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
 | 
			
		||||
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;
 | 
			
		||||
  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)';
 | 
			
		||||
    };
 | 
			
		||||
    navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle);
 | 
			
		||||
    return () => {
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const { roomList } = initMatrix;
 | 
			
		||||
    const handleProfileUpdate = (rId) => {
 | 
			
		||||
      if (roomId !== rId) return;
 | 
			
		||||
      forceUpdate();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    roomList.on(cons.events.roomList.ROOM_PROFILE_UPDATED, handleProfileUpdate);
 | 
			
		||||
    return () => {
 | 
			
		||||
      roomList.removeListener(cons.events.roomList.ROOM_PROFILE_UPDATED, handleProfileUpdate);
 | 
			
		||||
    };
 | 
			
		||||
  }, [roomId]);
 | 
			
		||||
 | 
			
		||||
  const openRoomOptions = (e) => {
 | 
			
		||||
    openReusableContextMenu('bottom', getEventCords(e, '.ic-btn'), (closeMenu) => (
 | 
			
		||||
      <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />
 | 
			
		||||
    ));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Header>
 | 
			
		||||
      <IconButton
 | 
			
		||||
        src={BackArrowIC}
 | 
			
		||||
        className="room-header__back-btn"
 | 
			
		||||
        tooltip="Return to navigation"
 | 
			
		||||
        onClick={() => openNavigation()}
 | 
			
		||||
      />
 | 
			
		||||
      <button
 | 
			
		||||
        ref={roomHeaderBtnRef}
 | 
			
		||||
        className="room-header__btn"
 | 
			
		||||
        onClick={() => toggleRoomSettings()}
 | 
			
		||||
        type="button"
 | 
			
		||||
        onMouseUp={(e) => blurOnBubbling(e, '.room-header__btn')}
 | 
			
		||||
      >
 | 
			
		||||
        <Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
 | 
			
		||||
        <TitleWrapper>
 | 
			
		||||
          <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={() => {
 | 
			
		||||
          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>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
RoomViewHeader.propTypes = {
 | 
			
		||||
  roomId: PropTypes.string.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default RoomViewHeader;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,47 +0,0 @@
 | 
			
		|||
@use '../../partials/flex';
 | 
			
		||||
@use '../../partials/dir';
 | 
			
		||||
@use '../../partials/screen';
 | 
			
		||||
 | 
			
		||||
.room-header__btn {
 | 
			
		||||
  min-width: 0;
 | 
			
		||||
  @extend .cp-fx__row--s-c;
 | 
			
		||||
  @include dir.side(margin, 0, auto);
 | 
			
		||||
  border-radius: var(--bo-radius);
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  
 | 
			
		||||
  & .ic-raw {
 | 
			
		||||
    @include dir.side(margin, 0, var(--sp-extra-tight));
 | 
			
		||||
    transition: transform 200ms ease-in-out;
 | 
			
		||||
  }
 | 
			
		||||
  @media (hover:hover) {
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: var(--bg-surface-hover);
 | 
			
		||||
      box-shadow: var(--bs-surface-outline);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &:focus,
 | 
			
		||||
  &:active {
 | 
			
		||||
    background-color: var(--bg-surface-active);
 | 
			
		||||
    box-shadow: var(--bs-surface-outline);
 | 
			
		||||
    outline: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.room-header__drawer-btn {
 | 
			
		||||
  @include screen.smallerThan(tabletBreakpoint) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.room-header__members-btn {
 | 
			
		||||
  @include screen.biggerThan(tabletBreakpoint) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.room-header__back-btn {
 | 
			
		||||
  @include dir.side(margin, 0, var(--sp-tight));
 | 
			
		||||
 | 
			
		||||
  @include screen.biggerThan(mobileBreakpoint) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,491 +0,0 @@
 | 
			
		|||
/* eslint-disable react/prop-types */
 | 
			
		||||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './RoomViewInput.scss';
 | 
			
		||||
 | 
			
		||||
import TextareaAutosize from 'react-autosize-textarea';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import settings from '../../../client/state/settings';
 | 
			
		||||
import { openEmojiBoard, openReusableContextMenu } from '../../../client/action/navigation';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import { bytesToSize, getEventCords } from '../../../util/common';
 | 
			
		||||
import { getUsername } from '../../../util/matrixUtil';
 | 
			
		||||
import colorMXID from '../../../util/colorMXID';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import RawIcon from '../../atoms/system-icons/RawIcon';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import ScrollView from '../../atoms/scroll/ScrollView';
 | 
			
		||||
import { MessageReply } from '../../molecules/message/Message';
 | 
			
		||||
 | 
			
		||||
import StickerBoard from '../sticker-board/StickerBoard';
 | 
			
		||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
 | 
			
		||||
 | 
			
		||||
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
 | 
			
		||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
 | 
			
		||||
import SendIC from '../../../../public/res/ic/outlined/send.svg';
 | 
			
		||||
import StickerIC from '../../../../public/res/ic/outlined/sticker.svg';
 | 
			
		||||
import ShieldIC from '../../../../public/res/ic/outlined/shield.svg';
 | 
			
		||||
import VLCIC from '../../../../public/res/ic/outlined/vlc.svg';
 | 
			
		||||
import VolumeFullIC from '../../../../public/res/ic/outlined/volume-full.svg';
 | 
			
		||||
import FileIC from '../../../../public/res/ic/outlined/file.svg';
 | 
			
		||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 | 
			
		||||
 | 
			
		||||
import commands from './commands';
 | 
			
		||||
 | 
			
		||||
const CMD_REGEX = /(^\/|:|@)(\S*)$/;
 | 
			
		||||
let isTyping = false;
 | 
			
		||||
let isCmdActivated = false;
 | 
			
		||||
let cmdCursorPos = null;
 | 
			
		||||
function RoomViewInput({
 | 
			
		||||
  roomId, roomTimeline, viewEvent,
 | 
			
		||||
}) {
 | 
			
		||||
  const [attachment, setAttachment] = useState(null);
 | 
			
		||||
  const [replyTo, setReplyTo] = useState(null);
 | 
			
		||||
 | 
			
		||||
  const textAreaRef = useRef(null);
 | 
			
		||||
  const inputBaseRef = useRef(null);
 | 
			
		||||
  const uploadInputRef = useRef(null);
 | 
			
		||||
  const uploadProgressRef = useRef(null);
 | 
			
		||||
  const rightOptionsRef = useRef(null);
 | 
			
		||||
 | 
			
		||||
  const TYPING_TIMEOUT = 5000;
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const { roomsInput } = initMatrix;
 | 
			
		||||
 | 
			
		||||
  function requestFocusInput() {
 | 
			
		||||
    if (textAreaRef === null) return;
 | 
			
		||||
    textAreaRef.current.focus();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    roomsInput.on(cons.events.roomsInput.ATTACHMENT_SET, setAttachment);
 | 
			
		||||
    viewEvent.on('focus_msg_input', requestFocusInput);
 | 
			
		||||
    return () => {
 | 
			
		||||
      roomsInput.removeListener(cons.events.roomsInput.ATTACHMENT_SET, setAttachment);
 | 
			
		||||
      viewEvent.removeListener('focus_msg_input', requestFocusInput);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const sendIsTyping = (isT) => {
 | 
			
		||||
    mx.sendTyping(roomId, isT, isT ? TYPING_TIMEOUT : undefined);
 | 
			
		||||
    isTyping = isT;
 | 
			
		||||
 | 
			
		||||
    if (isT === true) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        if (isTyping) sendIsTyping(false);
 | 
			
		||||
      }, TYPING_TIMEOUT);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function uploadingProgress(myRoomId, { loaded, total }) {
 | 
			
		||||
    if (myRoomId !== roomId) return;
 | 
			
		||||
    const progressPer = Math.round((loaded * 100) / total);
 | 
			
		||||
    uploadProgressRef.current.textContent = `Uploading: ${bytesToSize(loaded)}/${bytesToSize(total)} (${progressPer}%)`;
 | 
			
		||||
    inputBaseRef.current.style.backgroundImage = `linear-gradient(90deg, var(--bg-surface-hover) ${progressPer}%, var(--bg-surface-low) ${progressPer}%)`;
 | 
			
		||||
  }
 | 
			
		||||
  function clearAttachment(myRoomId) {
 | 
			
		||||
    if (roomId !== myRoomId) return;
 | 
			
		||||
    setAttachment(null);
 | 
			
		||||
    inputBaseRef.current.style.backgroundImage = 'unset';
 | 
			
		||||
    uploadInputRef.current.value = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function rightOptionsA11Y(A11Y) {
 | 
			
		||||
    const rightOptions = rightOptionsRef.current.children;
 | 
			
		||||
    for (let index = 0; index < rightOptions.length; index += 1) {
 | 
			
		||||
      rightOptions[index].tabIndex = A11Y ? 0 : -1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function activateCmd(prefix) {
 | 
			
		||||
    isCmdActivated = true;
 | 
			
		||||
    rightOptionsA11Y(false);
 | 
			
		||||
    viewEvent.emit('cmd_activate', prefix);
 | 
			
		||||
  }
 | 
			
		||||
  function deactivateCmd() {
 | 
			
		||||
    isCmdActivated = false;
 | 
			
		||||
    cmdCursorPos = null;
 | 
			
		||||
    rightOptionsA11Y(true);
 | 
			
		||||
  }
 | 
			
		||||
  function deactivateCmdAndEmit() {
 | 
			
		||||
    deactivateCmd();
 | 
			
		||||
    viewEvent.emit('cmd_deactivate');
 | 
			
		||||
  }
 | 
			
		||||
  function setCursorPosition(pos) {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      textAreaRef.current.focus();
 | 
			
		||||
      textAreaRef.current.setSelectionRange(pos, pos);
 | 
			
		||||
    }, 0);
 | 
			
		||||
  }
 | 
			
		||||
  function replaceCmdWith(msg, cursor, replacement) {
 | 
			
		||||
    if (msg === null) return null;
 | 
			
		||||
    const targetInput = msg.slice(0, cursor);
 | 
			
		||||
    const cmdParts = targetInput.match(CMD_REGEX);
 | 
			
		||||
    const leadingInput = msg.slice(0, cmdParts.index);
 | 
			
		||||
    if (replacement.length > 0) setCursorPosition(leadingInput.length + replacement.length);
 | 
			
		||||
    return leadingInput + replacement + msg.slice(cursor);
 | 
			
		||||
  }
 | 
			
		||||
  function firedCmd(cmdData) {
 | 
			
		||||
    const msg = textAreaRef.current.value;
 | 
			
		||||
    textAreaRef.current.value = replaceCmdWith(
 | 
			
		||||
      msg,
 | 
			
		||||
      cmdCursorPos,
 | 
			
		||||
      typeof cmdData?.replace !== 'undefined' ? cmdData.replace : '',
 | 
			
		||||
    );
 | 
			
		||||
    deactivateCmd();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function focusInput() {
 | 
			
		||||
    if (settings.isTouchScreenDevice) return;
 | 
			
		||||
    textAreaRef.current.focus();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function setUpReply(userId, eventId, body, formattedBody) {
 | 
			
		||||
    setReplyTo({ userId, eventId, body });
 | 
			
		||||
    roomsInput.setReplyTo(roomId, {
 | 
			
		||||
      userId, eventId, body, formattedBody,
 | 
			
		||||
    });
 | 
			
		||||
    focusInput();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    roomsInput.on(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress);
 | 
			
		||||
    roomsInput.on(cons.events.roomsInput.ATTACHMENT_CANCELED, clearAttachment);
 | 
			
		||||
    roomsInput.on(cons.events.roomsInput.FILE_UPLOADED, clearAttachment);
 | 
			
		||||
    viewEvent.on('cmd_fired', firedCmd);
 | 
			
		||||
    navigation.on(cons.events.navigation.REPLY_TO_CLICKED, setUpReply);
 | 
			
		||||
    if (textAreaRef?.current !== null) {
 | 
			
		||||
      isTyping = false;
 | 
			
		||||
      textAreaRef.current.value = roomsInput.getMessage(roomId);
 | 
			
		||||
      setAttachment(roomsInput.getAttachment(roomId));
 | 
			
		||||
      setReplyTo(roomsInput.getReplyTo(roomId));
 | 
			
		||||
    }
 | 
			
		||||
    return () => {
 | 
			
		||||
      roomsInput.removeListener(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress);
 | 
			
		||||
      roomsInput.removeListener(cons.events.roomsInput.ATTACHMENT_CANCELED, clearAttachment);
 | 
			
		||||
      roomsInput.removeListener(cons.events.roomsInput.FILE_UPLOADED, clearAttachment);
 | 
			
		||||
      viewEvent.removeListener('cmd_fired', firedCmd);
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.REPLY_TO_CLICKED, setUpReply);
 | 
			
		||||
      if (isCmdActivated) deactivateCmd();
 | 
			
		||||
      if (textAreaRef?.current === null) return;
 | 
			
		||||
 | 
			
		||||
      const msg = textAreaRef.current.value;
 | 
			
		||||
      textAreaRef.current.style.height = 'unset';
 | 
			
		||||
      inputBaseRef.current.style.backgroundImage = 'unset';
 | 
			
		||||
      if (msg.trim() === '') {
 | 
			
		||||
        roomsInput.setMessage(roomId, '');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      roomsInput.setMessage(roomId, msg);
 | 
			
		||||
    };
 | 
			
		||||
  }, [roomId]);
 | 
			
		||||
 | 
			
		||||
  const sendBody = async (body, options) => {
 | 
			
		||||
    const opt = options ?? {};
 | 
			
		||||
    if (!opt.msgType) opt.msgType = 'm.text';
 | 
			
		||||
    if (typeof opt.autoMarkdown !== 'boolean') opt.autoMarkdown = true;
 | 
			
		||||
    if (roomsInput.isSending(roomId)) return;
 | 
			
		||||
    sendIsTyping(false);
 | 
			
		||||
 | 
			
		||||
    roomsInput.setMessage(roomId, body);
 | 
			
		||||
    if (attachment !== null) {
 | 
			
		||||
      roomsInput.setAttachment(roomId, attachment);
 | 
			
		||||
    }
 | 
			
		||||
    textAreaRef.current.disabled = true;
 | 
			
		||||
    textAreaRef.current.style.cursor = 'not-allowed';
 | 
			
		||||
    await roomsInput.sendInput(roomId, opt);
 | 
			
		||||
    textAreaRef.current.disabled = false;
 | 
			
		||||
    textAreaRef.current.style.cursor = 'unset';
 | 
			
		||||
    focusInput();
 | 
			
		||||
 | 
			
		||||
    textAreaRef.current.value = roomsInput.getMessage(roomId);
 | 
			
		||||
    textAreaRef.current.style.height = 'unset';
 | 
			
		||||
    if (replyTo !== null) setReplyTo(null);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /** Return true if a command was executed. */
 | 
			
		||||
  const processCommand = async (cmdBody) => {
 | 
			
		||||
    const spaceIndex = cmdBody.indexOf(' ');
 | 
			
		||||
    const cmdName = cmdBody.slice(1, spaceIndex > -1 ? spaceIndex : undefined);
 | 
			
		||||
    const cmdData = spaceIndex > -1 ? cmdBody.slice(spaceIndex + 1) : '';
 | 
			
		||||
    if (!commands[cmdName]) {
 | 
			
		||||
      const sendAsMessage = await confirmDialog('Invalid Command', `"${cmdName}" is not a valid command. Did you mean to send this as a message?`, 'Send as message');
 | 
			
		||||
      if (sendAsMessage) {
 | 
			
		||||
        sendBody(cmdBody);
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    if (['me', 'shrug', 'plain'].includes(cmdName)) {
 | 
			
		||||
      commands[cmdName].exe(roomId, cmdData, sendBody);
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    commands[cmdName].exe(roomId, cmdData);
 | 
			
		||||
    return true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const sendMessage = async () => {
 | 
			
		||||
    requestAnimationFrame(() => deactivateCmdAndEmit());
 | 
			
		||||
    const msgBody = textAreaRef.current.value.trim();
 | 
			
		||||
    if (msgBody.startsWith('/')) {
 | 
			
		||||
      const executed = await processCommand(msgBody.trim());
 | 
			
		||||
      if (executed) {
 | 
			
		||||
        textAreaRef.current.value = '';
 | 
			
		||||
        textAreaRef.current.style.height = 'unset';
 | 
			
		||||
      }
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (msgBody === '' && attachment === null) return;
 | 
			
		||||
    sendBody(msgBody);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleSendSticker = async (data) => {
 | 
			
		||||
    roomsInput.sendSticker(roomId, data);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function processTyping(msg) {
 | 
			
		||||
    const isEmptyMsg = msg === '';
 | 
			
		||||
 | 
			
		||||
    if (isEmptyMsg && isTyping) {
 | 
			
		||||
      sendIsTyping(false);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!isEmptyMsg && !isTyping) {
 | 
			
		||||
      sendIsTyping(true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getCursorPosition() {
 | 
			
		||||
    return textAreaRef.current.selectionStart;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function recognizeCmd(rawInput) {
 | 
			
		||||
    const cursor = getCursorPosition();
 | 
			
		||||
    const targetInput = rawInput.slice(0, cursor);
 | 
			
		||||
 | 
			
		||||
    const cmdParts = targetInput.match(CMD_REGEX);
 | 
			
		||||
    if (cmdParts === null) {
 | 
			
		||||
      if (isCmdActivated) deactivateCmdAndEmit();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const cmdPrefix = cmdParts[1];
 | 
			
		||||
    const cmdSlug = cmdParts[2];
 | 
			
		||||
 | 
			
		||||
    if (cmdPrefix === ':') {
 | 
			
		||||
      // skip emoji autofill command if link is suspected.
 | 
			
		||||
      const checkForLink = targetInput.slice(0, cmdParts.index);
 | 
			
		||||
      if (checkForLink.match(/(http|https|mailto|matrix|ircs|irc)$/)) {
 | 
			
		||||
        deactivateCmdAndEmit();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    cmdCursorPos = cursor;
 | 
			
		||||
    if (cmdSlug === '') {
 | 
			
		||||
      activateCmd(cmdPrefix);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!isCmdActivated) activateCmd(cmdPrefix);
 | 
			
		||||
    viewEvent.emit('cmd_process', cmdPrefix, cmdSlug);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleMsgTyping = (e) => {
 | 
			
		||||
    const msg = e.target.value;
 | 
			
		||||
    recognizeCmd(e.target.value);
 | 
			
		||||
    if (!isCmdActivated) processTyping(msg);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleKeyDown = (e) => {
 | 
			
		||||
    if (e.key === 'Escape') {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      roomsInput.cancelReplyTo(roomId);
 | 
			
		||||
      setReplyTo(null);
 | 
			
		||||
    }
 | 
			
		||||
    if (e.key === 'Enter' && e.shiftKey === false) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      sendMessage();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handlePaste = (e) => {
 | 
			
		||||
    if (e.clipboardData === false) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (e.clipboardData.items === undefined) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < e.clipboardData.items.length; i += 1) {
 | 
			
		||||
      const item = e.clipboardData.items[i];
 | 
			
		||||
      if (item.type.indexOf('image') !== -1) {
 | 
			
		||||
        const image = item.getAsFile();
 | 
			
		||||
        if (attachment === null) {
 | 
			
		||||
          setAttachment(image);
 | 
			
		||||
          if (image !== null) {
 | 
			
		||||
            roomsInput.setAttachment(roomId, image);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function addEmoji(emoji) {
 | 
			
		||||
    textAreaRef.current.value += emoji.unicode;
 | 
			
		||||
    textAreaRef.current.focus();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleUploadClick = () => {
 | 
			
		||||
    if (attachment === null) uploadInputRef.current.click();
 | 
			
		||||
    else {
 | 
			
		||||
      roomsInput.cancelAttachment(roomId);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  function uploadFileChange(e) {
 | 
			
		||||
    const file = e.target.files.item(0);
 | 
			
		||||
    setAttachment(file);
 | 
			
		||||
    if (file !== null) roomsInput.setAttachment(roomId, file);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function renderInputs() {
 | 
			
		||||
    const canISend = roomTimeline.room.currentState.maySendMessage(mx.getUserId());
 | 
			
		||||
    const tombstoneEvent = roomTimeline.room.currentState.getStateEvents('m.room.tombstone')[0];
 | 
			
		||||
    if (!canISend || tombstoneEvent) {
 | 
			
		||||
      return (
 | 
			
		||||
        <Text className="room-input__alert">
 | 
			
		||||
          {
 | 
			
		||||
            tombstoneEvent
 | 
			
		||||
              ? tombstoneEvent.getContent()?.body ?? 'This room has been replaced and is no longer active.'
 | 
			
		||||
              : 'You do not have permission to post to this room'
 | 
			
		||||
          }
 | 
			
		||||
        </Text>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <div className={`room-input__option-container${attachment === null ? '' : ' room-attachment__option'}`}>
 | 
			
		||||
          <input onChange={uploadFileChange} style={{ display: 'none' }} ref={uploadInputRef} type="file" />
 | 
			
		||||
          <IconButton onClick={handleUploadClick} tooltip={attachment === null ? 'Upload' : 'Cancel'} src={CirclePlusIC} />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div ref={inputBaseRef} className="room-input__input-container">
 | 
			
		||||
          {roomTimeline.isEncrypted() && <RawIcon size="extra-small" src={ShieldIC} />}
 | 
			
		||||
          <ScrollView autoHide>
 | 
			
		||||
            <Text className="room-input__textarea-wrapper">
 | 
			
		||||
              <TextareaAutosize
 | 
			
		||||
                dir="auto"
 | 
			
		||||
                id="message-textarea"
 | 
			
		||||
                ref={textAreaRef}
 | 
			
		||||
                onChange={handleMsgTyping}
 | 
			
		||||
                onPaste={handlePaste}
 | 
			
		||||
                onKeyDown={handleKeyDown}
 | 
			
		||||
                placeholder="Send a message..."
 | 
			
		||||
              />
 | 
			
		||||
            </Text>
 | 
			
		||||
          </ScrollView>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div ref={rightOptionsRef} className="room-input__option-container">
 | 
			
		||||
          <IconButton
 | 
			
		||||
            onClick={(e) => {
 | 
			
		||||
              openReusableContextMenu(
 | 
			
		||||
                'top',
 | 
			
		||||
                (() => {
 | 
			
		||||
                  const cords = getEventCords(e);
 | 
			
		||||
                  cords.y -= 20;
 | 
			
		||||
                  return cords;
 | 
			
		||||
                })(),
 | 
			
		||||
                (closeMenu) => (
 | 
			
		||||
                  <StickerBoard
 | 
			
		||||
                    roomId={roomId}
 | 
			
		||||
                    onSelect={(data) => {
 | 
			
		||||
                      handleSendSticker(data);
 | 
			
		||||
                      closeMenu();
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                ),
 | 
			
		||||
              );
 | 
			
		||||
            }}
 | 
			
		||||
            tooltip="Sticker"
 | 
			
		||||
            src={StickerIC}
 | 
			
		||||
          />
 | 
			
		||||
          <IconButton
 | 
			
		||||
            onClick={(e) => {
 | 
			
		||||
              const cords = getEventCords(e);
 | 
			
		||||
              cords.x += (document.dir === 'rtl' ? -80 : 80);
 | 
			
		||||
              cords.y -= 250;
 | 
			
		||||
              openEmojiBoard(cords, addEmoji);
 | 
			
		||||
            }}
 | 
			
		||||
            tooltip="Emoji"
 | 
			
		||||
            src={EmojiIC}
 | 
			
		||||
          />
 | 
			
		||||
          <IconButton onClick={sendMessage} tooltip="Send" src={SendIC} />
 | 
			
		||||
        </div>
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function attachFile() {
 | 
			
		||||
    const fileType = attachment.type.slice(0, attachment.type.indexOf('/'));
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="room-attachment">
 | 
			
		||||
        <div className={`room-attachment__preview${fileType !== 'image' ? ' room-attachment__icon' : ''}`}>
 | 
			
		||||
          {fileType === 'image' && <img alt={attachment.name} src={URL.createObjectURL(attachment)} />}
 | 
			
		||||
          {fileType === 'video' && <RawIcon src={VLCIC} />}
 | 
			
		||||
          {fileType === 'audio' && <RawIcon src={VolumeFullIC} />}
 | 
			
		||||
          {fileType !== 'image' && fileType !== 'video' && fileType !== 'audio' && <RawIcon src={FileIC} />}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="room-attachment__info">
 | 
			
		||||
          <Text variant="b1">{attachment.name}</Text>
 | 
			
		||||
          <Text variant="b3"><span ref={uploadProgressRef}>{`size: ${bytesToSize(attachment.size)}`}</span></Text>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function attachReply() {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="room-reply">
 | 
			
		||||
        <IconButton
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            roomsInput.cancelReplyTo(roomId);
 | 
			
		||||
            setReplyTo(null);
 | 
			
		||||
          }}
 | 
			
		||||
          src={CrossIC}
 | 
			
		||||
          tooltip="Cancel reply"
 | 
			
		||||
          size="extra-small"
 | 
			
		||||
        />
 | 
			
		||||
        <MessageReply
 | 
			
		||||
          userId={replyTo.userId}
 | 
			
		||||
          onKeyDown={handleKeyDown}
 | 
			
		||||
          name={getUsername(replyTo.userId)}
 | 
			
		||||
          color={colorMXID(replyTo.userId)}
 | 
			
		||||
          body={replyTo.body}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      { replyTo !== null && attachReply()}
 | 
			
		||||
      { attachment !== null && attachFile() }
 | 
			
		||||
      <form className="room-input" onSubmit={(e) => { e.preventDefault(); }}>
 | 
			
		||||
        {
 | 
			
		||||
          renderInputs()
 | 
			
		||||
        }
 | 
			
		||||
      </form>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
RoomViewInput.propTypes = {
 | 
			
		||||
  roomId: PropTypes.string.isRequired,
 | 
			
		||||
  roomTimeline: PropTypes.shape({}).isRequired,
 | 
			
		||||
  viewEvent: PropTypes.shape({}).isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default RoomViewInput;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,108 +0,0 @@
 | 
			
		|||
@use '../../partials/dir';
 | 
			
		||||
 | 
			
		||||
.room-input {
 | 
			
		||||
  padding: var(--sp-extra-tight) calc(var(--sp-normal) - 2px);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  min-height: 56px;
 | 
			
		||||
 | 
			
		||||
  &__alert {
 | 
			
		||||
    margin: auto;
 | 
			
		||||
    padding: 0 var(--sp-tight);
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__input-container {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
 | 
			
		||||
    margin: 0 calc(var(--sp-tight)  - 2px);
 | 
			
		||||
    background-color: var(--bg-surface-low);
 | 
			
		||||
    box-shadow: var(--bs-surface-border);
 | 
			
		||||
    border-radius: var(--bo-radius);
 | 
			
		||||
 | 
			
		||||
    & > .ic-raw {
 | 
			
		||||
      transform: scale(0.8);
 | 
			
		||||
      margin: 0 var(--sp-extra-tight);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
    & .scrollbar {
 | 
			
		||||
      max-height: 50vh;
 | 
			
		||||
      flex: 1;
 | 
			
		||||
 | 
			
		||||
      &:first-child {
 | 
			
		||||
        @include dir.side(margin, var(--sp-tight), 0);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__textarea-wrapper {
 | 
			
		||||
    min-height: 40px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
 | 
			
		||||
    & textarea {
 | 
			
		||||
      resize: none;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      min-width: 0;
 | 
			
		||||
      min-height: 100%;
 | 
			
		||||
      padding: var(--sp-ultra-tight) 0;
 | 
			
		||||
 | 
			
		||||
      &::placeholder {
 | 
			
		||||
        color: var(--tc-surface-low);
 | 
			
		||||
      }
 | 
			
		||||
      &:focus {
 | 
			
		||||
        outline: none;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.room-attachment {
 | 
			
		||||
  --side-spacing: calc(var(--sp-normal) + var(--av-small) + var(--sp-tight));
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  @include dir.side(margin, var(--side-spacing), 0);
 | 
			
		||||
  margin-top: var(--sp-extra-tight);
 | 
			
		||||
  line-height: 0;
 | 
			
		||||
 | 
			
		||||
  &__preview > img {
 | 
			
		||||
    max-height: 40px;
 | 
			
		||||
    border-radius: var(--bo-radius);
 | 
			
		||||
    max-width: 150px;
 | 
			
		||||
  }
 | 
			
		||||
  &__icon {
 | 
			
		||||
    padding: var(--sp-extra-tight);
 | 
			
		||||
    background-color: var(--bg-surface-low);
 | 
			
		||||
    box-shadow: var(--bs-surface-border);
 | 
			
		||||
    border-radius: var(--bo-radius);
 | 
			
		||||
  }
 | 
			
		||||
  &__info {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
    margin: 0 var(--sp-tight);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__option button {
 | 
			
		||||
    transition: transform 200ms ease-in-out;
 | 
			
		||||
    transform: translateY(-48px);
 | 
			
		||||
    & .ic-raw {
 | 
			
		||||
      transition: transform 200ms ease-in-out;
 | 
			
		||||
      transform: rotate(45deg);
 | 
			
		||||
      background-color: var(--bg-caution);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.room-reply {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  background-color: var(--bg-surface-low);
 | 
			
		||||
  border-bottom: 1px solid var(--bg-surface-border);
 | 
			
		||||
 | 
			
		||||
  & .ic-btn-surface {
 | 
			
		||||
    @include dir.side(margin, 17px, 13px);
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,136 +0,0 @@
 | 
			
		|||
import { getScrollInfo } from '../../../util/common';
 | 
			
		||||
 | 
			
		||||
class TimelineScroll {
 | 
			
		||||
  constructor(target) {
 | 
			
		||||
    if (target === null) {
 | 
			
		||||
      throw new Error('Can not initialize TimelineScroll, target HTMLElement in null');
 | 
			
		||||
    }
 | 
			
		||||
    this.scroll = target;
 | 
			
		||||
 | 
			
		||||
    this.backwards = false;
 | 
			
		||||
    this.inTopHalf = false;
 | 
			
		||||
 | 
			
		||||
    this.isScrollable = false;
 | 
			
		||||
    this.top = 0;
 | 
			
		||||
    this.bottom = 0;
 | 
			
		||||
    this.height = 0;
 | 
			
		||||
    this.viewHeight = 0;
 | 
			
		||||
 | 
			
		||||
    this.topMsg = null;
 | 
			
		||||
    this.bottomMsg = null;
 | 
			
		||||
    this.diff = 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  scrollToBottom() {
 | 
			
		||||
    const scrollInfo = getScrollInfo(this.scroll);
 | 
			
		||||
    const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
 | 
			
		||||
 | 
			
		||||
    this._scrollTo(scrollInfo, maxScrollTop);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // use previous calc by this._updateTopBottomMsg() & this._calcDiff.
 | 
			
		||||
  tryRestoringScroll() {
 | 
			
		||||
    const scrollInfo = getScrollInfo(this.scroll);
 | 
			
		||||
 | 
			
		||||
    let scrollTop = 0;
 | 
			
		||||
    const ot = this.inTopHalf ? this.topMsg?.offsetTop : this.bottomMsg?.offsetTop;
 | 
			
		||||
    if (!ot) scrollTop = Math.round(this.height - this.viewHeight);
 | 
			
		||||
    else scrollTop = ot - this.diff;
 | 
			
		||||
 | 
			
		||||
    this._scrollTo(scrollInfo, scrollTop);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  scrollToIndex(index, offset = 0) {
 | 
			
		||||
    const scrollInfo = getScrollInfo(this.scroll);
 | 
			
		||||
    const msgs = this.scroll.lastElementChild.lastElementChild.children;
 | 
			
		||||
    const offsetTop = msgs[index]?.offsetTop;
 | 
			
		||||
 | 
			
		||||
    if (offsetTop === undefined) return;
 | 
			
		||||
    // if msg is already in visible are we don't need to scroll to that
 | 
			
		||||
    if (offsetTop > scrollInfo.top && offsetTop < (scrollInfo.top + scrollInfo.viewHeight)) return;
 | 
			
		||||
    const to = offsetTop - offset;
 | 
			
		||||
 | 
			
		||||
    this._scrollTo(scrollInfo, to);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _scrollTo(scrollInfo, scrollTop) {
 | 
			
		||||
    this.scroll.scrollTop = scrollTop;
 | 
			
		||||
 | 
			
		||||
    // browser emit 'onscroll' event only if the 'element.scrollTop' value changes.
 | 
			
		||||
    // so here we flag that the upcoming 'onscroll' event is
 | 
			
		||||
    // emitted as side effect of assigning 'this.scroll.scrollTop' above
 | 
			
		||||
    // only if it's changes.
 | 
			
		||||
    // by doing so we prevent this._updateCalc() from calc again.
 | 
			
		||||
    if (scrollTop !== this.top) {
 | 
			
		||||
      this.scrolledByCode = true;
 | 
			
		||||
    }
 | 
			
		||||
    const sInfo = { ...scrollInfo };
 | 
			
		||||
 | 
			
		||||
    const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
 | 
			
		||||
 | 
			
		||||
    sInfo.top = (scrollTop > maxScrollTop) ? maxScrollTop : scrollTop;
 | 
			
		||||
    this._updateCalc(sInfo);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // we maintain reference of top and bottom messages
 | 
			
		||||
  // to restore the scroll position when
 | 
			
		||||
  // messages gets removed from either end and added to other.
 | 
			
		||||
  _updateTopBottomMsg() {
 | 
			
		||||
    const msgs = this.scroll.lastElementChild.lastElementChild.children;
 | 
			
		||||
    const lMsgIndex = msgs.length - 1;
 | 
			
		||||
 | 
			
		||||
    // TODO: classname 'ph-msg' prevent this class from being used
 | 
			
		||||
    const PLACEHOLDER_COUNT = 2;
 | 
			
		||||
    this.topMsg = msgs[0]?.className === 'ph-msg'
 | 
			
		||||
      ? msgs[PLACEHOLDER_COUNT]
 | 
			
		||||
      : msgs[0];
 | 
			
		||||
    this.bottomMsg = msgs[lMsgIndex]?.className === 'ph-msg'
 | 
			
		||||
      ? msgs[lMsgIndex - PLACEHOLDER_COUNT]
 | 
			
		||||
      : msgs[lMsgIndex];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // we calculate the difference between first/last message and current scrollTop.
 | 
			
		||||
  // if we are going above we calc diff between first and scrollTop
 | 
			
		||||
  // else otherwise.
 | 
			
		||||
  // NOTE: This will help to restore the scroll when msgs get's removed
 | 
			
		||||
  // from one end and added to other end
 | 
			
		||||
  _calcDiff(scrollInfo) {
 | 
			
		||||
    if (!this.topMsg || !this.bottomMsg) return 0;
 | 
			
		||||
    if (this.inTopHalf) {
 | 
			
		||||
      return this.topMsg.offsetTop - scrollInfo.top;
 | 
			
		||||
    }
 | 
			
		||||
    return this.bottomMsg.offsetTop - scrollInfo.top;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _updateCalc(scrollInfo) {
 | 
			
		||||
    const halfViewHeight = Math.round(scrollInfo.viewHeight / 2);
 | 
			
		||||
    const scrollMiddle = scrollInfo.top + halfViewHeight;
 | 
			
		||||
    const lastMiddle = this.top + halfViewHeight;
 | 
			
		||||
 | 
			
		||||
    this.backwards = scrollMiddle < lastMiddle;
 | 
			
		||||
    this.inTopHalf = scrollMiddle < scrollInfo.height / 2;
 | 
			
		||||
 | 
			
		||||
    this.isScrollable = scrollInfo.isScrollable;
 | 
			
		||||
    this.top = scrollInfo.top;
 | 
			
		||||
    this.bottom = scrollInfo.height - (scrollInfo.top + scrollInfo.viewHeight);
 | 
			
		||||
    this.height = scrollInfo.height;
 | 
			
		||||
    this.viewHeight = scrollInfo.viewHeight;
 | 
			
		||||
 | 
			
		||||
    this._updateTopBottomMsg();
 | 
			
		||||
    this.diff = this._calcDiff(scrollInfo);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  calcScroll() {
 | 
			
		||||
    if (this.scrolledByCode) {
 | 
			
		||||
      this.scrolledByCode = false;
 | 
			
		||||
      return undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const scrollInfo = getScrollInfo(this.scroll);
 | 
			
		||||
    this._updateCalc(scrollInfo);
 | 
			
		||||
 | 
			
		||||
    return this.backwards;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default TimelineScroll;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,220 +0,0 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import './commands.scss';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import * as roomActions from '../../../client/action/room';
 | 
			
		||||
import { hasDMWith, hasDevices } from '../../../util/matrixUtil';
 | 
			
		||||
import { selectRoom, openReusableDialog } from '../../../client/action/navigation';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import SettingTile from '../../molecules/setting-tile/SettingTile';
 | 
			
		||||
 | 
			
		||||
const MXID_REG = /^@\S+:\S+$/;
 | 
			
		||||
const ROOM_ID_ALIAS_REG = /^(#|!)\S+:\S+$/;
 | 
			
		||||
const ROOM_ID_REG = /^!\S+:\S+$/;
 | 
			
		||||
const MXC_REG = /^mxc:\/\/\S+$/;
 | 
			
		||||
 | 
			
		||||
export function processMxidAndReason(data) {
 | 
			
		||||
  let reason;
 | 
			
		||||
  let idData = data;
 | 
			
		||||
  const reasonMatch = data.match(/\s-r\s/);
 | 
			
		||||
  if (reasonMatch) {
 | 
			
		||||
    idData = data.slice(0, reasonMatch.index);
 | 
			
		||||
    reason = data.slice(reasonMatch.index + reasonMatch[0].length);
 | 
			
		||||
    if (reason.trim() === '') reason = undefined;
 | 
			
		||||
  }
 | 
			
		||||
  const rawIds = idData.split(' ');
 | 
			
		||||
  const userIds = rawIds.filter((id) => id.match(MXID_REG));
 | 
			
		||||
  return {
 | 
			
		||||
    userIds,
 | 
			
		||||
    reason,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const commands = {
 | 
			
		||||
  me: {
 | 
			
		||||
    name: 'me',
 | 
			
		||||
    description: 'Display action',
 | 
			
		||||
    exe: (roomId, data, onSuccess) => {
 | 
			
		||||
      const body = data.trim();
 | 
			
		||||
      if (body === '') return;
 | 
			
		||||
      onSuccess(body, { msgType: 'm.emote' });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  shrug: {
 | 
			
		||||
    name: 'shrug',
 | 
			
		||||
    description: 'Send ¯\\_(ツ)_/¯ as message',
 | 
			
		||||
    exe: (roomId, data, onSuccess) => onSuccess(
 | 
			
		||||
      `¯\\_(ツ)_/¯${data.trim() !== '' ? ` ${data}` : ''}`,
 | 
			
		||||
      { msgType: 'm.text' },
 | 
			
		||||
    ),
 | 
			
		||||
  },
 | 
			
		||||
  plain: {
 | 
			
		||||
    name: 'plain',
 | 
			
		||||
    description: 'Send plain text message',
 | 
			
		||||
    exe: (roomId, data, onSuccess) => {
 | 
			
		||||
      const body = data.trim();
 | 
			
		||||
      if (body === '') return;
 | 
			
		||||
      onSuccess(body, { msgType: 'm.text', autoMarkdown: false });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  help: {
 | 
			
		||||
    name: 'help',
 | 
			
		||||
    description: 'View all commands',
 | 
			
		||||
    // eslint-disable-next-line no-use-before-define
 | 
			
		||||
    exe: () => openHelpDialog(),
 | 
			
		||||
  },
 | 
			
		||||
  startdm: {
 | 
			
		||||
    name: 'startdm',
 | 
			
		||||
    description: 'Start direct message with user. Example: /startdm userId1',
 | 
			
		||||
    exe: async (roomId, data) => {
 | 
			
		||||
      const mx = initMatrix.matrixClient;
 | 
			
		||||
      const rawIds = data.split(' ');
 | 
			
		||||
      const userIds = rawIds.filter((id) => id.match(MXID_REG) && id !== mx.getUserId());
 | 
			
		||||
      if (userIds.length === 0) return;
 | 
			
		||||
      if (userIds.length === 1) {
 | 
			
		||||
        const dmRoomId = hasDMWith(userIds[0]);
 | 
			
		||||
        if (dmRoomId) {
 | 
			
		||||
          selectRoom(dmRoomId);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      const devices = await Promise.all(userIds.map(hasDevices));
 | 
			
		||||
      const isEncrypt = devices.every((hasDevice) => hasDevice);
 | 
			
		||||
      const result = await roomActions.createDM(userIds, isEncrypt);
 | 
			
		||||
      selectRoom(result.room_id);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  join: {
 | 
			
		||||
    name: 'join',
 | 
			
		||||
    description: 'Join room with address. Example: /join address1 address2',
 | 
			
		||||
    exe: (roomId, data) => {
 | 
			
		||||
      const rawIds = data.split(' ');
 | 
			
		||||
      const roomIds = rawIds.filter((id) => id.match(ROOM_ID_ALIAS_REG));
 | 
			
		||||
      roomIds.map((id) => roomActions.join(id));
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  leave: {
 | 
			
		||||
    name: 'leave',
 | 
			
		||||
    description: 'Leave current room.',
 | 
			
		||||
    exe: (roomId, data) => {
 | 
			
		||||
      if (data.trim() === '') {
 | 
			
		||||
        roomActions.leave(roomId);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const rawIds = data.split(' ');
 | 
			
		||||
      const roomIds = rawIds.filter((id) => id.match(ROOM_ID_REG));
 | 
			
		||||
      roomIds.map((id) => roomActions.leave(id));
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  invite: {
 | 
			
		||||
    name: 'invite',
 | 
			
		||||
    description: 'Invite user to room. Example: /invite userId1 userId2 [-r reason]',
 | 
			
		||||
    exe: (roomId, data) => {
 | 
			
		||||
      const { userIds, reason } = processMxidAndReason(data);
 | 
			
		||||
      userIds.map((id) => roomActions.invite(roomId, id, reason));
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  disinvite: {
 | 
			
		||||
    name: 'disinvite',
 | 
			
		||||
    description: 'Disinvite user to room. Example: /disinvite userId1 userId2 [-r reason]',
 | 
			
		||||
    exe: (roomId, data) => {
 | 
			
		||||
      const { userIds, reason } = processMxidAndReason(data);
 | 
			
		||||
      userIds.map((id) => roomActions.kick(roomId, id, reason));
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  kick: {
 | 
			
		||||
    name: 'kick',
 | 
			
		||||
    description: 'Kick user from room. Example: /kick userId1 userId2 [-r reason]',
 | 
			
		||||
    exe: (roomId, data) => {
 | 
			
		||||
      const { userIds, reason } = processMxidAndReason(data);
 | 
			
		||||
      userIds.map((id) => roomActions.kick(roomId, id, reason));
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  ban: {
 | 
			
		||||
    name: 'ban',
 | 
			
		||||
    description: 'Ban user from room. Example: /ban userId1 userId2 [-r reason]',
 | 
			
		||||
    exe: (roomId, data) => {
 | 
			
		||||
      const { userIds, reason } = processMxidAndReason(data);
 | 
			
		||||
      userIds.map((id) => roomActions.ban(roomId, id, reason));
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  unban: {
 | 
			
		||||
    name: 'unban',
 | 
			
		||||
    description: 'Unban user from room. Example: /unban userId1 userId2',
 | 
			
		||||
    exe: (roomId, data) => {
 | 
			
		||||
      const rawIds = data.split(' ');
 | 
			
		||||
      const userIds = rawIds.filter((id) => id.match(MXID_REG));
 | 
			
		||||
      userIds.map((id) => roomActions.unban(roomId, id));
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  ignore: {
 | 
			
		||||
    name: 'ignore',
 | 
			
		||||
    description: 'Ignore user. Example: /ignore userId1 userId2',
 | 
			
		||||
    exe: (roomId, data) => {
 | 
			
		||||
      const rawIds = data.split(' ');
 | 
			
		||||
      const userIds = rawIds.filter((id) => id.match(MXID_REG));
 | 
			
		||||
      if (userIds.length > 0) roomActions.ignore(userIds);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  unignore: {
 | 
			
		||||
    name: 'unignore',
 | 
			
		||||
    description: 'Unignore user. Example: /unignore userId1 userId2',
 | 
			
		||||
    exe: (roomId, data) => {
 | 
			
		||||
      const rawIds = data.split(' ');
 | 
			
		||||
      const userIds = rawIds.filter((id) => id.match(MXID_REG));
 | 
			
		||||
      if (userIds.length > 0) roomActions.unignore(userIds);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  myroomnick: {
 | 
			
		||||
    name: 'myroomnick',
 | 
			
		||||
    description: 'Change nick in current room.',
 | 
			
		||||
    exe: (roomId, data) => {
 | 
			
		||||
      const nick = data.trim();
 | 
			
		||||
      if (nick === '') return;
 | 
			
		||||
      roomActions.setMyRoomNick(roomId, nick);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  myroomavatar: {
 | 
			
		||||
    name: 'myroomavatar',
 | 
			
		||||
    description: 'Change profile picture in current room. Example /myroomavatar mxc://xyzabc',
 | 
			
		||||
    exe: (roomId, data) => {
 | 
			
		||||
      if (data.match(MXC_REG)) {
 | 
			
		||||
        roomActions.setMyRoomAvatar(roomId, data);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  converttodm: {
 | 
			
		||||
    name: 'converttodm',
 | 
			
		||||
    description: 'Convert room to direct message',
 | 
			
		||||
    exe: (roomId) => {
 | 
			
		||||
      roomActions.convertToDm(roomId);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  converttoroom: {
 | 
			
		||||
    name: 'converttoroom',
 | 
			
		||||
    description: 'Convert direct message to room',
 | 
			
		||||
    exe: (roomId) => {
 | 
			
		||||
      roomActions.convertToRoom(roomId);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function openHelpDialog() {
 | 
			
		||||
  openReusableDialog(
 | 
			
		||||
    <Text variant="s1" weight="medium">Commands</Text>,
 | 
			
		||||
    () => (
 | 
			
		||||
      <div className="commands-dialog">
 | 
			
		||||
        {Object.keys(commands).map((cmdName) => (
 | 
			
		||||
          <SettingTile
 | 
			
		||||
            key={cmdName}
 | 
			
		||||
            title={cmdName}
 | 
			
		||||
            content={<Text variant="b3">{commands[cmdName].description}</Text>}
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    ),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default commands;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +0,0 @@
 | 
			
		|||
.commands-dialog {
 | 
			
		||||
  & > * {
 | 
			
		||||
    padding: var(--sp-tight) var(--sp-normal);
 | 
			
		||||
    border-bottom: 1px solid var(--bg-surface-border);
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      border-bottom: none;
 | 
			
		||||
      margin-bottom: var(--sp-extra-loose);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,222 +0,0 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
 | 
			
		||||
 | 
			
		||||
function getTimelineJSXMessages() {
 | 
			
		||||
  return {
 | 
			
		||||
    join(user) {
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
          {' joined the room'}
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    leave(user, reason) {
 | 
			
		||||
      const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
          {' left the room'}
 | 
			
		||||
          {twemojify(reasonMsg)}
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    invite(inviter, user) {
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(inviter)}</b>
 | 
			
		||||
          {' invited '}
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    cancelInvite(inviter, user) {
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(inviter)}</b>
 | 
			
		||||
          {' canceled '}
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
          {'\'s invite'}
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    rejectInvite(user) {
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
          {' rejected the invitation'}
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    kick(actor, user, reason) {
 | 
			
		||||
      const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(actor)}</b>
 | 
			
		||||
          {' kicked '}
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
          {twemojify(reasonMsg)}
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    ban(actor, user, reason) {
 | 
			
		||||
      const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(actor)}</b>
 | 
			
		||||
          {' banned '}
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
          {twemojify(reasonMsg)}
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    unban(actor, user) {
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(actor)}</b>
 | 
			
		||||
          {' unbanned '}
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    avatarSets(user) {
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
          {' set a avatar'}
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    avatarChanged(user) {
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
          {' changed their avatar'}
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    avatarRemoved(user) {
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
          {' removed their avatar'}
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    nameSets(user, newName) {
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
          {' set display name to '}
 | 
			
		||||
          <b>{twemojify(newName)}</b>
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    nameChanged(user, newName) {
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
          {' changed their display name to '}
 | 
			
		||||
          <b>{twemojify(newName)}</b>
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    nameRemoved(user, lastName) {
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          <b>{twemojify(user)}</b>
 | 
			
		||||
          {' removed their display name '}
 | 
			
		||||
          <b>{twemojify(lastName)}</b>
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getUsersActionJsx(roomId, userIds, actionStr) {
 | 
			
		||||
  const room = initMatrix.matrixClient.getRoom(roomId);
 | 
			
		||||
  const getUserDisplayName = (userId) => {
 | 
			
		||||
    if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
 | 
			
		||||
    return getUsername(userId);
 | 
			
		||||
  };
 | 
			
		||||
  const getUserJSX = (userId) => <b>{twemojify(getUserDisplayName(userId))}</b>;
 | 
			
		||||
  if (!Array.isArray(userIds)) return 'Idle';
 | 
			
		||||
  if (userIds.length === 0) return 'Idle';
 | 
			
		||||
  const MAX_VISIBLE_COUNT = 3;
 | 
			
		||||
 | 
			
		||||
  const u1Jsx = getUserJSX(userIds[0]);
 | 
			
		||||
  // eslint-disable-next-line react/jsx-one-expression-per-line
 | 
			
		||||
  if (userIds.length === 1) return <>{u1Jsx} is {actionStr}</>;
 | 
			
		||||
 | 
			
		||||
  const u2Jsx = getUserJSX(userIds[1]);
 | 
			
		||||
  // eslint-disable-next-line react/jsx-one-expression-per-line
 | 
			
		||||
  if (userIds.length === 2) return <>{u1Jsx} and {u2Jsx} are {actionStr}</>;
 | 
			
		||||
 | 
			
		||||
  const u3Jsx = getUserJSX(userIds[2]);
 | 
			
		||||
  if (userIds.length === 3) {
 | 
			
		||||
    // eslint-disable-next-line react/jsx-one-expression-per-line
 | 
			
		||||
    return <>{u1Jsx}, {u2Jsx} and {u3Jsx} are {actionStr}</>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const othersCount = userIds.length - MAX_VISIBLE_COUNT;
 | 
			
		||||
  // eslint-disable-next-line react/jsx-one-expression-per-line
 | 
			
		||||
  return <>{u1Jsx}, {u2Jsx}, {u3Jsx} and {othersCount} others are {actionStr}</>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function parseTimelineChange(mEvent) {
 | 
			
		||||
  const tJSXMsgs = getTimelineJSXMessages();
 | 
			
		||||
  const makeReturnObj = (variant, content) => ({
 | 
			
		||||
    variant,
 | 
			
		||||
    content,
 | 
			
		||||
  });
 | 
			
		||||
  const content = mEvent.getContent();
 | 
			
		||||
  const prevContent = mEvent.getPrevContent();
 | 
			
		||||
  const sender = mEvent.getSender();
 | 
			
		||||
  const senderName = getUsername(sender);
 | 
			
		||||
  const userName = getUsername(mEvent.getStateKey());
 | 
			
		||||
 | 
			
		||||
  switch (content.membership) {
 | 
			
		||||
    case 'invite': return makeReturnObj('invite', tJSXMsgs.invite(senderName, userName));
 | 
			
		||||
    case 'ban': return makeReturnObj('leave', tJSXMsgs.ban(senderName, userName, content.reason));
 | 
			
		||||
    case 'join':
 | 
			
		||||
      if (prevContent.membership === 'join') {
 | 
			
		||||
        if (content.displayname !== prevContent.displayname) {
 | 
			
		||||
          if (typeof content.displayname === 'undefined') return makeReturnObj('avatar', tJSXMsgs.nameRemoved(sender, prevContent.displayname));
 | 
			
		||||
          if (typeof prevContent.displayname === 'undefined') return makeReturnObj('avatar', tJSXMsgs.nameSets(sender, content.displayname));
 | 
			
		||||
          return makeReturnObj('avatar', tJSXMsgs.nameChanged(prevContent.displayname, content.displayname));
 | 
			
		||||
        }
 | 
			
		||||
        if (content.avatar_url !== prevContent.avatar_url) {
 | 
			
		||||
          if (typeof content.avatar_url === 'undefined') return makeReturnObj('avatar', tJSXMsgs.avatarRemoved(content.displayname));
 | 
			
		||||
          if (typeof prevContent.avatar_url === 'undefined') return makeReturnObj('avatar', tJSXMsgs.avatarSets(content.displayname));
 | 
			
		||||
          return makeReturnObj('avatar', tJSXMsgs.avatarChanged(content.displayname));
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
      return makeReturnObj('join', tJSXMsgs.join(senderName));
 | 
			
		||||
    case 'leave':
 | 
			
		||||
      if (sender === mEvent.getStateKey()) {
 | 
			
		||||
        switch (prevContent.membership) {
 | 
			
		||||
          case 'invite': return makeReturnObj('invite-cancel', tJSXMsgs.rejectInvite(senderName));
 | 
			
		||||
          default: return makeReturnObj('leave', tJSXMsgs.leave(senderName, content.reason));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      switch (prevContent.membership) {
 | 
			
		||||
        case 'invite': return makeReturnObj('invite-cancel', tJSXMsgs.cancelInvite(senderName, userName));
 | 
			
		||||
        case 'ban': return makeReturnObj('other', tJSXMsgs.unban(senderName, userName));
 | 
			
		||||
        // sender is not target and made the target leave,
 | 
			
		||||
        // if not from invite/ban then this is a kick
 | 
			
		||||
        default: return makeReturnObj('leave', tJSXMsgs.kick(senderName, userName, content.reason));
 | 
			
		||||
      }
 | 
			
		||||
    default: return null;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  getTimelineJSXMessages,
 | 
			
		||||
  getUsersActionJsx,
 | 
			
		||||
  parseTimelineChange,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import { useAtomValue } from 'jotai';
 | 
			
		||||
import './Search.scss';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +20,11 @@ import RoomSelector from '../../molecules/room-selector/RoomSelector';
 | 
			
		|||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
 | 
			
		||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 | 
			
		||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
 | 
			
		||||
import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList';
 | 
			
		||||
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
 | 
			
		||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
 | 
			
		||||
import { allRoomsAtom } from '../../state/room-list/roomList';
 | 
			
		||||
import { mDirectAtom } from '../../state/mDirectList';
 | 
			
		||||
 | 
			
		||||
function useVisiblityToggle(setResult) {
 | 
			
		||||
  const [isOpen, setIsOpen] = useState(false);
 | 
			
		||||
| 
						 | 
				
			
			@ -48,9 +54,8 @@ function useVisiblityToggle(setResult) {
 | 
			
		|||
  return [isOpen, requestClose];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function mapRoomIds(roomIds) {
 | 
			
		||||
function mapRoomIds(roomIds, directs, roomIdToParents) {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const { directs, roomIdToParents } = initMatrix.roomList;
 | 
			
		||||
 | 
			
		||||
  return roomIds.map((roomId) => {
 | 
			
		||||
    const room = mx.getRoom(roomId);
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +67,7 @@ function mapRoomIds(roomIds) {
 | 
			
		|||
 | 
			
		||||
    let type = 'room';
 | 
			
		||||
    if (room.isSpaceRoom()) type = 'space';
 | 
			
		||||
    else if (directs.has(roomId)) type = 'direct';
 | 
			
		||||
    else if (directs.includes(roomId)) type = 'direct';
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      type,
 | 
			
		||||
| 
						 | 
				
			
			@ -81,6 +86,12 @@ function Search() {
 | 
			
		|||
  const searchRef = useRef(null);
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const { navigateRoom, navigateSpace } = useRoomNavigate();
 | 
			
		||||
  const mDirects = useAtomValue(mDirectAtom);
 | 
			
		||||
  const spaces = useSpaces(mx, allRoomsAtom);
 | 
			
		||||
  const rooms = useRooms(mx, allRoomsAtom, mDirects);
 | 
			
		||||
  const directs = useDirects(mx, allRoomsAtom, mDirects);
 | 
			
		||||
  const roomToUnread = useAtomValue(roomToUnreadAtom);
 | 
			
		||||
  const roomToParents = useAtomValue(roomToParentsAtom);
 | 
			
		||||
 | 
			
		||||
  const handleSearchResults = (chunk, term) => {
 | 
			
		||||
    setResult({
 | 
			
		||||
| 
						 | 
				
			
			@ -97,7 +108,6 @@ function Search() {
 | 
			
		|||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { spaces, rooms, directs } = initMatrix.roomList;
 | 
			
		||||
    let ids = null;
 | 
			
		||||
 | 
			
		||||
    if (prefix) {
 | 
			
		||||
| 
						 | 
				
			
			@ -109,15 +119,15 @@ function Search() {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    ids.sort(roomIdByActivity);
 | 
			
		||||
    const mappedIds = mapRoomIds(ids);
 | 
			
		||||
    const mappedIds = mapRoomIds(ids, directs, roomToParents);
 | 
			
		||||
    asyncSearch.setup(mappedIds, { keys: 'name', isContain: true, limit: 20 });
 | 
			
		||||
    if (prefix) handleSearchResults(mappedIds, prefix);
 | 
			
		||||
    else asyncSearch.search(term);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const loadRecentRooms = () => {
 | 
			
		||||
    const { recentRooms } = navigation;
 | 
			
		||||
    handleSearchResults(mapRoomIds(recentRooms).reverse());
 | 
			
		||||
    const recentRooms = [];
 | 
			
		||||
    handleSearchResults(mapRoomIds(recentRooms, directs, roomToParents).reverse());
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleAfterOpen = () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -169,7 +179,6 @@ function Search() {
 | 
			
		|||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const noti = initMatrix.notifications;
 | 
			
		||||
  const renderRoomSelector = (item) => {
 | 
			
		||||
    let imageSrc = null;
 | 
			
		||||
    let iconSrc = null;
 | 
			
		||||
| 
						 | 
				
			
			@ -188,9 +197,9 @@ function Search() {
 | 
			
		|||
        roomId={item.roomId}
 | 
			
		||||
        imageSrc={imageSrc}
 | 
			
		||||
        iconSrc={iconSrc}
 | 
			
		||||
        isUnread={noti.hasNoti(item.roomId)}
 | 
			
		||||
        notificationCount={noti.getTotalNoti(item.roomId)}
 | 
			
		||||
        isAlert={noti.getHighlightNoti(item.roomId) > 0}
 | 
			
		||||
        isUnread={roomToUnread.has(item.roomId)}
 | 
			
		||||
        notificationCount={roomToUnread.get(item.roomId)?.total ?? 0}
 | 
			
		||||
        isAlert={roomToUnread.get(item.roomId)?.highlight > 0}
 | 
			
		||||
        onClick={() => openItem(item.roomId, item.type)}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,6 @@ import React, { useState } from 'react';
 | 
			
		|||
import './CrossSigning.scss';
 | 
			
		||||
import FileSaver from 'file-saver';
 | 
			
		||||
import { Formik } from 'formik';
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import { openReusableDialog } from '../../../client/action/navigation';
 | 
			
		||||
| 
						 | 
				
			
			@ -22,15 +21,17 @@ import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
 | 
			
		|||
const failedDialog = () => {
 | 
			
		||||
  const renderFailure = (requestClose) => (
 | 
			
		||||
    <div className="cross-signing__failure">
 | 
			
		||||
      <Text variant="h1">{twemojify('❌')}</Text>
 | 
			
		||||
      <Text variant="h1">❌</Text>
 | 
			
		||||
      <Text weight="medium">Failed to setup cross signing. Please try again.</Text>
 | 
			
		||||
      <Button onClick={requestClose}>Close</Button>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  openReusableDialog(
 | 
			
		||||
    <Text variant="s1" weight="medium">Setup cross signing</Text>,
 | 
			
		||||
    renderFailure,
 | 
			
		||||
    <Text variant="s1" weight="medium">
 | 
			
		||||
      Setup cross signing
 | 
			
		||||
    </Text>,
 | 
			
		||||
    renderFailure
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -48,11 +49,11 @@ const securityKeyDialog = (key) => {
 | 
			
		|||
  const renderSecurityKey = () => (
 | 
			
		||||
    <div className="cross-signing__key">
 | 
			
		||||
      <Text weight="medium">Please save this security key somewhere safe.</Text>
 | 
			
		||||
      <Text className="cross-signing__key-text">
 | 
			
		||||
        {key.encodedPrivateKey}
 | 
			
		||||
      </Text>
 | 
			
		||||
      <Text className="cross-signing__key-text">{key.encodedPrivateKey}</Text>
 | 
			
		||||
      <div className="cross-signing__key-btn">
 | 
			
		||||
        <Button variant="primary" onClick={() => copyKey(key)}>Copy</Button>
 | 
			
		||||
        <Button variant="primary" onClick={() => copyKey(key)}>
 | 
			
		||||
          Copy
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button onClick={() => downloadKey(key)}>Download</Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -62,8 +63,10 @@ const securityKeyDialog = (key) => {
 | 
			
		|||
  downloadKey();
 | 
			
		||||
 | 
			
		||||
  openReusableDialog(
 | 
			
		||||
    <Text variant="s1" weight="medium">Security Key</Text>,
 | 
			
		||||
    () => renderSecurityKey(),
 | 
			
		||||
    <Text variant="s1" weight="medium">
 | 
			
		||||
      Security Key
 | 
			
		||||
    </Text>,
 | 
			
		||||
    () => renderSecurityKey()
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +115,7 @@ function CrossSigningSetup() {
 | 
			
		|||
      errors.phrase = 'Phrase must contain 8-127 characters with no space.';
 | 
			
		||||
    }
 | 
			
		||||
    if (values.confirmPhrase.length > 0 && values.confirmPhrase !== values.phrase) {
 | 
			
		||||
      errors.confirmPhrase = 'Phrase don\'t match.';
 | 
			
		||||
      errors.confirmPhrase = "Phrase don't match.";
 | 
			
		||||
    }
 | 
			
		||||
    return errors;
 | 
			
		||||
  };
 | 
			
		||||
| 
						 | 
				
			
			@ -121,10 +124,14 @@ function CrossSigningSetup() {
 | 
			
		|||
    <div className="cross-signing__setup">
 | 
			
		||||
      <div className="cross-signing__setup-entry">
 | 
			
		||||
        <Text>
 | 
			
		||||
          We will generate a <b>Security Key</b>, 
 | 
			
		||||
          which you can use to manage messages backup and session verification.
 | 
			
		||||
          We will generate a <b>Security Key</b>, which you can use to manage messages backup and
 | 
			
		||||
          session verification.
 | 
			
		||||
        </Text>
 | 
			
		||||
        {genWithPhrase !== false && <Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>Generate Key</Button>}
 | 
			
		||||
        {genWithPhrase !== false && (
 | 
			
		||||
          <Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>
 | 
			
		||||
            Generate Key
 | 
			
		||||
          </Button>
 | 
			
		||||
        )}
 | 
			
		||||
        {genWithPhrase === false && <Spinner size="small" />}
 | 
			
		||||
      </div>
 | 
			
		||||
      <Text className="cross-signing__setup-divider">OR</Text>
 | 
			
		||||
| 
						 | 
				
			
			@ -133,9 +140,7 @@ function CrossSigningSetup() {
 | 
			
		|||
        onSubmit={(values) => setup(values.phrase)}
 | 
			
		||||
        validate={validator}
 | 
			
		||||
      >
 | 
			
		||||
        {({
 | 
			
		||||
          values, errors, handleChange, handleSubmit,
 | 
			
		||||
        }) => (
 | 
			
		||||
        {({ values, errors, handleChange, handleSubmit }) => (
 | 
			
		||||
          <form
 | 
			
		||||
            className="cross-signing__setup-entry"
 | 
			
		||||
            onSubmit={handleSubmit}
 | 
			
		||||
| 
						 | 
				
			
			@ -143,8 +148,8 @@ function CrossSigningSetup() {
 | 
			
		|||
          >
 | 
			
		||||
            <Text>
 | 
			
		||||
              Alternatively you can also set a <b>Security Phrase </b>
 | 
			
		||||
              so you don't have to remember long Security Key, 
 | 
			
		||||
              and optionally save the Key as backup.
 | 
			
		||||
              so you don't have to remember long Security Key, and optionally save the Key as
 | 
			
		||||
              backup.
 | 
			
		||||
            </Text>
 | 
			
		||||
            <Input
 | 
			
		||||
              name="phrase"
 | 
			
		||||
| 
						 | 
				
			
			@ -155,7 +160,11 @@ function CrossSigningSetup() {
 | 
			
		|||
              required
 | 
			
		||||
              disabled={genWithPhrase !== undefined}
 | 
			
		||||
            />
 | 
			
		||||
            {errors.phrase && <Text variant="b3" className="cross-signing__error">{errors.phrase}</Text>}
 | 
			
		||||
            {errors.phrase && (
 | 
			
		||||
              <Text variant="b3" className="cross-signing__error">
 | 
			
		||||
                {errors.phrase}
 | 
			
		||||
              </Text>
 | 
			
		||||
            )}
 | 
			
		||||
            <Input
 | 
			
		||||
              name="confirmPhrase"
 | 
			
		||||
              value={values.confirmPhrase}
 | 
			
		||||
| 
						 | 
				
			
			@ -165,8 +174,16 @@ function CrossSigningSetup() {
 | 
			
		|||
              required
 | 
			
		||||
              disabled={genWithPhrase !== undefined}
 | 
			
		||||
            />
 | 
			
		||||
            {errors.confirmPhrase && <Text variant="b3" className="cross-signing__error">{errors.confirmPhrase}</Text>}
 | 
			
		||||
            {genWithPhrase !== true && <Button variant="primary" type="submit" disabled={genWithPhrase !== undefined}>Set Phrase & Generate Key</Button>}
 | 
			
		||||
            {errors.confirmPhrase && (
 | 
			
		||||
              <Text variant="b3" className="cross-signing__error">
 | 
			
		||||
                {errors.confirmPhrase}
 | 
			
		||||
              </Text>
 | 
			
		||||
            )}
 | 
			
		||||
            {genWithPhrase !== true && (
 | 
			
		||||
              <Button variant="primary" type="submit" disabled={genWithPhrase !== undefined}>
 | 
			
		||||
                Set Phrase & Generate Key
 | 
			
		||||
              </Button>
 | 
			
		||||
            )}
 | 
			
		||||
            {genWithPhrase === true && <Spinner size="small" />}
 | 
			
		||||
          </form>
 | 
			
		||||
        )}
 | 
			
		||||
| 
						 | 
				
			
			@ -177,31 +194,36 @@ function CrossSigningSetup() {
 | 
			
		|||
 | 
			
		||||
const setupDialog = () => {
 | 
			
		||||
  openReusableDialog(
 | 
			
		||||
    <Text variant="s1" weight="medium">Setup cross signing</Text>,
 | 
			
		||||
    () => <CrossSigningSetup />,
 | 
			
		||||
    <Text variant="s1" weight="medium">
 | 
			
		||||
      Setup cross signing
 | 
			
		||||
    </Text>,
 | 
			
		||||
    () => <CrossSigningSetup />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function CrossSigningReset() {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="cross-signing__reset">
 | 
			
		||||
      <Text variant="h1">{twemojify('✋🧑🚒🤚')}</Text>
 | 
			
		||||
      <Text variant="h1">✋🧑🚒🤚</Text>
 | 
			
		||||
      <Text weight="medium">Resetting cross-signing keys is permanent.</Text>
 | 
			
		||||
      <Text>
 | 
			
		||||
        Anyone you have verified with will see security alerts and your message backup will be lost. 
 | 
			
		||||
        You almost certainly do not want to do this, 
 | 
			
		||||
        unless you have lost <b>Security Key</b> or <b>Phrase</b> and 
 | 
			
		||||
        every session you can cross-sign from.
 | 
			
		||||
        Anyone you have verified with will see security alerts and your message backup will be lost.
 | 
			
		||||
        You almost certainly do not want to do this, unless you have lost <b>Security Key</b> or{' '}
 | 
			
		||||
        <b>Phrase</b> and every session you can cross-sign from.
 | 
			
		||||
      </Text>
 | 
			
		||||
      <Button variant="danger" onClick={setupDialog}>Reset</Button>
 | 
			
		||||
      <Button variant="danger" onClick={setupDialog}>
 | 
			
		||||
        Reset
 | 
			
		||||
      </Button>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const resetDialog = () => {
 | 
			
		||||
  openReusableDialog(
 | 
			
		||||
    <Text variant="s1" weight="medium">Reset cross signing</Text>,
 | 
			
		||||
    () => <CrossSigningReset />,
 | 
			
		||||
    <Text variant="s1" weight="medium">
 | 
			
		||||
      Reset cross signing
 | 
			
		||||
    </Text>,
 | 
			
		||||
    () => <CrossSigningReset />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -210,12 +232,23 @@ function CrossSignin() {
 | 
			
		|||
  return (
 | 
			
		||||
    <SettingTile
 | 
			
		||||
      title="Cross signing"
 | 
			
		||||
      content={<Text variant="b3">Setup to verify and keep track of all your sessions. Also required to backup encrypted message.</Text>}
 | 
			
		||||
      options={(
 | 
			
		||||
        isCSEnabled
 | 
			
		||||
          ? <Button variant="danger" onClick={resetDialog}>Reset</Button>
 | 
			
		||||
          : <Button variant="primary" onClick={setupDialog}>Setup</Button>
 | 
			
		||||
      )}
 | 
			
		||||
      content={
 | 
			
		||||
        <Text variant="b3">
 | 
			
		||||
          Setup to verify and keep track of all your sessions. Also required to backup encrypted
 | 
			
		||||
          message.
 | 
			
		||||
        </Text>
 | 
			
		||||
      }
 | 
			
		||||
      options={
 | 
			
		||||
        isCSEnabled ? (
 | 
			
		||||
          <Button variant="danger" onClick={resetDialog}>
 | 
			
		||||
            Reset
 | 
			
		||||
          </Button>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Button variant="primary" onClick={setupDialog}>
 | 
			
		||||
            Setup
 | 
			
		||||
          </Button>
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@
 | 
			
		|||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './KeyBackup.scss';
 | 
			
		||||
import { twemojify } from '../../../util/twemojify';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import { openReusableDialog } from '../../../client/action/navigation';
 | 
			
		||||
| 
						 | 
				
			
			@ -34,10 +33,7 @@ function CreateKeyBackupDialog({ keyData }) {
 | 
			
		|||
    let info;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      info = await mx.prepareKeyBackupVersion(
 | 
			
		||||
        null,
 | 
			
		||||
        { secureSecretStorage: true },
 | 
			
		||||
      );
 | 
			
		||||
      info = await mx.prepareKeyBackupVersion(null, { secureSecretStorage: true });
 | 
			
		||||
      info = await mx.createKeyBackupVersion(info);
 | 
			
		||||
      await mx.scheduleAllGroupSessionsForBackup();
 | 
			
		||||
      if (!mountStore.getItem()) return;
 | 
			
		||||
| 
						 | 
				
			
			@ -65,7 +61,7 @@ function CreateKeyBackupDialog({ keyData }) {
 | 
			
		|||
      )}
 | 
			
		||||
      {done === true && (
 | 
			
		||||
        <>
 | 
			
		||||
          <Text variant="h1">{twemojify('✅')}</Text>
 | 
			
		||||
          <Text variant="h1">✅</Text>
 | 
			
		||||
          <Text>Successfully created backup</Text>
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
| 
						 | 
				
			
			@ -104,12 +100,9 @@ function RestoreKeyBackupDialog({ keyData }) {
 | 
			
		|||
 | 
			
		||||
    try {
 | 
			
		||||
      const backupInfo = await mx.getKeyBackupVersion();
 | 
			
		||||
      const info = await mx.restoreKeyBackupWithSecretStorage(
 | 
			
		||||
        backupInfo,
 | 
			
		||||
        undefined,
 | 
			
		||||
        undefined,
 | 
			
		||||
        { progressCallback },
 | 
			
		||||
      );
 | 
			
		||||
      const info = await mx.restoreKeyBackupWithSecretStorage(backupInfo, undefined, undefined, {
 | 
			
		||||
        progressCallback,
 | 
			
		||||
      });
 | 
			
		||||
      if (!mountStore.getItem()) return;
 | 
			
		||||
      setStatus({ done: `Successfully restored backup keys (${info.imported}/${info.total}).` });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
| 
						 | 
				
			
			@ -138,7 +131,7 @@ function RestoreKeyBackupDialog({ keyData }) {
 | 
			
		|||
      )}
 | 
			
		||||
      {status.done && (
 | 
			
		||||
        <>
 | 
			
		||||
          <Text variant="h1">{twemojify('✅')}</Text>
 | 
			
		||||
          <Text variant="h1">✅</Text>
 | 
			
		||||
          <Text>{status.done}</Text>
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
| 
						 | 
				
			
			@ -176,14 +169,16 @@ function DeleteKeyBackupDialog({ requestClose }) {
 | 
			
		|||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="key-backup__delete">
 | 
			
		||||
      <Text variant="h1">{twemojify('🗑')}</Text>
 | 
			
		||||
      <Text variant="h1">🗑</Text>
 | 
			
		||||
      <Text weight="medium">Deleting key backup is permanent.</Text>
 | 
			
		||||
      <Text>All encrypted messages keys stored on server will be deleted.</Text>
 | 
			
		||||
      {
 | 
			
		||||
        isDeleting
 | 
			
		||||
          ? <Spinner size="small" />
 | 
			
		||||
          : <Button variant="danger" onClick={deleteBackup}>Delete</Button>
 | 
			
		||||
      }
 | 
			
		||||
      {isDeleting ? (
 | 
			
		||||
        <Spinner size="small" />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <Button variant="danger" onClick={deleteBackup}>
 | 
			
		||||
          Delete
 | 
			
		||||
        </Button>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -224,9 +219,11 @@ function KeyBackup() {
 | 
			
		|||
    if (keyData === null) return;
 | 
			
		||||
 | 
			
		||||
    openReusableDialog(
 | 
			
		||||
      <Text variant="s1" weight="medium">Create Key Backup</Text>,
 | 
			
		||||
      <Text variant="s1" weight="medium">
 | 
			
		||||
        Create Key Backup
 | 
			
		||||
      </Text>,
 | 
			
		||||
      () => <CreateKeyBackupDialog keyData={keyData} />,
 | 
			
		||||
      () => fetchKeyBackupVersion(),
 | 
			
		||||
      () => fetchKeyBackupVersion()
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -235,29 +232,44 @@ function KeyBackup() {
 | 
			
		|||
    if (keyData === null) return;
 | 
			
		||||
 | 
			
		||||
    openReusableDialog(
 | 
			
		||||
      <Text variant="s1" weight="medium">Restore Key Backup</Text>,
 | 
			
		||||
      () => <RestoreKeyBackupDialog keyData={keyData} />,
 | 
			
		||||
      <Text variant="s1" weight="medium">
 | 
			
		||||
        Restore Key Backup
 | 
			
		||||
      </Text>,
 | 
			
		||||
      () => <RestoreKeyBackupDialog keyData={keyData} />
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const openDeleteKeyBackup = () => openReusableDialog(
 | 
			
		||||
    <Text variant="s1" weight="medium">Delete Key Backup</Text>,
 | 
			
		||||
    (requestClose) => (
 | 
			
		||||
      <DeleteKeyBackupDialog
 | 
			
		||||
        requestClose={(isDone) => {
 | 
			
		||||
          if (isDone) setKeyBackup(null);
 | 
			
		||||
          requestClose();
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    ),
 | 
			
		||||
  );
 | 
			
		||||
  const openDeleteKeyBackup = () =>
 | 
			
		||||
    openReusableDialog(
 | 
			
		||||
      <Text variant="s1" weight="medium">
 | 
			
		||||
        Delete Key Backup
 | 
			
		||||
      </Text>,
 | 
			
		||||
      (requestClose) => (
 | 
			
		||||
        <DeleteKeyBackupDialog
 | 
			
		||||
          requestClose={(isDone) => {
 | 
			
		||||
            if (isDone) setKeyBackup(null);
 | 
			
		||||
            requestClose();
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  const renderOptions = () => {
 | 
			
		||||
    if (keyBackup === undefined) return <Spinner size="small" />;
 | 
			
		||||
    if (keyBackup === null) return <Button variant="primary" onClick={openCreateKeyBackup}>Create Backup</Button>;
 | 
			
		||||
    if (keyBackup === null)
 | 
			
		||||
      return (
 | 
			
		||||
        <Button variant="primary" onClick={openCreateKeyBackup}>
 | 
			
		||||
          Create Backup
 | 
			
		||||
        </Button>
 | 
			
		||||
      );
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <IconButton src={DownloadIC} variant="positive" onClick={openRestoreKeyBackup} tooltip="Restore backup" />
 | 
			
		||||
        <IconButton
 | 
			
		||||
          src={DownloadIC}
 | 
			
		||||
          variant="positive"
 | 
			
		||||
          onClick={openRestoreKeyBackup}
 | 
			
		||||
          tooltip="Restore backup"
 | 
			
		||||
        />
 | 
			
		||||
        <IconButton src={BinIC} onClick={openDeleteKeyBackup} tooltip="Delete backup" />
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			@ -266,9 +278,12 @@ function KeyBackup() {
 | 
			
		|||
  return (
 | 
			
		||||
    <SettingTile
 | 
			
		||||
      title="Encrypted messages backup"
 | 
			
		||||
      content={(
 | 
			
		||||
      content={
 | 
			
		||||
        <>
 | 
			
		||||
          <Text variant="b3">Online backup your encrypted messages keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.</Text>
 | 
			
		||||
          <Text variant="b3">
 | 
			
		||||
            Online backup your encrypted messages keys with your account data in case you lose
 | 
			
		||||
            access to your sessions. Your keys will be secured with a unique Security Key.
 | 
			
		||||
          </Text>
 | 
			
		||||
          {!isCSEnabled && (
 | 
			
		||||
            <InfoCard
 | 
			
		||||
              style={{ marginTop: 'var(--sp-ultra-tight)' }}
 | 
			
		||||
| 
						 | 
				
			
			@ -279,7 +294,7 @@ function KeyBackup() {
 | 
			
		|||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
      }
 | 
			
		||||
      options={isCSEnabled ? renderOptions() : null}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,169 +0,0 @@
 | 
			
		|||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import './ShortcutSpaces.scss';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import navigation from '../../../client/state/navigation';
 | 
			
		||||
import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/accountData';
 | 
			
		||||
import { joinRuleToIconSrc } from '../../../util/matrixUtil';
 | 
			
		||||
import { roomIdByAtoZ } from '../../../util/sort';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import Button from '../../atoms/button/Button';
 | 
			
		||||
import IconButton from '../../atoms/button/IconButton';
 | 
			
		||||
import Checkbox from '../../atoms/button/Checkbox';
 | 
			
		||||
import Spinner from '../../atoms/spinner/Spinner';
 | 
			
		||||
import RoomSelector from '../../molecules/room-selector/RoomSelector';
 | 
			
		||||
import Dialog from '../../molecules/dialog/Dialog';
 | 
			
		||||
 | 
			
		||||
import PinIC from '../../../../public/res/ic/outlined/pin.svg';
 | 
			
		||||
import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
 | 
			
		||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 | 
			
		||||
 | 
			
		||||
import { useSpaceShortcut } from '../../hooks/useSpaceShortcut';
 | 
			
		||||
 | 
			
		||||
function ShortcutSpacesContent() {
 | 
			
		||||
  const mx = initMatrix.matrixClient;
 | 
			
		||||
  const { spaces, roomIdToParents } = initMatrix.roomList;
 | 
			
		||||
 | 
			
		||||
  const [spaceShortcut] = useSpaceShortcut();
 | 
			
		||||
  const spaceWithoutShortcut = [...spaces].filter(
 | 
			
		||||
    (spaceId) => !spaceShortcut.includes(spaceId),
 | 
			
		||||
  ).sort(roomIdByAtoZ);
 | 
			
		||||
 | 
			
		||||
  const [process, setProcess] = useState(null);
 | 
			
		||||
  const [selected, setSelected] = useState([]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (process !== null) {
 | 
			
		||||
      setProcess(null);
 | 
			
		||||
      setSelected([]);
 | 
			
		||||
    }
 | 
			
		||||
  }, [spaceShortcut]);
 | 
			
		||||
 | 
			
		||||
  const toggleSelection = (sId) => {
 | 
			
		||||
    if (process !== null) return;
 | 
			
		||||
    const newSelected = [...selected];
 | 
			
		||||
    const selectedIndex = newSelected.indexOf(sId);
 | 
			
		||||
 | 
			
		||||
    if (selectedIndex > -1) {
 | 
			
		||||
      newSelected.splice(selectedIndex, 1);
 | 
			
		||||
      setSelected(newSelected);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    newSelected.push(sId);
 | 
			
		||||
    setSelected(newSelected);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleAdd = () => {
 | 
			
		||||
    setProcess(`Pinning ${selected.length} spaces...`);
 | 
			
		||||
    createSpaceShortcut(selected);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const renderSpace = (spaceId, isShortcut) => {
 | 
			
		||||
    const room = mx.getRoom(spaceId);
 | 
			
		||||
    if (!room) return null;
 | 
			
		||||
 | 
			
		||||
    const parentSet = roomIdToParents.get(spaceId);
 | 
			
		||||
    const parentNames = parentSet
 | 
			
		||||
      ? [...parentSet].map((parentId) => mx.getRoom(parentId).name)
 | 
			
		||||
      : undefined;
 | 
			
		||||
    const parents = parentNames ? parentNames.join(', ') : null;
 | 
			
		||||
 | 
			
		||||
    const toggleSelected = () => toggleSelection(spaceId);
 | 
			
		||||
    const deleteShortcut = () => deleteSpaceShortcut(spaceId);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <RoomSelector
 | 
			
		||||
        key={spaceId}
 | 
			
		||||
        name={room.name}
 | 
			
		||||
        parentName={parents}
 | 
			
		||||
        roomId={spaceId}
 | 
			
		||||
        imageSrc={null}
 | 
			
		||||
        iconSrc={joinRuleToIconSrc(room.getJoinRule(), true)}
 | 
			
		||||
        isUnread={false}
 | 
			
		||||
        notificationCount={0}
 | 
			
		||||
        isAlert={false}
 | 
			
		||||
        onClick={isShortcut ? deleteShortcut : toggleSelected}
 | 
			
		||||
        options={isShortcut ? (
 | 
			
		||||
          <IconButton
 | 
			
		||||
            src={isShortcut ? PinFilledIC : PinIC}
 | 
			
		||||
            size="small"
 | 
			
		||||
            onClick={deleteShortcut}
 | 
			
		||||
            disabled={process !== null}
 | 
			
		||||
          />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Checkbox
 | 
			
		||||
            isActive={selected.includes(spaceId)}
 | 
			
		||||
            variant="positive"
 | 
			
		||||
            onToggle={toggleSelected}
 | 
			
		||||
            tabIndex={-1}
 | 
			
		||||
            disabled={process !== null}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Text className="shortcut-spaces__header" variant="b3" weight="bold">Pinned spaces</Text>
 | 
			
		||||
      {spaceShortcut.length === 0 && <Text>No pinned spaces</Text>}
 | 
			
		||||
      {spaceShortcut.map((spaceId) => renderSpace(spaceId, true))}
 | 
			
		||||
      <Text className="shortcut-spaces__header" variant="b3" weight="bold">Unpinned spaces</Text>
 | 
			
		||||
      {spaceWithoutShortcut.length === 0 && <Text>No unpinned spaces</Text>}
 | 
			
		||||
      {spaceWithoutShortcut.map((spaceId) => renderSpace(spaceId, false))}
 | 
			
		||||
      {selected.length !== 0 && (
 | 
			
		||||
        <div className="shortcut-spaces__footer">
 | 
			
		||||
          {process && <Spinner size="small" />}
 | 
			
		||||
          <Text weight="medium">{process || `${selected.length} spaces selected`}</Text>
 | 
			
		||||
          { !process && (
 | 
			
		||||
            <Button onClick={handleAdd} variant="primary">Pin</Button>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useVisibilityToggle() {
 | 
			
		||||
  const [isOpen, setIsOpen] = useState(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleOpen = () => setIsOpen(true);
 | 
			
		||||
    navigation.on(cons.events.navigation.SHORTCUT_SPACES_OPENED, handleOpen);
 | 
			
		||||
    return () => {
 | 
			
		||||
      navigation.removeListener(cons.events.navigation.SHORTCUT_SPACES_OPENED, handleOpen);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const requestClose = () => setIsOpen(false);
 | 
			
		||||
 | 
			
		||||
  return [isOpen, requestClose];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ShortcutSpaces() {
 | 
			
		||||
  const [isOpen, requestClose] = useVisibilityToggle();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog
 | 
			
		||||
      isOpen={isOpen}
 | 
			
		||||
      className="shortcut-spaces"
 | 
			
		||||
      title={(
 | 
			
		||||
        <Text variant="s1" weight="medium" primary>
 | 
			
		||||
          Pin spaces
 | 
			
		||||
        </Text>
 | 
			
		||||
      )}
 | 
			
		||||
      contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
 | 
			
		||||
      onRequestClose={requestClose}
 | 
			
		||||
    >
 | 
			
		||||
      {
 | 
			
		||||
        isOpen
 | 
			
		||||
          ? <ShortcutSpacesContent />
 | 
			
		||||
          : <div />
 | 
			
		||||
      }
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ShortcutSpaces;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,52 +0,0 @@
 | 
			
		|||
@use '../../partials/dir';
 | 
			
		||||
@use '../../partials/flex';
 | 
			
		||||
 | 
			
		||||
.shortcut-spaces {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  .dialog__content-container {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    padding-bottom: 80px;
 | 
			
		||||
    @include dir.side(padding, var(--sp-extra-tight), 0);
 | 
			
		||||
 | 
			
		||||
    & > .text-b1 {
 | 
			
		||||
      padding: 0 var(--sp-extra-tight);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  &__header {
 | 
			
		||||
    margin-top: var(--sp-extra-tight);
 | 
			
		||||
    padding: var(--sp-extra-tight);
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .room-selector {
 | 
			
		||||
    margin: 0 var(--sp-extra-tight);
 | 
			
		||||
  }
 | 
			
		||||
  .room-selector__options {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    .checkbox {
 | 
			
		||||
      margin: 0 6px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__footer {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding: var(--sp-normal);
 | 
			
		||||
    background-color: var(--bg-surface);
 | 
			
		||||
    border-top: 1px solid var(--bg-surface-border);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  
 | 
			
		||||
    & > .text {
 | 
			
		||||
      @extend .cp-fx__item-one;
 | 
			
		||||
      padding: 0 var(--sp-tight);
 | 
			
		||||
    }
 | 
			
		||||
  
 | 
			
		||||
    & > button {
 | 
			
		||||
      @include dir.side(margin, var(--sp-normal), 0);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue