diff --git a/package-lock.json b/package-lock.json index 1b65135..3dc4e73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,6 @@ "immer": "9.0.16", "is-hotkey": "0.2.0", "jotai": "1.12.0", - "katex": "0.16.4", "linkify-html": "4.0.2", "linkify-react": "4.1.1", "linkifyjs": "4.0.2", @@ -61,7 +60,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": { @@ -275,9 +273,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "engines": { "node": ">=6.9.0" } @@ -366,11 +364,11 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", - "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -505,9 +503,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.9.tgz", - "integrity": "sha512-kW5ccqWHVOOTGUkkJbtfoImtqu3kA1PFkivM+9QPFSHphPfPBlBalX9eDRqPK+wHCqKhU48/78T791qPgC9e9A==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.6.tgz", + "integrity": "sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g==", "cpu": [ "arm" ], @@ -520,9 +518,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.9.tgz", - "integrity": "sha512-ndIAZJUeLx4O+4AJbFQCurQW4VRUXjDsUvt1L+nP8bVELOWdmdCEOtlIweCUE6P+hU0uxYbEK2AEP0n5IVQvhg==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz", + "integrity": "sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg==", "cpu": [ "arm64" ], @@ -535,9 +533,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.9.tgz", - "integrity": "sha512-UbMcJB4EHrAVOnknQklREPgclNU2CPet2h+sCBCXmF2mfoYWopBn/CfTfeyOkb/JglOcdEADqAljFndMKnFtOw==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.6.tgz", + "integrity": "sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ==", "cpu": [ "x64" ], @@ -550,9 +548,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.9.tgz", - "integrity": "sha512-d7D7/nrt4CxPul98lx4PXhyNZwTYtbdaHhOSdXlZuu5zZIznjqtMqLac8Bv+IuT6SVHiHUwrkL6ywD7mOgLW+A==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.6.tgz", + "integrity": "sha512-bsDRvlbKMQMt6Wl08nHtFz++yoZHsyTOxnjfB2Q95gato+Yi4WnRl13oC2/PJJA9yLCoRv9gqT/EYX0/zDsyMA==", "cpu": [ "arm64" ], @@ -565,9 +563,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.9.tgz", - "integrity": "sha512-LZc+Wlz06AkJYtwWsBM3x2rSqTG8lntDuftsUNQ3fCx9ZttYtvlDcVtgb+NQ6t9s6K5No5zutN3pcjZEC2a4iQ==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz", + "integrity": "sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg==", "cpu": [ "x64" ], @@ -580,9 +578,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.9.tgz", - "integrity": "sha512-gIj0UQZlQo93CHYouHKkpzP7AuruSaMIm1etcWIxccFEVqCN1xDr6BWlN9bM+ol/f0W9w3hx3HDuEwcJVtGneQ==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz", + "integrity": "sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg==", "cpu": [ "arm64" ], @@ -595,9 +593,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.9.tgz", - "integrity": "sha512-GNors4vaMJ7lzGOuhzNc7jvgsQZqErGA8rsW+nck8N1nYu86CvsJW2seigVrQQWOV4QzEP8Zf3gm+QCjA2hnBQ==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz", + "integrity": "sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q==", "cpu": [ "x64" ], @@ -610,9 +608,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.9.tgz", - "integrity": "sha512-cNx1EF99c2t1Ztn0lk9N+MuwBijGF8mH6nx9GFsB3e0lpUpPkCE/yt5d+7NP9EwJf5uzqdjutgVYoH1SNqzudA==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz", + "integrity": "sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw==", "cpu": [ "arm" ], @@ -625,9 +623,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.9.tgz", - "integrity": "sha512-YPxQunReYp8RQ1FvexFrOEqqf+nLbS3bKVZF5FRT2uKM7Wio7BeATqAwO02AyrdSEntt3I5fhFsujUChIa8CZg==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz", + "integrity": "sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w==", "cpu": [ "arm64" ], @@ -640,9 +638,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.9.tgz", - "integrity": "sha512-zb12ixDIKNwFpIqR00J88FFitVwOEwO78EiUi8wi8FXlmSc3GtUuKV/BSO+730Kglt0B47+ZrJN1BhhOxZaVrw==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz", + "integrity": "sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ==", "cpu": [ "ia32" ], @@ -655,9 +653,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.9.tgz", - "integrity": "sha512-X8te4NLxtHiNT6H+4Pfm5RklzItA1Qy4nfyttihGGX+Koc53Ar20ViC+myY70QJ8PDEOehinXZj/F7QK3A+MKQ==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz", + "integrity": "sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ==", "cpu": [ "loong64" ], @@ -670,9 +668,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.9.tgz", - "integrity": "sha512-ZqyMDLt02c5smoS3enlF54ndK5zK4IpClLTxF0hHfzHJlfm4y8IAkIF8LUW0W7zxcKy7oAwI7BRDqeVvC120SA==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz", + "integrity": "sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA==", "cpu": [ "mips64el" ], @@ -685,9 +683,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.9.tgz", - "integrity": "sha512-k+ca5W5LDBEF3lfDwMV6YNXwm4wEpw9krMnNvvlNz3MrKSD2Eb2c861O0MaKrZkG/buTQAP4vkavbLwgIe6xjg==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz", + "integrity": "sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg==", "cpu": [ "ppc64" ], @@ -700,9 +698,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.9.tgz", - "integrity": "sha512-GuInVdogjmg9DhgkEmNipHkC+3tzkanPJzgzTC2ihsvrruLyFoR1YrTGixblNSMPudQLpiqkcwGwwe0oqfrvfA==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz", + "integrity": "sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ==", "cpu": [ "riscv64" ], @@ -715,9 +713,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.9.tgz", - "integrity": "sha512-49wQ0aYkvwXonGsxc7LuuLNICMX8XtO92Iqmug5Qau0kpnV6SP34jk+jIeu4suHwAbSbRhVFtDv75yRmyfQcHw==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz", + "integrity": "sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q==", "cpu": [ "s390x" ], @@ -730,9 +728,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.9.tgz", - "integrity": "sha512-Nx4oKEAJ6EcQlt4dK7qJyuZUoXZG7CAeY22R7rqZijFzwFfMOD+gLP56uV7RrV86jGf8PeRY8TBsRmOcZoG42w==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz", + "integrity": "sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw==", "cpu": [ "x64" ], @@ -745,9 +743,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.9.tgz", - "integrity": "sha512-d0WnpgJ+FTiMZXEQ1NOv9+0gvEhttbgKEvVqWWAtl1u9AvlspKXbodKHzQ5MLP6YV1y52Xp+p8FMYqj8ykTahg==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz", + "integrity": "sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A==", "cpu": [ "x64" ], @@ -760,9 +758,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.9.tgz", - "integrity": "sha512-jccK11278dvEscHFfMk5EIPjF4wv1qGD0vps7mBV1a6TspdR36O28fgPem/SA/0pcsCPHjww5ouCLwP+JNAFlw==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz", + "integrity": "sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw==", "cpu": [ "x64" ], @@ -775,9 +773,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.9.tgz", - "integrity": "sha512-OetwTSsv6mIDLqN7I7I2oX9MmHGwG+AP+wKIHvq+6sIHwcPPJqRx+DJB55jy9JG13CWcdcQno/7V5MTJ5a0xfQ==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz", + "integrity": "sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw==", "cpu": [ "x64" ], @@ -790,9 +788,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.9.tgz", - "integrity": "sha512-tKSSSK6unhxbGbHg+Cc+JhRzemkcsX0tPBvG0m5qsWbkShDK9c+/LSb13L18LWVdOQZwuA55Vbakxmt6OjBDOQ==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz", + "integrity": "sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg==", "cpu": [ "arm64" ], @@ -805,9 +803,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.9.tgz", - "integrity": "sha512-ZTQ5vhNS5gli0KK8I6/s6+LwXmNEfq1ftjnSVyyNm33dBw8zDpstqhGXYUbZSWWLvkqiRRjgxgmoncmi6Yy7Ng==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz", + "integrity": "sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg==", "cpu": [ "ia32" ], @@ -820,9 +818,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.9.tgz", - "integrity": "sha512-C4ZX+YFIp6+lPrru3tpH6Gaapy8IBRHw/e7l63fzGDhn/EaiGpQgbIlT5paByyy+oMvRFQoxxyvC4LE0AjJMqQ==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz", + "integrity": "sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA==", "cpu": [ "x64" ], @@ -2747,8 +2745,7 @@ "node_modules/@types/node": { "version": "18.11.18", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", - "dev": true + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, "node_modules/@types/prismjs": { "version": "1.26.0", @@ -3066,9 +3063,9 @@ } }, "node_modules/@vanilla-extract/babel-plugin-debug-ids": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.0.1.tgz", - "integrity": "sha512-ynyKqsJiMzM1/yiIJ6QdqpWKlK4IMJJWREpPtaemZrE1xG1B4E/Nfa6YazuDWjDkCJC1tRIpEGnVs+pMIjUxyw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.0.3.tgz", + "integrity": "sha512-vm4jYu1xhSa6ofQ9AhIpR3DkAp4c+eoR1Rpm8/TQI4DmWbmGbOjYRcqV0aWsfaIlNhN4kFuxFMKBNN9oG6iRzA==", "dependencies": { "@babel/core": "^7.20.7" } @@ -3164,22 +3161,126 @@ } }, "node_modules/@vanilla-extract/integration": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@vanilla-extract/integration/-/integration-6.0.2.tgz", - "integrity": "sha512-LwfXlh0THeNvVXdA3iWFYvJs1mvEP1PkfQD/7S6Purry7L8iDizDV/87FgWBJ79FnTmYIvMrc7BOQsUajNj9VQ==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@vanilla-extract/integration/-/integration-6.2.4.tgz", + "integrity": "sha512-+AfymNMVq9sEUe0OJpdCokmPZg4Zi6CqKaW/PnUOfDwEn53ighHOMOBl5hAgxYR8Kiz9NG43Bn00mkjWlFi+ng==", "dependencies": { "@babel/core": "^7.20.7", "@babel/plugin-syntax-typescript": "^7.20.0", - "@vanilla-extract/babel-plugin-debug-ids": "^1.0.1", - "@vanilla-extract/css": "^1.9.3", - "esbuild": "^0.16.3", - "eval": "0.1.6", + "@vanilla-extract/babel-plugin-debug-ids": "^1.0.2", + "@vanilla-extract/css": "^1.14.0", + "esbuild": "0.17.6", + "eval": "0.1.8", "find-up": "^5.0.0", "javascript-stringify": "^2.0.1", "lodash": "^4.17.21", + "mlly": "^1.1.0", + "outdent": "^0.8.0", + "vite": "^4.1.4", + "vite-node": "^0.28.5" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@vanilla-extract/css": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.14.0.tgz", + "integrity": "sha512-rYfm7JciWZ8PFzBM/HDiE2GLnKI3xJ6/vdmVJ5BSgcCZ5CxRlM9Cjqclni9lGzF3eMOijnUhCd/KV8TOzyzbMA==", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@vanilla-extract/private": "^1.0.3", + "chalk": "^4.1.1", + "css-what": "^6.1.0", + "cssesc": "^3.0.0", + "csstype": "^3.0.7", + "deep-object-diff": "^1.1.9", + "deepmerge": "^4.2.2", + "media-query-parser": "^2.0.2", + "modern-ahocorasick": "^1.0.0", "outdent": "^0.8.0" } }, + "node_modules/@vanilla-extract/integration/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@vanilla-extract/integration/node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@vanilla-extract/private": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.3.tgz", @@ -3233,10 +3334,9 @@ "optional": true }, "node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "dev": true, + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "bin": { "acorn": "bin/acorn" }, @@ -3608,6 +3708,19 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -4173,9 +4286,9 @@ } }, "node_modules/esbuild": { - "version": "0.16.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.9.tgz", - "integrity": "sha512-gkH83yHyijMSZcZFs1IWew342eMdFuWXmQo3zkDPTre25LIPBJsXryg02M3u8OpTwCJdBkdaQwqKkDLnAsAeLQ==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.6.tgz", + "integrity": "sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q==", "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -4184,28 +4297,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.16.9", - "@esbuild/android-arm64": "0.16.9", - "@esbuild/android-x64": "0.16.9", - "@esbuild/darwin-arm64": "0.16.9", - "@esbuild/darwin-x64": "0.16.9", - "@esbuild/freebsd-arm64": "0.16.9", - "@esbuild/freebsd-x64": "0.16.9", - "@esbuild/linux-arm": "0.16.9", - "@esbuild/linux-arm64": "0.16.9", - "@esbuild/linux-ia32": "0.16.9", - "@esbuild/linux-loong64": "0.16.9", - "@esbuild/linux-mips64el": "0.16.9", - "@esbuild/linux-ppc64": "0.16.9", - "@esbuild/linux-riscv64": "0.16.9", - "@esbuild/linux-s390x": "0.16.9", - "@esbuild/linux-x64": "0.16.9", - "@esbuild/netbsd-x64": "0.16.9", - "@esbuild/openbsd-x64": "0.16.9", - "@esbuild/sunos-x64": "0.16.9", - "@esbuild/win32-arm64": "0.16.9", - "@esbuild/win32-ia32": "0.16.9", - "@esbuild/win32-x64": "0.16.9" + "@esbuild/android-arm": "0.17.6", + "@esbuild/android-arm64": "0.17.6", + "@esbuild/android-x64": "0.17.6", + "@esbuild/darwin-arm64": "0.17.6", + "@esbuild/darwin-x64": "0.17.6", + "@esbuild/freebsd-arm64": "0.17.6", + "@esbuild/freebsd-x64": "0.17.6", + "@esbuild/linux-arm": "0.17.6", + "@esbuild/linux-arm64": "0.17.6", + "@esbuild/linux-ia32": "0.17.6", + "@esbuild/linux-loong64": "0.17.6", + "@esbuild/linux-mips64el": "0.17.6", + "@esbuild/linux-ppc64": "0.17.6", + "@esbuild/linux-riscv64": "0.17.6", + "@esbuild/linux-s390x": "0.17.6", + "@esbuild/linux-x64": "0.17.6", + "@esbuild/netbsd-x64": "0.17.6", + "@esbuild/openbsd-x64": "0.17.6", + "@esbuild/sunos-x64": "0.17.6", + "@esbuild/win32-arm64": "0.17.6", + "@esbuild/win32-ia32": "0.17.6", + "@esbuild/win32-x64": "0.17.6" } }, "node_modules/escalade": { @@ -4734,10 +4847,11 @@ } }, "node_modules/eval": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.6.tgz", - "integrity": "sha512-o0XUw+5OGkXw4pJZzQoXUk+H87DHuC+7ZE//oSrRGtatTmr12oTnLfg6QOq9DyTt0c/p4TwzgmkKrBzWTSizyQ==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", + "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", "dependencies": { + "@types/node": "*", "require-like": ">= 0.1.1" }, "engines": { @@ -4988,27 +5102,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", @@ -5043,7 +5136,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -5227,7 +5319,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", @@ -5870,16 +5963,10 @@ "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/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, "node_modules/jsx-ast-utils": { "version": "3.3.3", @@ -5894,29 +5981,6 @@ "node": ">=4.0" } }, - "node_modules/katex": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.4.tgz", - "integrity": "sha512-WudRKUj8yyBeVDI4aYMNxhx5Vhh2PjpzQw1GRu/LVGqL4m1AxwD1GcUp0IMbdJaf5zsjtj8ghP0DOQRYhroNkw==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "dependencies": { - "commander": "^8.0.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/katex/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/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -5960,9 +6024,9 @@ } }, "node_modules/lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "engines": { "node": ">=10" } @@ -6257,6 +6321,22 @@ "node": ">=10" } }, + "node_modules/mlly": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", + "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==", + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.0.3", + "ufo": "^1.3.2" + } + }, + "node_modules/modern-ahocorasick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.0.1.tgz", + "integrity": "sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6600,6 +6680,11 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" + }, "node_modules/pdfjs-dist": { "version": "3.10.111", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.10.111.tgz", @@ -6629,6 +6714,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, "node_modules/postcss": { "version": "8.4.24", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", @@ -7128,7 +7223,6 @@ "version": "3.25.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz", "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==", - "dev": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -7418,6 +7512,14 @@ "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.1.8.tgz", "integrity": "sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ==" }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -7426,6 +7528,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -7686,22 +7797,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", @@ -7757,6 +7852,11 @@ "node": "*" } }, + "node_modules/ufo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", + "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==" + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -7777,14 +7877,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.10", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", @@ -7837,7 +7929,6 @@ "version": "4.3.9", "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", - "dev": true, "dependencies": { "esbuild": "^0.17.5", "postcss": "^8.4.23", @@ -7881,6 +7972,30 @@ } } }, + "node_modules/vite-node": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.28.5.tgz", + "integrity": "sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.1.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "source-map": "^0.6.1", + "source-map-support": "^0.5.21", + "vite": "^3.0.0 || ^4.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.16.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/vite-plugin-static-copy": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-0.13.0.tgz", @@ -7941,7 +8056,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -7957,7 +8071,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -7973,7 +8086,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -7989,7 +8101,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -8005,7 +8116,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -8021,7 +8131,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -8037,7 +8146,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -8053,7 +8161,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -8069,7 +8176,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -8085,7 +8191,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -8101,7 +8206,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -8117,7 +8221,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -8133,7 +8236,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -8149,7 +8251,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -8165,7 +8266,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -8181,7 +8281,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -8197,7 +8296,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -8213,7 +8311,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -8229,7 +8326,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -8245,7 +8341,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -8261,7 +8356,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -8277,7 +8371,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -8290,7 +8383,6 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" diff --git a/package.json b/package.json index c4d0170..4ae4f9c 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "immer": "9.0.16", "is-hotkey": "0.2.0", "jotai": "1.12.0", - "katex": "0.16.4", "linkify-html": "4.0.2", "linkify-react": "4.1.1", "linkifyjs": "4.0.2", @@ -71,7 +70,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": { diff --git a/src/app/atoms/divider/Divider.jsx b/src/app/atoms/divider/Divider.jsx deleted file mode 100644 index 7672124..0000000 --- a/src/app/atoms/divider/Divider.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './Divider.scss'; - -import Text from '../text/Text'; - -function Divider({ text, variant, align }) { - const dividerClass = ` divider--${variant} divider--${align}`; - return ( -
- {text !== null && {text}} -
- ); -} - -Divider.defaultProps = { - text: null, - variant: 'surface', - align: 'center', -}; - -Divider.propTypes = { - text: PropTypes.string, - variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']), - align: PropTypes.oneOf(['left', 'center', 'right']), -}; - -export default Divider; diff --git a/src/app/atoms/math/Math.jsx b/src/app/atoms/math/Math.jsx deleted file mode 100644 index ab52a47..0000000 --- a/src/app/atoms/math/Math.jsx +++ /dev/null @@ -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 ; -}); -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; diff --git a/src/app/components/editor/Editor.preview.tsx b/src/app/components/editor/Editor.preview.tsx deleted file mode 100644 index ad67dc1..0000000 --- a/src/app/components/editor/Editor.preview.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { useState } from 'react'; -import FocusTrap from 'focus-trap-react'; -import { - config, - Icon, - IconButton, - Icons, - Line, - Modal, - Overlay, - OverlayBackdrop, - OverlayCenter, -} from 'folds'; - -import { CustomEditor, useEditor } from './Editor'; -import { Toolbar } from './Toolbar'; - -export function EditorPreview() { - const [open, setOpen] = useState(false); - const editor = useEditor(); - const [toolbar, setToolbar] = useState(false); - - return ( - <> - setOpen(!open)}> - - - }> - - setOpen(false), - clickOutsideDeactivates: true, - }} - > - -
- - - - } - after={ - <> - setToolbar(!toolbar)} - aria-pressed={toolbar} - > - - - - - - - - - - } - bottom={ - toolbar && ( -
- - -
- ) - } - /> -
-
-
-
-
- - ); -} diff --git a/src/app/components/sidebar/Sidebar.css.ts b/src/app/components/sidebar/Sidebar.css.ts deleted file mode 100644 index e62ed6f..0000000 --- a/src/app/components/sidebar/Sidebar.css.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { style } from '@vanilla-extract/css'; -import { recipe, RecipeVariants } from '@vanilla-extract/recipes'; -import { color, config, DefaultReset, toRem } from 'folds'; - -export const Sidebar = style([ - DefaultReset, - { - width: toRem(66), - backgroundColor: color.Background.Container, - borderRight: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`, - - display: 'flex', - flexDirection: 'column', - color: color.Background.OnContainer, - }, -]); - -export const SidebarStack = style([ - DefaultReset, - { - width: '100%', - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - gap: config.space.S300, - padding: `${config.space.S300} 0`, - }, -]); - -const PUSH_X = 2; -export const SidebarAvatarBox = recipe({ - base: [ - DefaultReset, - { - display: 'flex', - alignItems: 'center', - position: 'relative', - transition: 'transform 200ms cubic-bezier(0, 0.8, 0.67, 0.97)', - - selectors: { - '&:hover': { - transform: `translateX(${toRem(PUSH_X)})`, - }, - '&::before': { - content: '', - display: 'none', - position: 'absolute', - left: toRem(-11.5 - PUSH_X), - width: toRem(3 + PUSH_X), - height: toRem(16), - borderRadius: `0 ${toRem(4)} ${toRem(4)} 0`, - background: 'CurrentColor', - transition: 'height 200ms linear', - }, - '&:hover::before': { - display: 'block', - width: toRem(3), - }, - }, - }, - ], - variants: { - active: { - true: { - selectors: { - '&::before': { - display: 'block', - height: toRem(24), - }, - '&:hover::before': { - width: toRem(3 + PUSH_X), - }, - }, - }, - }, - }, -}); - -export type SidebarAvatarBoxVariants = RecipeVariants; - -export const SidebarBadgeBox = recipe({ - base: [ - DefaultReset, - { - position: 'absolute', - zIndex: 1, - }, - ], - variants: { - hasCount: { - true: { - top: toRem(-6), - right: toRem(-6), - }, - false: { - top: toRem(-2), - right: toRem(-2), - }, - }, - }, - defaultVariants: { - hasCount: false, - }, -}); - -export type SidebarBadgeBoxVariants = RecipeVariants; - -export const SidebarBadgeOutline = style({ - boxShadow: `0 0 0 ${config.borderWidth.B500} ${color.Background.Container}`, -}); diff --git a/src/app/components/sidebar/Sidebar.tsx b/src/app/components/sidebar/Sidebar.tsx deleted file mode 100644 index 7caf1b2..0000000 --- a/src/app/components/sidebar/Sidebar.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import classNames from 'classnames'; -import { as } from 'folds'; -import React from 'react'; -import * as css from './Sidebar.css'; - -export const Sidebar = as<'div'>(({ as: AsSidebar = 'div', className, ...props }, ref) => ( - -)); diff --git a/src/app/components/sidebar/SidebarAvatar.tsx b/src/app/components/sidebar/SidebarAvatar.tsx deleted file mode 100644 index 86665f7..0000000 --- a/src/app/components/sidebar/SidebarAvatar.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import classNames from 'classnames'; -import { as, Avatar, Box, color, config, Text, Tooltip, TooltipProvider } from 'folds'; -import React, { forwardRef, MouseEventHandler, ReactNode } from 'react'; -import * as css from './Sidebar.css'; - -const SidebarAvatarBox = as<'div', css.SidebarAvatarBoxVariants>( - ({ as: AsSidebarAvatarBox = 'div', className, active, ...props }, ref) => ( - - ) -); - -export const SidebarAvatar = forwardRef< - HTMLDivElement, - css.SidebarAvatarBoxVariants & - css.SidebarBadgeBoxVariants & { - outlined?: boolean; - avatarChildren: ReactNode; - tooltip: ReactNode | string; - notificationBadge?: (badgeClassName: string) => ReactNode; - onClick?: MouseEventHandler; - onContextMenu?: MouseEventHandler; - } ->( - ( - { - active, - hasCount, - outlined, - avatarChildren, - tooltip, - notificationBadge, - onClick, - onContextMenu, - }, - ref - ) => ( - - - {tooltip} - - } - > - {(avRef) => ( - - {avatarChildren} - - )} - - {notificationBadge && ( - - {notificationBadge(css.SidebarBadgeOutline)} - - )} - - ) -); diff --git a/src/app/components/sidebar/SidebarContent.tsx b/src/app/components/sidebar/SidebarContent.tsx deleted file mode 100644 index 4f40587..0000000 --- a/src/app/components/sidebar/SidebarContent.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React, { ReactNode } from 'react'; -import { Box, Scroll } from 'folds'; - -type SidebarContentProps = { - scrollable: ReactNode; - sticky: ReactNode; -}; -export function SidebarContent({ scrollable, sticky }: SidebarContentProps) { - return ( - <> - - - {scrollable} - - - - {sticky} - - - ); -} diff --git a/src/app/components/sidebar/SidebarStack.tsx b/src/app/components/sidebar/SidebarStack.tsx deleted file mode 100644 index c0e976c..0000000 --- a/src/app/components/sidebar/SidebarStack.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import { as } from 'folds'; -import * as css from './Sidebar.css'; - -export const SidebarStack = as<'div'>( - ({ as: AsSidebarStack = 'div', className, ...props }, ref) => ( - - ) -); diff --git a/src/app/components/sidebar/SidebarStackSeparator.tsx b/src/app/components/sidebar/SidebarStackSeparator.tsx deleted file mode 100644 index 110341c..0000000 --- a/src/app/components/sidebar/SidebarStackSeparator.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import { Line, toRem } from 'folds'; - -export function SidebarStackSeparator() { - return ( - - ); -} diff --git a/src/app/components/sidebar/index.ts b/src/app/components/sidebar/index.ts deleted file mode 100644 index f744628..0000000 --- a/src/app/components/sidebar/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './Sidebar'; -export * from './SidebarAvatar'; -export * from './SidebarContent'; -export * from './SidebarStack'; -export * from './SidebarStackSeparator'; diff --git a/src/app/hooks/useForceUpdate.ts b/src/app/hooks/useForceUpdate.ts deleted file mode 100644 index 0691aa9..0000000 --- a/src/app/hooks/useForceUpdate.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useReducer } from 'react'; - -const reducer = (prevCount: number): number => prevCount + 1; - -export const useForceUpdate = (): [number, () => void] => { - const [state, dispatch] = useReducer(reducer, 0); - - return [state, dispatch]; -}; diff --git a/src/app/hooks/useStateEvents.ts b/src/app/hooks/useStateEvents.ts deleted file mode 100644 index dd08569..0000000 --- a/src/app/hooks/useStateEvents.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { Room } from 'matrix-js-sdk'; -import { StateEvent } from '../../types/matrix/room'; -import { useForceUpdate } from './useForceUpdate'; -import { useStateEventCallback } from './useStateEventCallback'; -import { getStateEvents } from '../utils/room'; - -export const useStateEvents = (room: Room, eventType: StateEvent) => { - const [updateCount, forceUpdate] = useForceUpdate(); - - useStateEventCallback( - room.client, - useCallback( - (event) => { - if (event.getRoomId() === room.roomId && event.getType() === eventType) { - forceUpdate(); - } - }, - [room, eventType, forceUpdate] - ) - ); - - return useMemo( - () => getStateEvents(room, eventType), - // eslint-disable-next-line react-hooks/exhaustive-deps - [room, eventType, updateCount] - ); -}; diff --git a/src/app/molecules/following-members/FollowingMembers.jsx b/src/app/molecules/following-members/FollowingMembers.jsx deleted file mode 100644 index 949dac7..0000000 --- a/src/app/molecules/following-members/FollowingMembers.jsx +++ /dev/null @@ -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 && ( - - ) - ); -} - -FollowingMembers.propTypes = { - roomTimeline: PropTypes.shape({}).isRequired, -}; - -export default FollowingMembers; diff --git a/src/app/molecules/message/TimelineChange.jsx b/src/app/molecules/message/TimelineChange.jsx deleted file mode 100644 index bc6e913..0000000 --- a/src/app/molecules/message/TimelineChange.jsx +++ /dev/null @@ -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 ( - - ); -} - -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; diff --git a/src/app/molecules/room-intro/RoomIntro.jsx b/src/app/molecules/room-intro/RoomIntro.jsx deleted file mode 100644 index 2ec46cb..0000000 --- a/src/app/molecules/room-intro/RoomIntro.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './RoomIntro.scss'; - -import colorMXID from '../../../util/colorMXID'; - -import Text from '../../atoms/text/Text'; -import Avatar from '../../atoms/avatar/Avatar'; - -function RoomIntro({ - roomId, avatarSrc, name, heading, desc, time, -}) { - return ( -
- -
- {heading} - {desc} - { time !== null && {time}} -
-
- ); -} - -RoomIntro.defaultProps = { - avatarSrc: null, - time: null, -}; - -RoomIntro.propTypes = { - roomId: PropTypes.string.isRequired, - avatarSrc: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.bool, - ]), - name: PropTypes.string.isRequired, - heading: PropTypes.node.isRequired, - desc: PropTypes.node.isRequired, - time: PropTypes.node, -}; - -export default RoomIntro; diff --git a/src/app/organisms/emoji-board/EmojiBoard.jsx b/src/app/organisms/emoji-board/EmojiBoard.jsx deleted file mode 100644 index 84c4130..0000000 --- a/src/app/organisms/emoji-board/EmojiBoard.jsx +++ /dev/null @@ -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( - - {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 - {emoji.shortcode} - )} - - ); - } - emojiBoard.push( -
- {emojiRow} -
- ); - } - return emojiBoard; - } - - return ( -
- - {name} - - {groupEmojis.length !== 0 &&
{getEmojiBoard()}
} -
- ); -}); - -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 ( - - ); -} - -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 ( -
- -
- {recentEmojis.length > 0 && ( - openGroup(0)} - src={RecentClockIC} - tooltip="Recent" - tooltipPlacement="left" - /> - )} -
- {availableEmojis.map((pack) => { - const src = initMatrix.matrixClient.mxcUrlToHttp( - pack.avatarUrl ?? pack.getEmojis()[0].mxc - ); - return ( - openGroup(recentOffset + pack.packIndex)} - src={src} - key={pack.packIndex} - tooltip={pack.displayName ?? 'Unknown'} - tooltipPlacement="left" - isImage - /> - ); - })} -
-
- {[ - [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]) => ( - openGroup(recentOffset + availableEmojis.length + indx)} - key={indx} - src={ico} - tooltip={name} - tooltipPlacement="left" - /> - ))} -
-
-
-
-
- - -
-
- -
- - {recentEmojis.length > 0 && ( - - )} - {availableEmojis.map((pack) => ( - - ))} - {emojiGroups.map((group) => ( - - ))} -
-
-
-
-
{parse(twemoji.parse('🙂', { base: TWEMOJI_BASE_URL }))}
- :slight_smile: -
-
-
- ); -} - -EmojiBoard.propTypes = { - onSelect: PropTypes.func.isRequired, - searchRef: PropTypes.shape({}).isRequired, -}; - -export default EmojiBoard; diff --git a/src/app/organisms/emoji-board/EmojiBoardOpener.jsx b/src/app/organisms/emoji-board/EmojiBoardOpener.jsx deleted file mode 100644 index 32b7a83..0000000 --- a/src/app/organisms/emoji-board/EmojiBoardOpener.jsx +++ /dev/null @@ -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 ( - - )} - afterToggle={afterEmojiBoardToggle} - render={(toggleMenu) => ( - - )} - /> - ); -} - -export default EmojiBoardOpener; diff --git a/src/app/organisms/emoji-board/recent.js b/src/app/organisms/emoji-board/recent.js deleted file mode 100644 index dff67fb..0000000 --- a/src/app/organisms/emoji-board/recent.js +++ /dev/null @@ -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), - }); -} diff --git a/src/app/organisms/navigation/Sidebar1.tsx b/src/app/organisms/navigation/Sidebar1.tsx deleted file mode 100644 index d9ee466..0000000 --- a/src/app/organisms/navigation/Sidebar1.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from 'react'; -import { Icon, Icons, Badge, AvatarFallback, Text } from 'folds'; -import { useAtom } from 'jotai'; - -import { - Sidebar, - SidebarContent, - SidebarStackSeparator, - SidebarStack, - SidebarAvatar, -} from '../../components/sidebar'; -import { selectedTabAtom, SidebarTab } from '../../state/selectedTab'; - -export function Sidebar1() { - const [selectedTab, setSelectedTab] = useAtom(selectedTabAtom); - - return ( - - - - } - onClick={() => setSelectedTab(SidebarTab.Home)} - /> - } - onClick={() => setSelectedTab(SidebarTab.People)} - /> - - - - ( - - )} - avatarChildren={ - - B - - } - /> - ( - - 64 - - )} - avatarChildren={ - - C - - } - /> - - - - } - /> - } - /> - - - } - sticky={ - <> - - - } - /> - - A - - } - /> - - - } - /> - - ); -} diff --git a/src/app/organisms/room/EventLimit.js b/src/app/organisms/room/EventLimit.js deleted file mode 100644 index de87da3..0000000 --- a/src/app/organisms/room/EventLimit.js +++ /dev/null @@ -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; diff --git a/src/app/organisms/room/PeopleDrawer.jsx b/src/app/organisms/room/PeopleDrawer.jsx deleted file mode 100644 index 8f98324..0000000 --- a/src/app/organisms/room/PeopleDrawer.jsx +++ /dev/null @@ -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 ( -
-
- - - People - {`${room.getJoinedMemberCount()} members`} - - - openInviteUser(roomId)} tooltip="Invite" src={AddUserIC} disabled={!canInvite} /> -
-
-
- -
- { - 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) => ( - openProfileViewer(member.userId, roomId)} - avatarSrc={member.avatarSrc} - name={member.name} - color={colorMXID(member.userId)} - peopleRole={member.peopleRole} - /> - )) - } - { - (searchedMembers?.data.length === 0 || memberList.length === 0) - && ( -
- No results found! -
- ) - } -
- { - mList.length !== 0 - && memberList.length > itemCount - && searchedMembers === null - && ( - - ) - } -
-
-
-
-
-
e.preventDefault()} className="people-search"> - - - { - searchedMembers !== null - && - } - -
-
-
- ); -} - -PeopleDrawer.propTypes = { - roomId: PropTypes.string.isRequired, -}; - -export default PeopleDrawer; diff --git a/src/app/organisms/room/RoomViewCmdBar.jsx b/src/app/organisms/room/RoomViewCmdBar.jsx deleted file mode 100644 index 0d21123..0000000 --- a/src/app/organisms/room/RoomViewCmdBar.jsx +++ /dev/null @@ -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 ( - - ); -} -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) => ( - { - fireCmd({ - prefix: cmdPrefix, - option, - result: commands[cmd], - }); - }} - > - {`${cmd}${cmd.isOptions ? cmdOptString : ''}`} - - )); - } - - 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 ( - {`:${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) => ( - - fireCmd({ - prefix: emPrefix, - result: emoji, - }) - } - > - {renderEmoji(emoji)} - {`:${emoji.shortcode}:`} - - )); - } - - function renderNameSuggestion(namePrefix, members) { - return members.map((member) => ( - { - fireCmd({ - prefix: namePrefix, - result: member, - }); - }} - > - {twemojify(member.name)} - - )); - } - - 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 ( -
- -
- ); - } - - return ( -
-
- TAB -
-
- -
{renderSuggestions(cmd, fireCmd)}
-
-
-
- ); -} -RoomViewCmdBar.propTypes = { - roomId: PropTypes.string.isRequired, - roomTimeline: PropTypes.shape({}).isRequired, - viewEvent: PropTypes.shape({}).isRequired, -}; - -export default RoomViewCmdBar; diff --git a/src/app/organisms/room/RoomViewCmdBar.scss b/src/app/organisms/room/RoomViewCmdBar.scss deleted file mode 100644 index 3f03fb0..0000000 --- a/src/app/organisms/room/RoomViewCmdBar.scss +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/src/app/organisms/room/RoomViewContent.jsx b/src/app/organisms/room/RoomViewContent.jsx deleted file mode 100644 index 5726fe1..0000000 --- a/src/app/organisms/room/RoomViewContent.jsx +++ /dev/null @@ -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(); - } - return pl; - }; - - return ( - - {genPlaceholders()} - - ); -} - -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 @ - {nameJsx} - {'. '} - {topic} - - ) - : ( - <> - {'This is the beginning of the '} - {nameJsx} - {' 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 ( - - ); -} - -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
; - return ( - - ); - } - return ( - - ); -} - -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( - , - ); - itemCountIndex += 1; - // eslint-disable-next-line no-continue - continue; - } else { - tl.push(); - itemCountIndex += 1; - } - } - - let isNewEvent = false; - if (!unreadDivider) { - unreadDivider = (readUptoEvent - && prevMEvent?.getTs() <= readUptoEvent.getTs() - && readUptoEvent.getTs() < mEvent.getTs()); - if (unreadDivider) { - isNewEvent = true; - tl.push(); - itemCountIndex += 1; - if (jumpToItemIndex === -1) jumpToItemIndex = itemCountIndex; - } - } - const dayDivider = prevMEvent && !isInSameDay(mEvent.getDate(), prevMEvent.getDate()); - if (dayDivider) { - tl.push(); - 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 ( - -
-
- { roomTimeline.initialized ? renderTimeline() : loadingMsgPlaceholders('loading', 3) } -
-
-
- ); -} - -RoomViewContent.defaultProps = { - eventId: null, -}; -RoomViewContent.propTypes = { - eventId: PropTypes.string, - roomTimeline: PropTypes.shape({}).isRequired, - roomInputRef: PropTypes.shape({ - current: PropTypes.shape({}) - }).isRequired -}; - -export default RoomViewContent; diff --git a/src/app/organisms/room/RoomViewFloating.jsx b/src/app/organisms/room/RoomViewFloating.jsx deleted file mode 100644 index d027aff..0000000 --- a/src/app/organisms/room/RoomViewFloating.jsx +++ /dev/null @@ -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 ( - <> -
- - -
-
0 ? ' room-view__typing--open' : ''}`}> -
- {getUsersActionJsx(roomId, [...typingMembers], 'typing...')} -
-
- -
- - ); -} -RoomViewFloating.propTypes = { - roomId: PropTypes.string.isRequired, - roomTimeline: PropTypes.shape({}).isRequired, -}; - -export default RoomViewFloating; diff --git a/src/app/organisms/room/RoomViewInput.jsx b/src/app/organisms/room/RoomViewInput.jsx deleted file mode 100644 index 3fb780a..0000000 --- a/src/app/organisms/room/RoomViewInput.jsx +++ /dev/null @@ -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 ( - - { - tombstoneEvent - ? tombstoneEvent.getContent()?.body ?? 'This room has been replaced and is no longer active.' - : 'You do not have permission to post to this room' - } - - ); - } - return ( - <> -
- - -
-
- {roomTimeline.isEncrypted() && } - - - - - -
-
- { - openReusableContextMenu( - 'top', - (() => { - const cords = getEventCords(e); - cords.y -= 20; - return cords; - })(), - (closeMenu) => ( - { - handleSendSticker(data); - closeMenu(); - }} - /> - ), - ); - }} - tooltip="Sticker" - src={StickerIC} - /> - { - const cords = getEventCords(e); - cords.x += (document.dir === 'rtl' ? -80 : 80); - cords.y -= 250; - openEmojiBoard(cords, addEmoji); - }} - tooltip="Emoji" - src={EmojiIC} - /> - -
- - ); - } - - function attachFile() { - const fileType = attachment.type.slice(0, attachment.type.indexOf('/')); - return ( -
-
- {fileType === 'image' && {attachment.name}} - {fileType === 'video' && } - {fileType === 'audio' && } - {fileType !== 'image' && fileType !== 'video' && fileType !== 'audio' && } -
-
- {attachment.name} - {`size: ${bytesToSize(attachment.size)}`} -
-
- ); - } - - function attachReply() { - return ( -
- { - roomsInput.cancelReplyTo(roomId); - setReplyTo(null); - }} - src={CrossIC} - tooltip="Cancel reply" - size="extra-small" - /> - -
- ); - } - - return ( - <> - { replyTo !== null && attachReply()} - { attachment !== null && attachFile() } -
{ e.preventDefault(); }}> - { - renderInputs() - } -
- - ); -} -RoomViewInput.propTypes = { - roomId: PropTypes.string.isRequired, - roomTimeline: PropTypes.shape({}).isRequired, - viewEvent: PropTypes.shape({}).isRequired, -}; - -export default RoomViewInput; diff --git a/src/app/organisms/room/TimelineScroll.js b/src/app/organisms/room/TimelineScroll.js deleted file mode 100644 index ccdc9a9..0000000 --- a/src/app/organisms/room/TimelineScroll.js +++ /dev/null @@ -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; diff --git a/src/app/organisms/room/commands.jsx b/src/app/organisms/room/commands.jsx deleted file mode 100644 index 463f9d9..0000000 --- a/src/app/organisms/room/commands.jsx +++ /dev/null @@ -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( - Commands, - () => ( -
- {Object.keys(commands).map((cmdName) => ( - {commands[cmdName].description}} - /> - ))} -
- ), - ); -} - -export default commands; diff --git a/src/app/organisms/room/common.jsx b/src/app/organisms/room/common.jsx deleted file mode 100644 index 28974a8..0000000 --- a/src/app/organisms/room/common.jsx +++ /dev/null @@ -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 ( - <> - {twemojify(user)} - {' joined the room'} - - ); - }, - leave(user, reason) { - const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : ''; - return ( - <> - {twemojify(user)} - {' left the room'} - {twemojify(reasonMsg)} - - ); - }, - invite(inviter, user) { - return ( - <> - {twemojify(inviter)} - {' invited '} - {twemojify(user)} - - ); - }, - cancelInvite(inviter, user) { - return ( - <> - {twemojify(inviter)} - {' canceled '} - {twemojify(user)} - {'\'s invite'} - - ); - }, - rejectInvite(user) { - return ( - <> - {twemojify(user)} - {' rejected the invitation'} - - ); - }, - kick(actor, user, reason) { - const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : ''; - return ( - <> - {twemojify(actor)} - {' kicked '} - {twemojify(user)} - {twemojify(reasonMsg)} - - ); - }, - ban(actor, user, reason) { - const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : ''; - return ( - <> - {twemojify(actor)} - {' banned '} - {twemojify(user)} - {twemojify(reasonMsg)} - - ); - }, - unban(actor, user) { - return ( - <> - {twemojify(actor)} - {' unbanned '} - {twemojify(user)} - - ); - }, - avatarSets(user) { - return ( - <> - {twemojify(user)} - {' set a avatar'} - - ); - }, - avatarChanged(user) { - return ( - <> - {twemojify(user)} - {' changed their avatar'} - - ); - }, - avatarRemoved(user) { - return ( - <> - {twemojify(user)} - {' removed their avatar'} - - ); - }, - nameSets(user, newName) { - return ( - <> - {twemojify(user)} - {' set display name to '} - {twemojify(newName)} - - ); - }, - nameChanged(user, newName) { - return ( - <> - {twemojify(user)} - {' changed their display name to '} - {twemojify(newName)} - - ); - }, - nameRemoved(user, lastName) { - return ( - <> - {twemojify(user)} - {' removed their display name '} - {twemojify(lastName)} - - ); - }, - }; -} - -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) => {twemojify(getUserDisplayName(userId))}; - 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, -}; diff --git a/src/app/organisms/sticker-board/StickerBoard.jsx b/src/app/organisms/sticker-board/StickerBoard.jsx deleted file mode 100644 index 91e2591..0000000 --- a/src/app/organisms/sticker-board/StickerBoard.jsx +++ /dev/null @@ -1,115 +0,0 @@ -/* eslint-disable jsx-a11y/click-events-have-key-events */ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -import React, { useRef } from 'react'; -import PropTypes from 'prop-types'; -import './StickerBoard.scss'; - -import initMatrix from '../../../client/initMatrix'; -import { getRelevantPacks } from '../emoji-board/custom-emoji'; - -import Text from '../../atoms/text/Text'; -import ScrollView from '../../atoms/scroll/ScrollView'; -import IconButton from '../../atoms/button/IconButton'; - -function StickerBoard({ roomId, onSelect }) { - const mx = initMatrix.matrixClient; - const room = mx.getRoom(roomId); - const scrollRef = useRef(null); - - const parentIds = initMatrix.roomList.getAllParentSpaces(room.roomId); - const parentRooms = [...parentIds].map((id) => mx.getRoom(id)); - - const packs = getRelevantPacks( - mx, - [room, ...parentRooms], - ).filter((pack) => pack.getStickers().length !== 0); - - function isTargetNotSticker(target) { - return target.classList.contains('sticker-board__sticker') === false; - } - function getStickerData(target) { - const mxc = target.getAttribute('data-mx-sticker'); - const body = target.getAttribute('title'); - const httpUrl = target.getAttribute('src'); - return { mxc, body, httpUrl }; - } - const handleOnSelect = (e) => { - if (isTargetNotSticker(e.target)) return; - - const stickerData = getStickerData(e.target); - onSelect(stickerData); - }; - - const openGroup = (groupIndex) => { - const scrollContent = scrollRef.current.firstElementChild; - scrollContent.children[groupIndex].scrollIntoView(); - }; - - const renderPack = (pack) => ( -
- {pack.displayName ?? 'Unknown'} -
- {pack.getStickers().map((sticker) => ( - {sticker.shortcode} - ))} -
-
- ); - - return ( -
- {packs.length > 0 && ( - -
- {packs.map((pack, index) => { - const src = mx.mxcUrlToHttp(pack.avatarUrl ?? pack.getStickers()[0].mxc); - return ( - openGroup(index)} - src={src} - tooltip={pack.displayName || 'Unknown'} - tooltipPlacement="left" - isImage - /> - ); - })} -
-
- )} -
- -
- { - packs.length > 0 - ? packs.map(renderPack) - : ( -
- There is no sticker pack. -
- ) - } -
-
-
-
-
- ); -} -StickerBoard.propTypes = { - roomId: PropTypes.string.isRequired, - onSelect: PropTypes.func.isRequired, -}; - -export default StickerBoard; diff --git a/src/app/state/hooks/inviteList.ts b/src/app/state/hooks/inviteList.ts deleted file mode 100644 index f8b7e05..0000000 --- a/src/app/state/hooks/inviteList.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useAtomValue, WritableAtom } from 'jotai'; -import { selectAtom } from 'jotai/utils'; -import { MatrixClient } from 'matrix-js-sdk'; -import { useCallback } from 'react'; -import { isDirectInvite, isRoom, isSpace, isUnsupportedRoom } from '../../utils/room'; -import { compareRoomsEqual, RoomsAction } from '../utils'; -import { MDirectAction } from '../mDirectList'; - -export const useSpaceInvites = ( - mx: MatrixClient, - allInvitesAtom: WritableAtom -) => { - const selector = useCallback( - (rooms: string[]) => rooms.filter((roomId) => isSpace(mx.getRoom(roomId))), - [mx] - ); - return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual)); -}; - -export const useRoomInvites = ( - mx: MatrixClient, - allInvitesAtom: WritableAtom, - mDirectAtom: WritableAtom, MDirectAction> -) => { - const mDirects = useAtomValue(mDirectAtom); - const selector = useCallback( - (rooms: string[]) => - rooms.filter( - (roomId) => - isRoom(mx.getRoom(roomId)) && - !(mDirects.has(roomId) || isDirectInvite(mx.getRoom(roomId), mx.getUserId())) - ), - [mx, mDirects] - ); - return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual)); -}; - -export const useDirectInvites = ( - mx: MatrixClient, - allInvitesAtom: WritableAtom, - mDirectAtom: WritableAtom, MDirectAction> -) => { - const mDirects = useAtomValue(mDirectAtom); - const selector = useCallback( - (rooms: string[]) => - rooms.filter( - (roomId) => mDirects.has(roomId) || isDirectInvite(mx.getRoom(roomId), mx.getUserId()) - ), - [mx, mDirects] - ); - return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual)); -}; - -export const useUnsupportedInvites = ( - mx: MatrixClient, - allInvitesAtom: WritableAtom -) => { - const selector = useCallback( - (rooms: string[]) => rooms.filter((roomId) => isUnsupportedRoom(mx.getRoom(roomId))), - [mx] - ); - return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual)); -}; diff --git a/src/app/state/hooks/roomList.ts b/src/app/state/hooks/roomList.ts deleted file mode 100644 index 5d0890b..0000000 --- a/src/app/state/hooks/roomList.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useAtomValue, WritableAtom } from 'jotai'; -import { selectAtom } from 'jotai/utils'; -import { MatrixClient } from 'matrix-js-sdk'; -import { useCallback } from 'react'; -import { isRoom, isSpace, isUnsupportedRoom } from '../../utils/room'; -import { compareRoomsEqual, RoomsAction } from '../utils'; -import { MDirectAction } from '../mDirectList'; - -export const useSpaces = (mx: MatrixClient, allRoomsAtom: WritableAtom) => { - const selector = useCallback( - (rooms: string[]) => rooms.filter((roomId) => isSpace(mx.getRoom(roomId))), - [mx] - ); - return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual)); -}; - -export const useRooms = ( - mx: MatrixClient, - allRoomsAtom: WritableAtom, - mDirectAtom: WritableAtom, MDirectAction> -) => { - const mDirects = useAtomValue(mDirectAtom); - const selector = useCallback( - (rooms: string[]) => - rooms.filter((roomId) => isRoom(mx.getRoom(roomId)) && !mDirects.has(roomId)), - [mx, mDirects] - ); - return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual)); -}; - -export const useDirects = ( - mx: MatrixClient, - allRoomsAtom: WritableAtom, - mDirectAtom: WritableAtom, MDirectAction> -) => { - const mDirects = useAtomValue(mDirectAtom); - const selector = useCallback( - (rooms: string[]) => - rooms.filter((roomId) => isRoom(mx.getRoom(roomId)) && mDirects.has(roomId)), - [mx, mDirects] - ); - return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual)); -}; - -export const useUnsupportedRooms = ( - mx: MatrixClient, - allRoomsAtom: WritableAtom -) => { - const selector = useCallback( - (rooms: string[]) => rooms.filter((roomId) => isUnsupportedRoom(mx.getRoom(roomId))), - [mx] - ); - return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual)); -}; diff --git a/src/app/state/hooks/useBindAtoms.ts b/src/app/state/hooks/useBindAtoms.ts deleted file mode 100644 index 6dc2a3d..0000000 --- a/src/app/state/hooks/useBindAtoms.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MatrixClient } from 'matrix-js-sdk'; -import { allInvitesAtom, useBindAllInvitesAtom } from '../inviteList'; -import { allRoomsAtom, useBindAllRoomsAtom } from '../roomList'; -import { mDirectAtom, useBindMDirectAtom } from '../mDirectList'; -import { muteChangesAtom, mutedRoomsAtom, useBindMutedRoomsAtom } from '../mutedRoomList'; -import { roomToUnreadAtom, useBindRoomToUnreadAtom } from '../roomToUnread'; -import { roomToParentsAtom, useBindRoomToParentsAtom } from '../roomToParents'; - -export const useBindAtoms = (mx: MatrixClient) => { - useBindMDirectAtom(mx, mDirectAtom); - useBindAllInvitesAtom(mx, allInvitesAtom); - useBindAllRoomsAtom(mx, allRoomsAtom); - useBindRoomToParentsAtom(mx, roomToParentsAtom); - useBindMutedRoomsAtom(mx, mutedRoomsAtom); - useBindRoomToUnreadAtom(mx, roomToUnreadAtom, muteChangesAtom); -}; diff --git a/src/app/state/inviteList.ts b/src/app/state/inviteList.ts deleted file mode 100644 index 463fd35..0000000 --- a/src/app/state/inviteList.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { atom, WritableAtom } from 'jotai'; -import { MatrixClient } from 'matrix-js-sdk'; -import { useMemo } from 'react'; -import { Membership } from '../../types/matrix/room'; -import { RoomsAction, useBindRoomsWithMembershipsAtom } from './utils'; - -const baseRoomsAtom = atom([]); -export const allInvitesAtom = atom( - (get) => get(baseRoomsAtom), - (get, set, action) => { - if (action.type === 'INITIALIZE') { - set(baseRoomsAtom, action.rooms); - return; - } - set(baseRoomsAtom, (ids) => { - const newIds = ids.filter((id) => id !== action.roomId); - if (action.type === 'PUT') newIds.push(action.roomId); - return newIds; - }); - } -); - -export const useBindAllInvitesAtom = ( - mx: MatrixClient, - allRooms: WritableAtom -) => { - useBindRoomsWithMembershipsAtom( - mx, - allRooms, - useMemo(() => [Membership.Invite], []) - ); -}; diff --git a/src/app/state/mDirectList.ts b/src/app/state/mDirectList.ts deleted file mode 100644 index 96e2f0d..0000000 --- a/src/app/state/mDirectList.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { atom, useSetAtom, WritableAtom } from 'jotai'; -import { ClientEvent, MatrixClient, MatrixEvent } from 'matrix-js-sdk'; -import { useEffect } from 'react'; -import { AccountDataEvent } from '../../types/matrix/accountData'; -import { getAccountData, getMDirects } from '../utils/room'; - -export type MDirectAction = { - type: 'INITIALIZE' | 'UPDATE'; - rooms: Set; -}; - -const baseMDirectAtom = atom(new Set()); -export const mDirectAtom = atom, MDirectAction>( - (get) => get(baseMDirectAtom), - (get, set, action) => { - set(baseMDirectAtom, action.rooms); - } -); - -export const useBindMDirectAtom = ( - mx: MatrixClient, - mDirect: WritableAtom, MDirectAction> -) => { - const setMDirect = useSetAtom(mDirect); - - useEffect(() => { - const mDirectEvent = getAccountData(mx, AccountDataEvent.Direct); - if (mDirectEvent) { - setMDirect({ - type: 'INITIALIZE', - rooms: getMDirects(mDirectEvent), - }); - } - - const handleAccountData = (event: MatrixEvent) => { - setMDirect({ - type: 'UPDATE', - rooms: getMDirects(event), - }); - }; - - mx.on(ClientEvent.AccountData, handleAccountData); - return () => { - mx.removeListener(ClientEvent.AccountData, handleAccountData); - }; - }, [mx, setMDirect]); -}; diff --git a/src/app/state/mutedRoomList.ts b/src/app/state/mutedRoomList.ts deleted file mode 100644 index d456f85..0000000 --- a/src/app/state/mutedRoomList.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { atom, WritableAtom, useSetAtom } from 'jotai'; -import { ClientEvent, IPushRule, IPushRules, MatrixClient, MatrixEvent } from 'matrix-js-sdk'; -import { useEffect } from 'react'; -import { MuteChanges } from '../../types/matrix/room'; -import { findMutedRule, isMutedRule } from '../utils/room'; - -export type MutedRoomsUpdate = - | { - type: 'INITIALIZE'; - addRooms: string[]; - } - | { - type: 'UPDATE'; - addRooms: string[]; - removeRooms: string[]; - }; - -export const muteChangesAtom = atom({ - added: [], - removed: [], -}); - -const baseMutedRoomsAtom = atom(new Set()); -export const mutedRoomsAtom = atom, MutedRoomsUpdate>( - (get) => get(baseMutedRoomsAtom), - (get, set, action) => { - const mutedRooms = new Set([...get(mutedRoomsAtom)]); - if (action.type === 'INITIALIZE') { - set(baseMutedRoomsAtom, new Set([...action.addRooms])); - set(muteChangesAtom, { - added: [...action.addRooms], - removed: [], - }); - return; - } - if (action.type === 'UPDATE') { - action.removeRooms.forEach((roomId) => mutedRooms.delete(roomId)); - action.addRooms.forEach((roomId) => mutedRooms.add(roomId)); - set(baseMutedRoomsAtom, mutedRooms); - set(muteChangesAtom, { - added: [...action.addRooms], - removed: [...action.removeRooms], - }); - } - } -); - -export const useBindMutedRoomsAtom = ( - mx: MatrixClient, - mutedAtom: WritableAtom, MutedRoomsUpdate> -) => { - const setMuted = useSetAtom(mutedAtom); - - useEffect(() => { - const overrideRules = mx.getAccountData('m.push_rules')?.getContent() - ?.global?.override; - if (overrideRules) { - const mutedRooms = overrideRules.reduce((rooms, rule) => { - if (isMutedRule(rule)) rooms.push(rule.rule_id); - return rooms; - }, []); - setMuted({ - type: 'INITIALIZE', - addRooms: mutedRooms, - }); - } - }, [mx, setMuted]); - - useEffect(() => { - const handlePushRules = (mEvent: MatrixEvent, oldMEvent?: MatrixEvent) => { - if (mEvent.getType() === 'm.push_rules') { - const override = mEvent?.getContent()?.global?.override as IPushRule[] | undefined; - const oldOverride = oldMEvent?.getContent()?.global?.override as IPushRule[] | undefined; - if (!override || !oldOverride) return; - - const isMuteToggled = (rule: IPushRule, otherOverride: IPushRule[]) => { - const roomId = rule.rule_id; - - const isMuted = isMutedRule(rule); - if (!isMuted) return false; - const isOtherMuted = findMutedRule(otherOverride, roomId); - if (isOtherMuted) return false; - return true; - }; - - const mutedRules = override.filter((rule) => isMuteToggled(rule, oldOverride)); - const unMutedRules = oldOverride.filter((rule) => isMuteToggled(rule, override)); - - setMuted({ - type: 'UPDATE', - addRooms: mutedRules.map((rule) => rule.rule_id), - removeRooms: unMutedRules.map((rule) => rule.rule_id), - }); - } - }; - mx.on(ClientEvent.AccountData, handlePushRules); - return () => { - mx.removeListener(ClientEvent.AccountData, handlePushRules); - }; - }, [mx, setMuted]); -}; diff --git a/src/app/state/roomList.ts b/src/app/state/roomList.ts deleted file mode 100644 index 7a793d8..0000000 --- a/src/app/state/roomList.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { atom, WritableAtom } from 'jotai'; -import { MatrixClient } from 'matrix-js-sdk'; -import { useMemo } from 'react'; -import { Membership } from '../../types/matrix/room'; -import { RoomsAction, useBindRoomsWithMembershipsAtom } from './utils'; - -const baseRoomsAtom = atom([]); -export const allRoomsAtom = atom( - (get) => get(baseRoomsAtom), - (get, set, action) => { - if (action.type === 'INITIALIZE') { - set(baseRoomsAtom, action.rooms); - return; - } - set(baseRoomsAtom, (ids) => { - const newIds = ids.filter((id) => id !== action.roomId); - if (action.type === 'PUT') newIds.push(action.roomId); - return newIds; - }); - } -); -export const useBindAllRoomsAtom = ( - mx: MatrixClient, - allRooms: WritableAtom -) => { - useBindRoomsWithMembershipsAtom( - mx, - allRooms, - useMemo(() => [Membership.Join], []) - ); -}; diff --git a/src/app/state/roomToParents.ts b/src/app/state/roomToParents.ts deleted file mode 100644 index 374ddd5..0000000 --- a/src/app/state/roomToParents.ts +++ /dev/null @@ -1,120 +0,0 @@ -import produce from 'immer'; -import { atom, useSetAtom, WritableAtom } from 'jotai'; -import { - ClientEvent, - MatrixClient, - MatrixEvent, - Room, - RoomEvent, - RoomStateEvent, -} from 'matrix-js-sdk'; -import { useEffect } from 'react'; -import { Membership, RoomToParents, StateEvent } from '../../types/matrix/room'; -import { - getRoomToParents, - getSpaceChildren, - isSpace, - isValidChild, - mapParentWithChildren, -} from '../utils/room'; - -export type RoomToParentsAction = - | { - type: 'INITIALIZE'; - roomToParents: RoomToParents; - } - | { - type: 'PUT'; - parent: string; - children: string[]; - } - | { - type: 'DELETE'; - roomId: string; - }; - -const baseRoomToParents = atom(new Map()); -export const roomToParentsAtom = atom( - (get) => get(baseRoomToParents), - (get, set, action) => { - if (action.type === 'INITIALIZE') { - set(baseRoomToParents, action.roomToParents); - return; - } - if (action.type === 'PUT') { - set( - baseRoomToParents, - produce(get(baseRoomToParents), (draftRoomToParents) => { - mapParentWithChildren(draftRoomToParents, action.parent, action.children); - }) - ); - return; - } - if (action.type === 'DELETE') { - set( - baseRoomToParents, - produce(get(baseRoomToParents), (draftRoomToParents) => { - const noParentRooms: string[] = []; - draftRoomToParents.delete(action.roomId); - draftRoomToParents.forEach((parents, child) => { - parents.delete(action.roomId); - if (parents.size === 0) noParentRooms.push(child); - }); - noParentRooms.forEach((room) => draftRoomToParents.delete(room)); - }) - ); - } - } -); - -export const useBindRoomToParentsAtom = ( - mx: MatrixClient, - roomToParents: WritableAtom -) => { - const setRoomToParents = useSetAtom(roomToParents); - - useEffect(() => { - setRoomToParents({ type: 'INITIALIZE', roomToParents: getRoomToParents(mx) }); - - const handleAddRoom = (room: Room) => { - if (isSpace(room) && room.getMyMembership() !== Membership.Invite) { - setRoomToParents({ type: 'PUT', parent: room.roomId, children: getSpaceChildren(room) }); - } - }; - - const handleMembershipChange = (room: Room, membership: string) => { - if (isSpace(room) && membership === Membership.Join) { - setRoomToParents({ type: 'PUT', parent: room.roomId, children: getSpaceChildren(room) }); - } - }; - - const handleStateChange = (mEvent: MatrixEvent) => { - if (mEvent.getType() === StateEvent.SpaceChild) { - const childId = mEvent.getStateKey(); - const roomId = mEvent.getRoomId(); - if (childId && roomId) { - if (isValidChild(mEvent)) { - setRoomToParents({ type: 'PUT', parent: roomId, children: [childId] }); - } else { - setRoomToParents({ type: 'DELETE', roomId: childId }); - } - } - } - }; - - const handleDeleteRoom = (roomId: string) => { - setRoomToParents({ type: 'DELETE', roomId }); - }; - - mx.on(ClientEvent.Room, handleAddRoom); - mx.on(RoomEvent.MyMembership, handleMembershipChange); - mx.on(RoomStateEvent.Events, handleStateChange); - mx.on(ClientEvent.DeleteRoom, handleDeleteRoom); - return () => { - mx.removeListener(ClientEvent.Room, handleAddRoom); - mx.removeListener(RoomEvent.MyMembership, handleMembershipChange); - mx.removeListener(RoomStateEvent.Events, handleStateChange); - mx.removeListener(ClientEvent.DeleteRoom, handleDeleteRoom); - }; - }, [mx, setRoomToParents]); -}; diff --git a/src/app/state/roomToUnread.ts b/src/app/state/roomToUnread.ts deleted file mode 100644 index 0c7b6bd..0000000 --- a/src/app/state/roomToUnread.ts +++ /dev/null @@ -1,219 +0,0 @@ -import produce from 'immer'; -import { atom, useSetAtom, PrimitiveAtom, WritableAtom, useAtomValue } from 'jotai'; -import { IRoomTimelineData, MatrixClient, MatrixEvent, Room, RoomEvent } from 'matrix-js-sdk'; -import { ReceiptContent, ReceiptType } from 'matrix-js-sdk/lib/@types/read_receipts'; -import { useEffect } from 'react'; -import { - MuteChanges, - Membership, - NotificationType, - RoomToUnread, - UnreadInfo, -} from '../../types/matrix/room'; -import { - getAllParents, - getNotificationType, - getUnreadInfo, - getUnreadInfos, - isNotificationEvent, - roomHaveUnread, -} from '../utils/room'; -import { roomToParentsAtom } from './roomToParents'; - -export type RoomToUnreadAction = - | { - type: 'RESET'; - unreadInfos: UnreadInfo[]; - } - | { - type: 'PUT'; - unreadInfo: UnreadInfo; - } - | { - type: 'DELETE'; - roomId: string; - }; - -const putUnreadInfo = ( - roomToUnread: RoomToUnread, - allParents: Set, - unreadInfo: UnreadInfo -) => { - const oldUnread = roomToUnread.get(unreadInfo.roomId) ?? { highlight: 0, total: 0, from: null }; - roomToUnread.set(unreadInfo.roomId, { - highlight: unreadInfo.highlight, - total: unreadInfo.total, - from: null, - }); - - const newH = unreadInfo.highlight - oldUnread.highlight; - const newT = unreadInfo.total - oldUnread.total; - - allParents.forEach((parentId) => { - const oldParentUnread = roomToUnread.get(parentId) ?? { highlight: 0, total: 0, from: null }; - roomToUnread.set(parentId, { - highlight: (oldParentUnread.highlight += newH), - total: (oldParentUnread.total += newT), - from: new Set([...(oldParentUnread.from ?? []), unreadInfo.roomId]), - }); - }); -}; - -const deleteUnreadInfo = (roomToUnread: RoomToUnread, allParents: Set, roomId: string) => { - const oldUnread = roomToUnread.get(roomId); - if (!oldUnread) return; - roomToUnread.delete(roomId); - - allParents.forEach((parentId) => { - const oldParentUnread = roomToUnread.get(parentId); - if (!oldParentUnread) return; - const newFrom = new Set([...(oldParentUnread.from ?? roomId)]); - newFrom.delete(roomId); - if (newFrom.size === 0) { - roomToUnread.delete(parentId); - return; - } - roomToUnread.set(parentId, { - highlight: oldParentUnread.highlight - oldUnread.highlight, - total: oldParentUnread.total - oldUnread.total, - from: newFrom, - }); - }); -}; - -const baseRoomToUnread = atom(new Map()); -export const roomToUnreadAtom = atom( - (get) => get(baseRoomToUnread), - (get, set, action) => { - if (action.type === 'RESET') { - const draftRoomToUnread: RoomToUnread = new Map(); - action.unreadInfos.forEach((unreadInfo) => { - putUnreadInfo( - draftRoomToUnread, - getAllParents(get(roomToParentsAtom), unreadInfo.roomId), - unreadInfo - ); - }); - set(baseRoomToUnread, draftRoomToUnread); - return; - } - if (action.type === 'PUT') { - set( - baseRoomToUnread, - produce(get(baseRoomToUnread), (draftRoomToUnread) => - putUnreadInfo( - draftRoomToUnread, - getAllParents(get(roomToParentsAtom), action.unreadInfo.roomId), - action.unreadInfo - ) - ) - ); - return; - } - if (action.type === 'DELETE' && get(baseRoomToUnread).has(action.roomId)) { - set( - baseRoomToUnread, - produce(get(baseRoomToUnread), (draftRoomToUnread) => - deleteUnreadInfo( - draftRoomToUnread, - getAllParents(get(roomToParentsAtom), action.roomId), - action.roomId - ) - ) - ); - } - } -); - -export const useBindRoomToUnreadAtom = ( - mx: MatrixClient, - unreadAtom: WritableAtom, - muteChangesAtom: PrimitiveAtom -) => { - const setUnreadAtom = useSetAtom(unreadAtom); - const muteChanges = useAtomValue(muteChangesAtom); - - useEffect(() => { - setUnreadAtom({ - type: 'RESET', - unreadInfos: getUnreadInfos(mx), - }); - }, [mx, setUnreadAtom]); - - useEffect(() => { - const handleTimelineEvent = ( - mEvent: MatrixEvent, - room: Room | undefined, - toStartOfTimeline: boolean | undefined, - removed: boolean, - data: IRoomTimelineData - ) => { - if (!room || !data.liveEvent || room.isSpaceRoom() || !isNotificationEvent(mEvent)) return; - if (getNotificationType(mx, room.roomId) === NotificationType.Mute) { - setUnreadAtom({ - type: 'DELETE', - roomId: room.roomId, - }); - return; - } - - if (mEvent.getSender() === mx.getUserId()) return; - setUnreadAtom({ type: 'PUT', unreadInfo: getUnreadInfo(room) }); - }; - mx.on(RoomEvent.Timeline, handleTimelineEvent); - return () => { - mx.removeListener(RoomEvent.Timeline, handleTimelineEvent); - }; - }, [mx, setUnreadAtom]); - - useEffect(() => { - const handleReceipt = (mEvent: MatrixEvent, room: Room) => { - if (mEvent.getType() === 'm.receipt') { - const myUserId = mx.getUserId(); - if (!myUserId) return; - if (room.isSpaceRoom()) return; - const content = mEvent.getContent(); - - const isMyReceipt = Object.keys(content).find((eventId) => - (Object.keys(content[eventId]) as ReceiptType[]).find( - (receiptType) => content[eventId][receiptType][myUserId] - ) - ); - if (isMyReceipt) { - setUnreadAtom({ type: 'DELETE', roomId: room.roomId }); - } - } - }; - mx.on(RoomEvent.Receipt, handleReceipt); - return () => { - mx.removeListener(RoomEvent.Receipt, handleReceipt); - }; - }, [mx, setUnreadAtom]); - - useEffect(() => { - muteChanges.removed.forEach((roomId) => { - const room = mx.getRoom(roomId); - if (!room) return; - if (!roomHaveUnread(mx, room)) return; - setUnreadAtom({ type: 'PUT', unreadInfo: getUnreadInfo(room) }); - }); - muteChanges.added.forEach((roomId) => { - setUnreadAtom({ type: 'DELETE', roomId }); - }); - }, [mx, setUnreadAtom, muteChanges]); - - useEffect(() => { - const handleMembershipChange = (room: Room, membership: string) => { - if (membership !== Membership.Join) { - setUnreadAtom({ - type: 'DELETE', - roomId: room.roomId, - }); - } - }; - mx.on(RoomEvent.MyMembership, handleMembershipChange); - return () => { - mx.removeListener(RoomEvent.MyMembership, handleMembershipChange); - }; - }, [mx, setUnreadAtom]); -}; diff --git a/src/app/state/selectedRoom.ts b/src/app/state/selectedRoom.ts deleted file mode 100644 index 1ef04de..0000000 --- a/src/app/state/selectedRoom.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { atom } from 'jotai'; - -export const selectedRoomAtom = atom(undefined); diff --git a/src/app/state/selectedTab.ts b/src/app/state/selectedTab.ts deleted file mode 100644 index e680ae6..0000000 --- a/src/app/state/selectedTab.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { atom } from 'jotai'; - -export enum SidebarTab { - Home = 'Home', - People = 'People', -} - -export const selectedTabAtom = atom(SidebarTab.Home); diff --git a/src/app/state/tabToRoom.ts b/src/app/state/tabToRoom.ts deleted file mode 100644 index 2f4ee92..0000000 --- a/src/app/state/tabToRoom.ts +++ /dev/null @@ -1,34 +0,0 @@ -import produce from 'immer'; -import { atom } from 'jotai'; -import { MatrixClient } from 'matrix-js-sdk'; - -type RoomInfo = { - roomId: string; - timestamp: number; -}; -type TabToRoom = Map; - -type TabToRoomAction = { - type: 'PUT'; - tabInfo: { tabId: string; roomInfo: RoomInfo }; -}; - -const baseTabToRoom = atom(new Map()); -export const tabToRoomAtom = atom( - (get) => get(baseTabToRoom), - (get, set, action) => { - if (action.type === 'PUT') { - set( - baseTabToRoom, - produce(get(baseTabToRoom), (draft) => { - draft.set(action.tabInfo.tabId, action.tabInfo.roomInfo); - }) - ); - } - } -); - -export const useBindTabToRoomAtom = (mx: MatrixClient) => { - console.log(mx); - // TODO: -}; diff --git a/src/app/state/utils.ts b/src/app/state/utils.ts deleted file mode 100644 index 355c941..0000000 --- a/src/app/state/utils.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useSetAtom, WritableAtom } from 'jotai'; -import { ClientEvent, MatrixClient, Room, RoomEvent } from 'matrix-js-sdk'; -import { useEffect } from 'react'; -import { Membership } from '../../types/matrix/room'; - -export type RoomsAction = - | { - type: 'INITIALIZE'; - rooms: string[]; - } - | { - type: 'PUT' | 'DELETE'; - roomId: string; - }; - -export const useBindRoomsWithMembershipsAtom = ( - mx: MatrixClient, - roomsAtom: WritableAtom, - memberships: Membership[] -) => { - const setRoomsAtom = useSetAtom(roomsAtom); - - useEffect(() => { - const satisfyMembership = (room: Room): boolean => - !!memberships.find((membership) => membership === room.getMyMembership()); - setRoomsAtom({ - type: 'INITIALIZE', - rooms: mx - .getRooms() - .filter(satisfyMembership) - .map((room) => room.roomId), - }); - - const handleAddRoom = (room: Room) => { - if (satisfyMembership(room)) { - setRoomsAtom({ type: 'PUT', roomId: room.roomId }); - } - }; - - const handleMembershipChange = (room: Room) => { - if (!satisfyMembership(room)) { - setRoomsAtom({ type: 'DELETE', roomId: room.roomId }); - } - }; - - const handleDeleteRoom = (roomId: string) => { - setRoomsAtom({ type: 'DELETE', roomId }); - }; - - mx.on(ClientEvent.Room, handleAddRoom); - mx.on(RoomEvent.MyMembership, handleMembershipChange); - mx.on(ClientEvent.DeleteRoom, handleDeleteRoom); - return () => { - mx.removeListener(ClientEvent.Room, handleAddRoom); - mx.removeListener(RoomEvent.MyMembership, handleMembershipChange); - mx.removeListener(ClientEvent.DeleteRoom, handleDeleteRoom); - }; - }, [mx, memberships, setRoomsAtom]); -}; - -export const compareRoomsEqual = (a: string[], b: string[]) => { - if (a.length !== b.length) return false; - return a.every((roomId, roomIdIndex) => roomId === b[roomIdIndex]); -}; diff --git a/src/app/utils/disposable.ts b/src/app/utils/disposable.ts deleted file mode 100644 index 7840fe4..0000000 --- a/src/app/utils/disposable.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type DisposeCallback = (...args: Q) => R; -export type DisposableContext

= ( - ...args: P -) => DisposeCallback; - -export const disposable =

( - context: DisposableContext -) => context; diff --git a/src/client/mx.ts b/src/client/mx.ts deleted file mode 100644 index 3090945..0000000 --- a/src/client/mx.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { MatrixClient } from 'matrix-js-sdk'; -import initMatrix from './initMatrix'; - -export const mx = (): MatrixClient => { - if (!initMatrix.matrixClient) console.error('Matrix client is used before initialization!'); - return initMatrix.matrixClient!; -}; diff --git a/src/client/state/RoomTimeline.js b/src/client/state/RoomTimeline.js deleted file mode 100644 index 57d91c1..0000000 --- a/src/client/state/RoomTimeline.js +++ /dev/null @@ -1,407 +0,0 @@ -import EventEmitter from 'events'; -import initMatrix from '../initMatrix'; -import cons from './cons'; - -import settings from './settings'; - -function isEdited(mEvent) { - return mEvent.getRelation()?.rel_type === 'm.replace'; -} - -function isReaction(mEvent) { - return mEvent.getType() === 'm.reaction'; -} - -function hideMemberEvents(mEvent) { - const content = mEvent.getContent(); - const prevContent = mEvent.getPrevContent(); - const { membership } = content; - if (settings.hideMembershipEvents) { - if (membership === 'invite' || membership === 'ban' || membership === 'leave') return true; - if (prevContent.membership !== 'join') return true; - } - if (settings.hideNickAvatarEvents) { - if (membership === 'join' && prevContent.membership === 'join') return true; - } - return false; -} - -function getRelateToId(mEvent) { - const relation = mEvent.getRelation(); - return relation && relation.event_id; -} - -function addToMap(myMap, mEvent) { - const relateToId = getRelateToId(mEvent); - if (relateToId === null) return null; - const mEventId = mEvent.getId(); - - if (typeof myMap.get(relateToId) === 'undefined') myMap.set(relateToId, []); - const mEvents = myMap.get(relateToId); - if (mEvents.find((ev) => ev.getId() === mEventId)) return mEvent; - mEvents.push(mEvent); - return mEvent; -} - -function getFirstLinkedTimeline(timeline) { - let tm = timeline; - while (tm.prevTimeline) { - tm = tm.prevTimeline; - } - return tm; -} -function getLastLinkedTimeline(timeline) { - let tm = timeline; - while (tm.nextTimeline) { - tm = tm.nextTimeline; - } - return tm; -} - -function iterateLinkedTimelines(timeline, backwards, callback) { - let tm = timeline; - while (tm) { - callback(tm); - if (backwards) tm = tm.prevTimeline; - else tm = tm.nextTimeline; - } -} - -function isTimelineLinked(tm1, tm2) { - let tm = getFirstLinkedTimeline(tm1); - while (tm) { - if (tm === tm2) return true; - tm = tm.nextTimeline; - } - return false; -} - -class RoomTimeline extends EventEmitter { - constructor(roomId) { - super(); - // These are local timelines - this.timeline = []; - this.editedTimeline = new Map(); - this.reactionTimeline = new Map(); - this.typingMembers = new Set(); - - this.matrixClient = initMatrix.matrixClient; - this.roomId = roomId; - this.room = this.matrixClient.getRoom(roomId); - - this.liveTimeline = this.room.getLiveTimeline(); - this.activeTimeline = this.liveTimeline; - - this.isOngoingPagination = false; - this.ongoingDecryptionCount = 0; - this.initialized = false; - - setTimeout(() => this.room.loadMembersIfNeeded()); - - // TODO: remove below line - window.selectedRoom = this; - } - - isServingLiveTimeline() { - return getLastLinkedTimeline(this.activeTimeline) === this.liveTimeline; - } - - canPaginateBackward() { - if (this.timeline[0]?.getType() === 'm.room.create') return false; - const tm = getFirstLinkedTimeline(this.activeTimeline); - return tm.getPaginationToken('b') !== null; - } - - canPaginateForward() { - return !this.isServingLiveTimeline(); - } - - isEncrypted() { - return this.matrixClient.isRoomEncrypted(this.roomId); - } - - clearLocalTimelines() { - this.timeline = []; - } - - addToTimeline(mEvent) { - if (mEvent.getType() === 'm.room.member' && hideMemberEvents(mEvent)) { - return; - } - if (mEvent.isRedacted()) return; - if (isReaction(mEvent)) { - addToMap(this.reactionTimeline, mEvent); - return; - } - if (!cons.supportEventTypes.includes(mEvent.getType())) return; - if (isEdited(mEvent)) { - addToMap(this.editedTimeline, mEvent); - return; - } - this.timeline.push(mEvent); - } - - _populateAllLinkedEvents(timeline) { - const firstTimeline = getFirstLinkedTimeline(timeline); - iterateLinkedTimelines(firstTimeline, false, (tm) => { - tm.getEvents().forEach((mEvent) => this.addToTimeline(mEvent)); - }); - } - - _populateTimelines() { - this.clearLocalTimelines(); - this._populateAllLinkedEvents(this.activeTimeline); - } - - async _reset() { - if (this.isEncrypted()) await this.decryptAllEventsOfTimeline(this.activeTimeline); - this._populateTimelines(); - if (!this.initialized) { - this.initialized = true; - this._listenEvents(); - } - } - - async loadLiveTimeline() { - this.activeTimeline = this.liveTimeline; - await this._reset(); - this.emit(cons.events.roomTimeline.READY, null); - return true; - } - - async loadEventTimeline(eventId) { - // we use first unfiltered EventTimelineSet for room pagination. - const timelineSet = this.getUnfilteredTimelineSet(); - try { - const eventTimeline = await this.matrixClient.getEventTimeline(timelineSet, eventId); - this.activeTimeline = eventTimeline; - await this._reset(); - this.emit(cons.events.roomTimeline.READY, eventId); - return true; - } catch { - return false; - } - } - - async paginateTimeline(backwards = false, limit = 30) { - if (this.initialized === false) return false; - if (this.isOngoingPagination) return false; - - this.isOngoingPagination = true; - - const timelineToPaginate = backwards - ? getFirstLinkedTimeline(this.activeTimeline) - : getLastLinkedTimeline(this.activeTimeline); - - if (timelineToPaginate.getPaginationToken(backwards ? 'b' : 'f') === null) { - this.emit(cons.events.roomTimeline.PAGINATED, backwards, 0); - this.isOngoingPagination = false; - return false; - } - - const oldSize = this.timeline.length; - try { - await this.matrixClient.paginateEventTimeline(timelineToPaginate, { backwards, limit }); - - if (this.isEncrypted()) await this.decryptAllEventsOfTimeline(this.activeTimeline); - this._populateTimelines(); - - const loaded = this.timeline.length - oldSize; - this.emit(cons.events.roomTimeline.PAGINATED, backwards, loaded); - this.isOngoingPagination = false; - return true; - } catch { - this.emit(cons.events.roomTimeline.PAGINATED, backwards, 0); - this.isOngoingPagination = false; - return false; - } - } - - decryptAllEventsOfTimeline(eventTimeline) { - const decryptionPromises = eventTimeline - .getEvents() - .filter((event) => event.isEncrypted() && !event.clearEvent) - .reverse() - .map((event) => event.attemptDecryption(this.matrixClient.crypto, { isRetry: true })); - - return Promise.allSettled(decryptionPromises); - } - - hasEventInTimeline(eventId, timeline = this.activeTimeline) { - const timelineSet = this.getUnfilteredTimelineSet(); - const eventTimeline = timelineSet.getTimelineForEvent(eventId); - if (!eventTimeline) return false; - return isTimelineLinked(eventTimeline, timeline); - } - - getUnfilteredTimelineSet() { - return this.room.getUnfilteredTimelineSet(); - } - - getEventReaders(mEvent) { - const liveEvents = this.liveTimeline.getEvents(); - const readers = []; - if (!mEvent) return []; - - for (let i = liveEvents.length - 1; i >= 0; i -= 1) { - readers.splice(readers.length, 0, ...this.room.getUsersReadUpTo(liveEvents[i])); - if (mEvent === liveEvents[i]) break; - } - - return [...new Set(readers)]; - } - - getLiveReaders() { - const liveEvents = this.liveTimeline.getEvents(); - const getLatestVisibleEvent = () => { - for (let i = liveEvents.length - 1; i >= 0; i -= 1) { - const mEvent = liveEvents[i]; - if (mEvent.getType() === 'm.room.member' && hideMemberEvents(mEvent)) { - // eslint-disable-next-line no-continue - continue; - } - if (!mEvent.isRedacted() - && !isReaction(mEvent) - && !isEdited(mEvent) - && cons.supportEventTypes.includes(mEvent.getType()) - ) return mEvent; - } - return liveEvents[liveEvents.length - 1]; - }; - - return this.getEventReaders(getLatestVisibleEvent()); - } - - getUnreadEventIndex(readUpToEventId) { - if (!this.hasEventInTimeline(readUpToEventId)) return -1; - - const readUpToEvent = this.findEventByIdInTimelineSet(readUpToEventId); - if (!readUpToEvent) return -1; - const rTs = readUpToEvent.getTs(); - - const tLength = this.timeline.length; - - for (let i = 0; i < tLength; i += 1) { - const mEvent = this.timeline[i]; - if (mEvent.getTs() > rTs) return i; - } - return -1; - } - - getReadUpToEventId() { - return this.room.getEventReadUpTo(this.matrixClient.getUserId()); - } - - getEventIndex(eventId) { - return this.timeline.findIndex((mEvent) => mEvent.getId() === eventId); - } - - findEventByIdInTimelineSet(eventId, eventTimelineSet = this.getUnfilteredTimelineSet()) { - return eventTimelineSet.findEventById(eventId); - } - - findEventById(eventId) { - return this.timeline[this.getEventIndex(eventId)] ?? null; - } - - deleteFromTimeline(eventId) { - const i = this.getEventIndex(eventId); - if (i === -1) return undefined; - return this.timeline.splice(i, 1)[0]; - } - - _listenEvents() { - this._listenRoomTimeline = (event, room, toStartOfTimeline, removed, data) => { - if (room.roomId !== this.roomId) return; - if (this.isOngoingPagination) return; - - // User is currently viewing the old events probably - // no need to add new event and emit changes. - // only add reactions and edited messages - if (this.isServingLiveTimeline() === false) { - if (!isReaction(event) && !isEdited(event)) return; - } - - // We only process live events here - if (!data.liveEvent) return; - - if (event.isEncrypted()) { - // We will add this event after it is being decrypted. - this.ongoingDecryptionCount += 1; - return; - } - - // FIXME: An unencrypted plain event can come - // while previous event is still decrypting - // and has not been added to timeline - // causing unordered timeline view. - - this.addToTimeline(event); - this.emit(cons.events.roomTimeline.EVENT, event); - }; - - this._listenDecryptEvent = (event) => { - if (event.getRoomId() !== this.roomId) return; - if (this.isOngoingPagination) return; - - // Not a live event. - // so we don't need to process it here - if (this.ongoingDecryptionCount === 0) return; - - if (this.ongoingDecryptionCount > 0) { - this.ongoingDecryptionCount -= 1; - } - this.addToTimeline(event); - this.emit(cons.events.roomTimeline.EVENT, event); - }; - - this._listenRedaction = (mEvent, room) => { - if (room.roomId !== this.roomId) return; - const rEvent = this.deleteFromTimeline(mEvent.event.redacts); - this.editedTimeline.delete(mEvent.event.redacts); - this.reactionTimeline.delete(mEvent.event.redacts); - this.emit(cons.events.roomTimeline.EVENT_REDACTED, rEvent, mEvent); - }; - - this._listenTypingEvent = (event, member) => { - if (member.roomId !== this.roomId) return; - - const isTyping = member.typing; - if (isTyping) this.typingMembers.add(member.userId); - else this.typingMembers.delete(member.userId); - this.emit(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, new Set([...this.typingMembers])); - }; - this._listenReciptEvent = (event, room) => { - // we only process receipt for latest message here. - if (room.roomId !== this.roomId) return; - const receiptContent = event.getContent(); - - const mEvents = this.liveTimeline.getEvents(); - const lastMEvent = mEvents[mEvents.length - 1]; - const lastEventId = lastMEvent.getId(); - const lastEventRecipt = receiptContent[lastEventId]; - - if (typeof lastEventRecipt === 'undefined') return; - if (lastEventRecipt['m.read']) { - this.emit(cons.events.roomTimeline.LIVE_RECEIPT); - } - }; - - this.matrixClient.on('Room.timeline', this._listenRoomTimeline); - this.matrixClient.on('Room.redaction', this._listenRedaction); - this.matrixClient.on('Event.decrypted', this._listenDecryptEvent); - this.matrixClient.on('RoomMember.typing', this._listenTypingEvent); - this.matrixClient.on('Room.receipt', this._listenReciptEvent); - } - - removeInternalListeners() { - if (!this.initialized) return; - this.matrixClient.removeListener('Room.timeline', this._listenRoomTimeline); - this.matrixClient.removeListener('Room.redaction', this._listenRedaction); - this.matrixClient.removeListener('Event.decrypted', this._listenDecryptEvent); - this.matrixClient.removeListener('RoomMember.typing', this._listenTypingEvent); - this.matrixClient.removeListener('Room.receipt', this._listenReciptEvent); - } -} - -export default RoomTimeline;