diff --git a/index.html b/index.html index af2952d38..9196cf3dc 100644 --- a/index.html +++ b/index.html @@ -90,12 +90,6 @@ window.global ||= window;
- - diff --git a/package-lock.json b/package-lock.json index 17e4dd509..ab70f0fbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0", "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3", "@fontsource/inter": "4.5.14", - "@khanacademy/simple-markdown": "0.8.6", "@matrix-org/olm": "3.2.14", "@tanstack/react-query": "5.24.1", "@tanstack/react-query-devtools": "5.24.1", @@ -41,8 +40,6 @@ "immer": "9.0.16", "is-hotkey": "0.2.0", "jotai": "2.6.0", - "katex": "0.16.10", - "linkify-html": "4.0.2", "linkify-react": "4.1.1", "linkifyjs": "4.0.2", "matrix-js-sdk": "29.1.0", @@ -54,8 +51,6 @@ "react-aria": "3.29.1", "react-autosize-textarea": "7.1.0", "react-blurhash": "0.2.0", - "react-dnd": "16.0.1", - "react-dnd-html5-backend": "16.0.1", "react-dom": "18.2.0", "react-error-boundary": "4.0.10", "react-google-recaptcha": "2.1.0", @@ -67,7 +62,6 @@ "slate-history": "0.93.0", "slate-react": "0.98.4", "tippy.js": "6.3.7", - "twemoji": "14.0.2", "ua-parser-js": "1.0.35" }, "devDependencies": { @@ -1109,18 +1103,6 @@ "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" }, - "node_modules/@khanacademy/simple-markdown": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/@khanacademy/simple-markdown/-/simple-markdown-0.8.6.tgz", - "integrity": "sha512-mAUlR9lchzfqunR89pFvNI51jQKsMpJeWYsYWw0DQcUXczn/T/V6510utgvm7X0N3zN87j1SvuKk8cMbl9IAFw==", - "dependencies": { - "@types/react": ">=16.0.0" - }, - "peerDependencies": { - "react": "16.14.0", - "react-dom": "16.14.0" - } - }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -1942,21 +1924,6 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-dnd/asap": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", - "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" - }, - "node_modules/@react-dnd/invariant": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", - "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" - }, - "node_modules/@react-dnd/shallowequal": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", - "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" - }, "node_modules/@react-stately/calendar": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.4.1.tgz", @@ -3307,12 +3274,14 @@ "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true }, "node_modules/@types/react": { "version": "18.2.39", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", + "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3354,7 +3323,8 @@ "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true }, "node_modules/@types/semver": { "version": "7.3.13", @@ -4419,14 +4389,6 @@ "color-support": "bin.js" } }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "engines": { - "node": ">= 12" - } - }, "node_modules/compute-scroll-into-view": { "version": "1.0.20", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", @@ -4723,16 +4685,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/dnd-core": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", - "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", - "dependencies": { - "@react-dnd/asap": "^5.0.1", - "@react-dnd/invariant": "^4.0.1", - "redux": "^4.2.0" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -5559,7 +5511,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-glob": { "version": "3.2.12", @@ -5796,27 +5749,6 @@ "react": ">=16.8.0" } }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-extra/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -6060,7 +5992,8 @@ "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true }, "node_modules/grapheme-splitter": { "version": "1.0.4", @@ -6749,17 +6682,6 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz", - "integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==", - "dependencies": { - "universalify": "^0.1.2" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -6778,21 +6700,6 @@ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" }, - "node_modules/katex": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz", - "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, "node_modules/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -6854,14 +6761,6 @@ "node": ">= 4.0.0" } }, - "node_modules/linkify-html": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/linkify-html/-/linkify-html-4.0.2.tgz", - "integrity": "sha512-YcN3tsyutK2Y/uSuoG0zne8FQdoqzrAgNU5ko0DWE7M2oQ3ms4z/202f2W4TvRm9uxKdrsWAullfynANLaVMqw==", - "peerDependencies": { - "linkifyjs": "^4.0.0" - } - }, "node_modules/linkify-react": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.1.tgz", @@ -7766,43 +7665,6 @@ "react": ">=15" } }, - "node_modules/react-dnd": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", - "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", - "dependencies": { - "@react-dnd/invariant": "^4.0.1", - "@react-dnd/shallowequal": "^4.0.1", - "dnd-core": "^16.0.1", - "fast-deep-equal": "^3.1.3", - "hoist-non-react-statics": "^3.3.2" - }, - "peerDependencies": { - "@types/hoist-non-react-statics": ">= 3.3.1", - "@types/node": ">= 12", - "@types/react": ">= 16", - "react": ">= 16.14" - }, - "peerDependenciesMeta": { - "@types/hoist-non-react-statics": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-dnd-html5-backend": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", - "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", - "dependencies": { - "dnd-core": "^16.0.1" - } - }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -7950,14 +7812,6 @@ "node": ">=8.10.0" } }, - "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -8711,22 +8565,6 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, - "node_modules/twemoji": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/twemoji/-/twemoji-14.0.2.tgz", - "integrity": "sha512-BzOoXIe1QVdmsUmZ54xbEH+8AgtOKUiG53zO5vVP2iUu6h5u9lN15NcuS6te4OY96qx0H7JK9vjjl9WQbkTRuA==", - "dependencies": { - "fs-extra": "^8.0.1", - "jsonfile": "^5.0.0", - "twemoji-parser": "14.0.0", - "universalify": "^0.1.2" - } - }, - "node_modules/twemoji-parser": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-14.0.0.tgz", - "integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8875,14 +8713,6 @@ "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==" }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", diff --git a/package.json b/package.json index e626e8373..a4bf6f34b 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0", "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3", "@fontsource/inter": "4.5.14", - "@khanacademy/simple-markdown": "0.8.6", "@matrix-org/olm": "3.2.14", "@tanstack/react-query": "5.24.1", "@tanstack/react-query-devtools": "5.24.1", @@ -52,8 +51,6 @@ "immer": "9.0.16", "is-hotkey": "0.2.0", "jotai": "2.6.0", - "katex": "0.16.10", - "linkify-html": "4.0.2", "linkify-react": "4.1.1", "linkifyjs": "4.0.2", "matrix-js-sdk": "29.1.0", @@ -65,8 +62,6 @@ "react-aria": "3.29.1", "react-autosize-textarea": "7.1.0", "react-blurhash": "0.2.0", - "react-dnd": "16.0.1", - "react-dnd-html5-backend": "16.0.1", "react-dom": "18.2.0", "react-error-boundary": "4.0.10", "react-google-recaptcha": "2.1.0", @@ -78,7 +73,6 @@ "slate-history": "0.93.0", "slate-react": "0.98.4", "tippy.js": "6.3.7", - "twemoji": "14.0.2", "ua-parser-js": "1.0.35" }, "devDependencies": { diff --git a/src/app/atoms/avatar/Avatar.jsx b/src/app/atoms/avatar/Avatar.jsx index fc46c2504..27bf7c906 100644 --- a/src/app/atoms/avatar/Avatar.jsx +++ b/src/app/atoms/avatar/Avatar.jsx @@ -2,17 +2,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import './Avatar.scss'; -import { twemojify } from '../../../util/twemojify'; - import Text from '../text/Text'; import RawIcon from '../system-icons/RawIcon'; import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg'; import { avatarInitials } from '../../../util/common'; -const Avatar = React.forwardRef(({ - text, bgColor, iconSrc, iconColor, imageSrc, size, -}, ref) => { +const Avatar = React.forwardRef(({ text, bgColor, iconSrc, iconColor, imageSrc, size }, ref) => { let textSize = 's1'; if (size === 'large') textSize = 'h1'; if (size === 'small') textSize = 'b1'; @@ -20,34 +16,34 @@ const Avatar = React.forwardRef(({ return (
- { - imageSrc !== null - ? ( - { e.target.style.backgroundColor = 'transparent'; }} - onError={(e) => { e.target.src = ImageBrokenSVG; }} - alt="" - /> - ) - : ( - - { - iconSrc !== null - ? - : text !== null && ( - - {twemojify(avatarInitials(text))} - - ) - } - - ) - } + {imageSrc !== null ? ( + { + e.target.style.backgroundColor = 'transparent'; + }} + onError={(e) => { + e.target.src = ImageBrokenSVG; + }} + alt="" + /> + ) : ( + + {iconSrc !== null ? ( + + ) : ( + text !== null && ( + + {avatarInitials(text)} + + ) + )} + + )}
); }); diff --git a/src/app/atoms/math/Math.jsx b/src/app/atoms/math/Math.jsx deleted file mode 100644 index ab52a4784..000000000 --- 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/atoms/math/Math.scss b/src/app/atoms/math/Math.scss deleted file mode 100644 index 306b147cf..000000000 --- a/src/app/atoms/math/Math.scss +++ /dev/null @@ -1,3 +0,0 @@ -.katex-display { - margin: 0 !important; -} diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 6f9a2495b..2728a54c8 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -8,7 +8,7 @@ import React, { useRef, useState, } from 'react'; -import { useAtom } from 'jotai'; +import { useAtom, useAtomValue } from 'jotai'; import { isKeyHotkey } from 'is-hotkey'; import { EventType, IContent, MsgType, Room } from 'matrix-js-sdk'; import { ReactEditor } from 'slate-react'; @@ -56,7 +56,6 @@ import { } from '../../components/editor'; import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board'; import { UseStateProvider } from '../../components/UseStateProvider'; -import initMatrix from '../../../client/initMatrix'; import { TUploadContent, encryptFile, getImageInfo, getMxIdLocalPart } from '../../utils/matrix'; import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater'; import { useFilePicker } from '../../hooks/useFilePicker'; @@ -95,6 +94,7 @@ import { } from './msgContent'; import colorMXID from '../../../util/colorMXID'; import { + getAllParents, getMemberDisplayName, parseReplyBody, parseReplyFormattedBody, @@ -107,6 +107,7 @@ import { Command, SHRUG, useCommands } from '../../hooks/useCommands'; import { mobileOrTablet } from '../../utils/user-agent'; import { useElementSizeObserver } from '../../hooks/useElementSizeObserver'; import { ReplyLayout } from '../../components/message'; +import { roomToParentsAtom } from '../../state/room/roomToParents'; interface RoomInputProps { editor: Editor; @@ -121,6 +122,7 @@ export const RoomInput = forwardRef( const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const commands = useCommands(mx, room); const emojiBtnRef = useRef(null); + const roomToParents = useAtomValue(roomToParentsAtom); const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId)); const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId)); @@ -133,13 +135,13 @@ export const RoomInput = forwardRef( const uploadBoardHandlers = useRef(); const imagePackRooms: Room[] = useMemo(() => { - const allParentSpaces = [roomId, ...(initMatrix.roomList?.getAllParentSpaces(roomId) ?? [])]; + const allParentSpaces = [roomId].concat(Array.from(getAllParents(roomToParents, roomId))); return allParentSpaces.reduce((list, rId) => { const r = mx.getRoom(rId); if (r) list.push(r); return list; }, []); - }, [mx, roomId]); + }, [mx, roomId, roomToParents]); const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar'); const [autocompleteQuery, setAutocompleteQuery] = diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 29b874fca..14cdb56e8 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -28,7 +28,7 @@ import classNames from 'classnames'; import { ReactEditor } from 'slate-react'; import { Editor } from 'slate'; import to from 'await-to-js'; -import { useSetAtom } from 'jotai'; +import { useAtomValue, useSetAtom } from 'jotai'; import { Badge, Box, @@ -74,6 +74,7 @@ import { getReactCustomHtmlParser } from '../../plugins/react-custom-html-parser import { canEditEvent, decryptAllTimelineEvent, + getAllParents, getEditedEvent, getEventReactions, getLatestEditableEvt, @@ -103,14 +104,15 @@ import { createMentionElement, isEmptyEditor, moveCursor } from '../../component import { roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts'; import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels'; import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room'; -import initMatrix from '../../../client/initMatrix'; import { useKeyDown } from '../../hooks/useKeyDown'; -import cons from '../../../client/state/cons'; import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange'; import { RenderMessageContent } from '../../components/RenderMessageContent'; import { Image } from '../../components/media'; import { ImageViewer } from '../../components/image-viewer'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; +import { roomToParentsAtom } from '../../state/room/roomToParents'; +import { useRoomUnread } from '../../state/hooks/unread'; +import { roomToUnreadAtom } from '../../state/room/roomToUnread'; const TimelineFloat = as<'div', css.TimelineFloatVariants>( ({ position, className, ...props }, ref) => ( @@ -444,18 +446,19 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel); const [editId, setEditId] = useState(); const { navigateRoom, navigateSpace } = useRoomNavigate(); + const roomToParents = useAtomValue(roomToParentsAtom); + const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const imagePackRooms: Room[] = useMemo(() => { - const allParentSpaces = [ - room.roomId, - ...(initMatrix.roomList?.getAllParentSpaces(room.roomId) ?? []), - ]; + const allParentSpaces = [room.roomId].concat( + Array.from(getAllParents(roomToParents, room.roomId)) + ); return allParentSpaces.reduce((list, rId) => { const r = mx.getRoom(rId); if (r) list.push(r); return list; }, []); - }, [mx, room]); + }, [mx, room, roomToParents]); const [unreadInfo, setUnreadInfo] = useState(() => getRoomUnreadInfo(room, true)); const readUptoEventIdRef = useRef(); @@ -794,15 +797,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli // Remove unreadInfo on mark as read useEffect(() => { - const handleFullRead = (rId: string) => { - if (rId !== room.roomId) return; + if (!unread) { setUnreadInfo(undefined); - }; - initMatrix.notifications?.on(cons.events.notifications.FULL_READ, handleFullRead); - return () => { - initMatrix.notifications?.removeListener(cons.events.notifications.FULL_READ, handleFullRead); - }; - }, [room]); + } + }, [unread]); // scroll out of view msg editor in view. useEffect(() => { diff --git a/src/app/hooks/useCategorizedSpaces.js b/src/app/hooks/useCategorizedSpaces.js deleted file mode 100644 index 6d3c02afd..000000000 --- a/src/app/hooks/useCategorizedSpaces.js +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -import { useState, useEffect } from 'react'; - -import initMatrix from '../../client/initMatrix'; -import cons from '../../client/state/cons'; - -export function useCategorizedSpaces() { - const { accountData } = initMatrix; - const [categorizedSpaces, setCategorizedSpaces] = useState([...accountData.categorizedSpaces]); - - useEffect(() => { - const handleCategorizedSpaces = () => { - setCategorizedSpaces([...accountData.categorizedSpaces]); - }; - accountData.on(cons.events.accountData.CATEGORIZE_SPACE_UPDATED, handleCategorizedSpaces); - return () => { - accountData.removeListener( - cons.events.accountData.CATEGORIZE_SPACE_UPDATED, - handleCategorizedSpaces, - ); - }; - }, []); - - return [categorizedSpaces]; -} diff --git a/src/app/hooks/useCommands.ts b/src/app/hooks/useCommands.ts index aadbf5348..3c8295149 100644 --- a/src/app/hooks/useCommands.ts +++ b/src/app/hooks/useCommands.ts @@ -114,12 +114,12 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { description: 'Leave current room.', exe: async (payload) => { if (payload.trim() === '') { - roomActions.leave(room.roomId); + mx.leave(room.roomId); return; } const rawIds = payload.split(' '); const roomIds = rawIds.filter((id) => isRoomId(id)); - roomIds.map((id) => roomActions.leave(id)); + roomIds.map((id) => mx.leave(id)); }, }, [Command.Invite]: { diff --git a/src/app/hooks/usePreviousValue.ts b/src/app/hooks/usePreviousValue.ts new file mode 100644 index 000000000..01b4850da --- /dev/null +++ b/src/app/hooks/usePreviousValue.ts @@ -0,0 +1,11 @@ +import { useEffect, useRef } from 'react'; + +export const usePreviousValue = (currentValue: T, initialValue: T) => { + const valueRef = useRef(initialValue); + + useEffect(() => { + valueRef.current = currentValue; + }, [currentValue]); + + return valueRef.current; +}; diff --git a/src/app/hooks/useRoomTypingMembers.ts b/src/app/hooks/useRoomTypingMembers.ts index 5f24fb5df..f526cbf16 100644 --- a/src/app/hooks/useRoomTypingMembers.ts +++ b/src/app/hooks/useRoomTypingMembers.ts @@ -1,10 +1,26 @@ import { useAtomValue } from 'jotai'; -import { useMemo } from 'react'; -import { roomIdToTypingMembersAtom, selectRoomTypingMembersAtom } from '../state/typingMembers'; +import { selectAtom } from 'jotai/utils'; +import { useCallback } from 'react'; +import { + IRoomIdToTypingMembers, + TypingReceipt, + roomIdToTypingMembersAtom, +} from '../state/typingMembers'; + +const typingReceiptEqual = (a: TypingReceipt, b: TypingReceipt): boolean => + a.userId === b.userId && a.ts === b.ts; + +const equalTypingMembers = (x: TypingReceipt[], y: TypingReceipt[]): boolean => { + if (x.length !== y.length) return false; + return x.every((a, i) => typingReceiptEqual(a, y[i])); +}; export const useRoomTypingMember = (roomId: string) => { - const typing = useAtomValue( - useMemo(() => selectRoomTypingMembersAtom(roomId, roomIdToTypingMembersAtom), [roomId]) + const selector = useCallback( + (roomToTyping: IRoomIdToTypingMembers) => roomToTyping.get(roomId) ?? [], + [roomId] ); + + const typing = useAtomValue(selectAtom(roomIdToTypingMembersAtom, selector, equalTypingMembers)); return typing; }; diff --git a/src/app/hooks/useSelectedSpace.js b/src/app/hooks/useSelectedSpace.js deleted file mode 100644 index 535c5dcda..000000000 --- a/src/app/hooks/useSelectedSpace.js +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -import { useState, useEffect } from 'react'; - -import cons from '../../client/state/cons'; -import navigation from '../../client/state/navigation'; - -export function useSelectedSpace() { - const [spaceId, setSpaceId] = useState(navigation.selectedSpaceId); - - useEffect(() => { - const onSpaceSelected = (roomId) => { - setSpaceId(roomId); - }; - navigation.on(cons.events.navigation.SPACE_SELECTED, onSpaceSelected); - return () => { - navigation.removeListener(cons.events.navigation.SPACE_SELECTED, onSpaceSelected); - }; - }, []); - - return [spaceId]; -} diff --git a/src/app/hooks/useSelectedTab.js b/src/app/hooks/useSelectedTab.js deleted file mode 100644 index 33b76a81b..000000000 --- a/src/app/hooks/useSelectedTab.js +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -import { useState, useEffect } from 'react'; - -import cons from '../../client/state/cons'; -import navigation from '../../client/state/navigation'; - -export function useSelectedTab() { - const [selectedTab, setSelectedTab] = useState(navigation.selectedTab); - - useEffect(() => { - const onTabSelected = (tabId) => { - setSelectedTab(tabId); - }; - navigation.on(cons.events.navigation.TAB_SELECTED, onTabSelected); - return () => { - navigation.removeListener(cons.events.navigation.TAB_SELECTED, onTabSelected); - }; - }, []); - - return [selectedTab]; -} diff --git a/src/app/hooks/useSpaceShortcut.js b/src/app/hooks/useSpaceShortcut.js deleted file mode 100644 index a1710c603..000000000 --- a/src/app/hooks/useSpaceShortcut.js +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -import { useState, useEffect } from 'react'; - -import initMatrix from '../../client/initMatrix'; -import cons from '../../client/state/cons'; - -export function useSpaceShortcut() { - const { accountData } = initMatrix; - const [spaceShortcut, setSpaceShortcut] = useState([...accountData.spaceShortcut]); - - useEffect(() => { - const onSpaceShortcutUpdated = () => { - setSpaceShortcut([...accountData.spaceShortcut]); - }; - accountData.on(cons.events.accountData.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated); - return () => { - accountData.removeListener( - cons.events.accountData.SPACE_SHORTCUT_UPDATED, - onSpaceShortcutUpdated, - ); - }; - }, []); - - return [spaceShortcut]; -} diff --git a/src/app/molecules/dialog/Dialog.jsx b/src/app/molecules/dialog/Dialog.jsx index 2f29971d5..478a08558 100644 --- a/src/app/molecules/dialog/Dialog.jsx +++ b/src/app/molecules/dialog/Dialog.jsx @@ -2,16 +2,21 @@ import React from 'react'; import PropTypes from 'prop-types'; import './Dialog.scss'; -import { twemojify } from '../../../util/twemojify'; - import Text from '../../atoms/text/Text'; import Header, { TitleWrapper } from '../../atoms/header/Header'; import ScrollView from '../../atoms/scroll/ScrollView'; import RawModal from '../../atoms/modal/RawModal'; function Dialog({ - className, isOpen, title, onAfterOpen, onAfterClose, - contentOptions, onRequestClose, closeFromOutside, children, + className, + isOpen, + title, + onAfterOpen, + onAfterClose, + contentOptions, + onRequestClose, + closeFromOutside, + children, invisibleScroll, }) { return ( @@ -28,19 +33,19 @@ function Dialog({
- { - typeof title === 'string' - ? {twemojify(title)} - : title - } + {typeof title === 'string' ? ( + + {title} + + ) : ( + title + )} {contentOptions}
-
- {children} -
+
{children}
diff --git a/src/app/molecules/following-members/FollowingMembers.jsx b/src/app/molecules/following-members/FollowingMembers.jsx deleted file mode 100644 index 949dac76a..000000000 --- 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/following-members/FollowingMembers.scss b/src/app/molecules/following-members/FollowingMembers.scss deleted file mode 100644 index a0daf5ad5..000000000 --- a/src/app/molecules/following-members/FollowingMembers.scss +++ /dev/null @@ -1,31 +0,0 @@ -@use '../../partials/text'; - -.following-members { - width: 100%; - padding: 0 var(--sp-normal); - display: flex; - justify-content: flex-end; - align-items: center; - cursor: pointer; - - & .ic-raw { - min-width: var(--ic-extra-small); - opacity: 0.4; - margin: 0 var(--sp-extra-tight); - } - & .text { - @extend .cp-txt__ellipsis; - color: var(--tc-surface-low); - b { - color: var(--tc-surface-normal); - } - } - - &:hover, - &:focus { - background-color: var(--bg-surface-hover); - } - &:active { - background-color: var(--bg-surface-active); - } -} \ No newline at end of file diff --git a/src/app/molecules/image-lightbox/ImageLightbox.jsx b/src/app/molecules/image-lightbox/ImageLightbox.jsx deleted file mode 100644 index c1c45db87..000000000 --- a/src/app/molecules/image-lightbox/ImageLightbox.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './ImageLightbox.scss'; -import FileSaver from 'file-saver'; - -import Text from '../../atoms/text/Text'; -import RawModal from '../../atoms/modal/RawModal'; -import IconButton from '../../atoms/button/IconButton'; - -import DownloadSVG from '../../../../public/res/ic/outlined/download.svg'; -import ExternalSVG from '../../../../public/res/ic/outlined/external.svg'; - -function ImageLightbox({ - url, alt, isOpen, onRequestClose, -}) { - const handleDownload = () => { - FileSaver.saveAs(url, alt); - }; - - return ( - -
- {alt} - window.open(url)} size="small" src={ExternalSVG} /> - -
-
- {alt} -
-
- ); -} - -ImageLightbox.propTypes = { - url: PropTypes.string.isRequired, - alt: PropTypes.string.isRequired, - isOpen: PropTypes.bool.isRequired, - onRequestClose: PropTypes.func.isRequired, -}; - -export default ImageLightbox; diff --git a/src/app/molecules/image-lightbox/ImageLightbox.scss b/src/app/molecules/image-lightbox/ImageLightbox.scss deleted file mode 100644 index 201fc91c9..000000000 --- a/src/app/molecules/image-lightbox/ImageLightbox.scss +++ /dev/null @@ -1,50 +0,0 @@ -@use '../../partials/flex'; -@use '../../partials/text'; - -.image-lightbox__modal { - box-shadow: none; - width: unset; - gap: var(--sp-normal); - - border-radius: 0; - pointer-events: none; - - & .text { - color: white; - } - & .ic-raw { - background-color: white; - } -} - -.image-lightbox__overlay { - background-color: var(--bg-overlay-low); -} - - -.image-lightbox__header > *, -.image-lightbox__content > * { - pointer-events: all; -} -.image-lightbox__header { - display: flex; - align-items: center; - - & > .text { - @extend .cp-fx__item-one; - @extend .cp-txt__ellipsis; - } -} -.image-lightbox__content { - display: flex; - justify-content: center; - max-height: 80vh; - - & img { - background-color: var(--bg-surface-low); - object-fit: contain; - max-width: 100%; - max-height: 100%; - border-radius: var(--bo-radius); - } -} \ No newline at end of file diff --git a/src/app/molecules/media/Media.jsx b/src/app/molecules/media/Media.jsx deleted file mode 100644 index e2b61775c..000000000 --- a/src/app/molecules/media/Media.jsx +++ /dev/null @@ -1,366 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import './Media.scss'; - -import encrypt from 'browser-encrypt-attachment'; - -import { BlurhashCanvas } from 'react-blurhash'; -import Text from '../../atoms/text/Text'; -import IconButton from '../../atoms/button/IconButton'; -import Spinner from '../../atoms/spinner/Spinner'; -import ImageLightbox from '../image-lightbox/ImageLightbox'; - -import DownloadSVG from '../../../../public/res/ic/outlined/download.svg'; -import ExternalSVG from '../../../../public/res/ic/outlined/external.svg'; -import PlaySVG from '../../../../public/res/ic/outlined/play.svg'; - -import { getBlobSafeMimeType } from '../../../util/mimetypes'; - -async function getDecryptedBlob(response, type, decryptData) { - const arrayBuffer = await response.arrayBuffer(); - const dataArray = await encrypt.decryptAttachment(arrayBuffer, decryptData); - const blob = new Blob([dataArray], { type: getBlobSafeMimeType(type) }); - return blob; -} - -async function getUrl(link, type, decryptData) { - try { - const response = await fetch(link, { method: 'GET' }); - if (decryptData !== null) { - return URL.createObjectURL(await getDecryptedBlob(response, type, decryptData)); - } - const blob = await response.blob(); - return URL.createObjectURL(blob); - } catch (e) { - return link; - } -} - -function getNativeHeight(width, height, maxWidth = 296) { - const scale = maxWidth / width; - return scale * height; -} - -function FileHeader({ - name, link, external, - file, type, -}) { - const [url, setUrl] = useState(null); - - async function getFile() { - const myUrl = await getUrl(link, type, file); - setUrl(myUrl); - } - - async function handleDownload(e) { - if (file !== null && url === null) { - e.preventDefault(); - await getFile(); - e.target.click(); - } - } - return ( -
- {name} - { link !== null && ( - <> - { - external && ( - window.open(url || link)} - /> - ) - } - - - - - )} -
- ); -} -FileHeader.defaultProps = { - external: false, - file: null, - link: null, -}; -FileHeader.propTypes = { - name: PropTypes.string.isRequired, - link: PropTypes.string, - external: PropTypes.bool, - file: PropTypes.shape({}), - type: PropTypes.string.isRequired, -}; - -function File({ - name, link, file, type, -}) { - return ( -
- -
- ); -} -File.defaultProps = { - file: null, - type: '', -}; -File.propTypes = { - name: PropTypes.string.isRequired, - link: PropTypes.string.isRequired, - type: PropTypes.string, - file: PropTypes.shape({}), -}; - -function Image({ - name, width, height, link, file, type, blurhash, -}) { - const [url, setUrl] = useState(null); - const [blur, setBlur] = useState(true); - const [lightbox, setLightbox] = useState(false); - - useEffect(() => { - let unmounted = false; - async function fetchUrl() { - const myUrl = await getUrl(link, type, file); - if (unmounted) return; - setUrl(myUrl); - } - fetchUrl(); - return () => { - unmounted = true; - }; - }, []); - - const toggleLightbox = () => { - if (!url) return; - setLightbox(!lightbox); - }; - - return ( - <> -
-
- { blurhash && blur && } - { url !== null && ( - setBlur(false)} - src={url || link} - alt={name} - /> - )} -
-
- {url && ( - - )} - - ); -} -Image.defaultProps = { - file: null, - width: null, - height: null, - type: '', - blurhash: '', -}; -Image.propTypes = { - name: PropTypes.string.isRequired, - width: PropTypes.number, - height: PropTypes.number, - link: PropTypes.string.isRequired, - file: PropTypes.shape({}), - type: PropTypes.string, - blurhash: PropTypes.string, -}; - -function Sticker({ - name, height, width, link, file, type, -}) { - const [url, setUrl] = useState(null); - - useEffect(() => { - let unmounted = false; - async function fetchUrl() { - const myUrl = await getUrl(link, type, file); - if (unmounted) return; - setUrl(myUrl); - } - fetchUrl(); - return () => { - unmounted = true; - }; - }, []); - - return ( -
- { url !== null && {name}} -
- ); -} -Sticker.defaultProps = { - file: null, - type: '', - width: null, - height: null, -}; -Sticker.propTypes = { - name: PropTypes.string.isRequired, - width: PropTypes.number, - height: PropTypes.number, - link: PropTypes.string.isRequired, - file: PropTypes.shape({}), - type: PropTypes.string, -}; - -function Audio({ - name, link, type, file, -}) { - const [isLoading, setIsLoading] = useState(false); - const [url, setUrl] = useState(null); - - async function loadAudio() { - const myUrl = await getUrl(link, type, file); - setUrl(myUrl); - setIsLoading(false); - } - function handlePlayAudio() { - setIsLoading(true); - loadAudio(); - } - - return ( -
- -
- { url === null && isLoading && } - { url === null && !isLoading && } - { url !== null && ( - /* eslint-disable-next-line jsx-a11y/media-has-caption */ - - )} -
-
- ); -} -Audio.defaultProps = { - file: null, - type: '', -}; -Audio.propTypes = { - name: PropTypes.string.isRequired, - link: PropTypes.string.isRequired, - type: PropTypes.string, - file: PropTypes.shape({}), -}; - -function Video({ - name, link, thumbnail, thumbnailFile, thumbnailType, - width, height, file, type, blurhash, -}) { - const [isLoading, setIsLoading] = useState(false); - const [url, setUrl] = useState(null); - const [thumbUrl, setThumbUrl] = useState(null); - const [blur, setBlur] = useState(true); - - useEffect(() => { - let unmounted = false; - async function fetchUrl() { - const myThumbUrl = await getUrl(thumbnail, thumbnailType, thumbnailFile); - if (unmounted) return; - setThumbUrl(myThumbUrl); - } - if (thumbnail !== null) fetchUrl(); - return () => { - unmounted = true; - }; - }, []); - - const loadVideo = async () => { - const myUrl = await getUrl(link, type, file); - setUrl(myUrl); - setIsLoading(false); - }; - - const handlePlayVideo = () => { - setIsLoading(true); - loadVideo(); - }; - - return ( -
- -
- { url === null ? ( - <> - { blurhash && blur && } - { thumbUrl !== null && ( - setBlur(false)} alt={name} /> - )} - {isLoading && } - {!isLoading && } - - ) : ( - /* eslint-disable-next-line jsx-a11y/media-has-caption */ - - )} -
-
- ); -} -Video.defaultProps = { - width: null, - height: null, - file: null, - thumbnail: null, - thumbnailType: null, - thumbnailFile: null, - type: '', - blurhash: null, -}; -Video.propTypes = { - name: PropTypes.string.isRequired, - link: PropTypes.string.isRequired, - thumbnail: PropTypes.string, - thumbnailFile: PropTypes.shape({}), - thumbnailType: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - file: PropTypes.shape({}), - type: PropTypes.string, - blurhash: PropTypes.string, -}; - -export { - File, Image, Sticker, Audio, Video, -}; diff --git a/src/app/molecules/media/Media.scss b/src/app/molecules/media/Media.scss deleted file mode 100644 index 8d98c428e..000000000 --- a/src/app/molecules/media/Media.scss +++ /dev/null @@ -1,90 +0,0 @@ -@use '../../partials/text'; - -.file-header { - display: flex; - align-items: center; - padding: var(--sp-ultra-tight) var(--sp-tight); - min-height: 42px; - - & .file-name { - @extend .cp-txt__ellipsis; - flex: 1; - color: var(--tc-surface-low); - } - - & a { - line-height: 0; - } -} - -.file-container { - --media-max-width: 296px; - - background-color: var(--bg-surface-hover); - border-radius: calc(var(--bo-radius) / 2); - overflow: hidden; - max-width: var(--media-max-width); - white-space: initial; -} - -.sticker-container { - display: inline-flex; - max-width: 128px; - width: 100%; - & img { - width: 100% !important; - } -} - -.image-container, -.video-container, -.audio-container { - font-size: 0; - line-height: 0; - - display: flex; - justify-content: center; - align-items: center; - - background-position: center; - background-repeat: no-repeat; - background-size: cover; -} - -.image-container, -.video-container { - & img, - & canvas { - max-width: unset !important; - width: 100% !important; - height: 100%; - border-radius: 0 !important; - margin: 0 !important; - } -} -.image-container { - max-height: 460px; - img { - cursor: pointer; - object-fit: cover; - } -} - -.video-container { - position: relative; - & .ic-btn-surface { - background-color: var(--bg-surface-low); - } - & .ic-btn-surface, - & .donut-spinner { - position: absolute; - } - video { - width: 100%; - } -} -.audio-container { - audio { - width: 100%; - } -} diff --git a/src/app/molecules/message/Message.jsx b/src/app/molecules/message/Message.jsx deleted file mode 100644 index 26a5b29da..000000000 --- a/src/app/molecules/message/Message.jsx +++ /dev/null @@ -1,853 +0,0 @@ -/* eslint-disable react/prop-types */ -import React, { - useState, useEffect, useCallback, useRef, -} from 'react'; -import PropTypes from 'prop-types'; -import './Message.scss'; - -import { twemojify } from '../../../util/twemojify'; - -import initMatrix from '../../../client/initMatrix'; -import { - getUsername, getUsernameOfRoomMember, parseReply, trimHTMLReply, -} from '../../../util/matrixUtil'; -import colorMXID from '../../../util/colorMXID'; -import { getEventCords } from '../../../util/common'; -import { redactEvent, sendReaction } from '../../../client/action/roomTimeline'; -import { - openEmojiBoard, openProfileViewer, openReadReceipts, openViewSource, replyTo, -} from '../../../client/action/navigation'; -import { sanitizeCustomHtml } from '../../../util/sanitize'; - -import Text from '../../atoms/text/Text'; -import RawIcon from '../../atoms/system-icons/RawIcon'; -import Button from '../../atoms/button/Button'; -import Tooltip from '../../atoms/tooltip/Tooltip'; -import Input from '../../atoms/input/Input'; -import Avatar from '../../atoms/avatar/Avatar'; -import IconButton from '../../atoms/button/IconButton'; -import Time from '../../atoms/time/Time'; -import ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu'; -import * as Media from '../media/Media'; - -import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg'; -import EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg'; -import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg'; -import PencilIC from '../../../../public/res/ic/outlined/pencil.svg'; -import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; -import CmdIC from '../../../../public/res/ic/outlined/cmd.svg'; -import BinIC from '../../../../public/res/ic/outlined/bin.svg'; - -import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; -import { getBlobSafeMimeType } from '../../../util/mimetypes'; -import { html, plain } from '../../../util/markdown'; - -function PlaceholderMessage() { - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
- ); -} - -const MessageAvatar = React.memo(({ - roomId, avatarSrc, userId, username, -}) => ( -
- -
-)); - -const MessageHeader = React.memo(({ - userId, username, timestamp, fullTime, -}) => ( -
- - {twemojify(username)} - {twemojify(userId)} - -
- - -
-
-)); -MessageHeader.defaultProps = { - fullTime: false, -}; -MessageHeader.propTypes = { - userId: PropTypes.string.isRequired, - username: PropTypes.string.isRequired, - timestamp: PropTypes.number.isRequired, - fullTime: PropTypes.bool, -}; - -function MessageReply({ name, color, body }) { - return ( -
- - - {twemojify(name)} - {' '} - {twemojify(body)} - -
- ); -} - -MessageReply.propTypes = { - name: PropTypes.string.isRequired, - color: PropTypes.string.isRequired, - body: PropTypes.string.isRequired, -}; - -const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => { - const [reply, setReply] = useState(null); - const isMountedRef = useRef(true); - - useEffect(() => { - const mx = initMatrix.matrixClient; - const timelineSet = roomTimeline.getUnfilteredTimelineSet(); - const loadReply = async () => { - try { - const eTimeline = await mx.getEventTimeline(timelineSet, eventId); - await roomTimeline.decryptAllEventsOfTimeline(eTimeline); - - let mEvent = eTimeline.getTimelineSet().findEventById(eventId); - const editedList = roomTimeline.editedTimeline.get(mEvent.getId()); - if (editedList) { - mEvent = editedList[editedList.length - 1]; - } - - const rawBody = mEvent.getContent().body; - const username = getUsernameOfRoomMember(mEvent.sender); - - if (isMountedRef.current === false) return; - const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***'; - let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody; - if (editedList && parsedBody.startsWith(' * ')) { - parsedBody = parsedBody.slice(3); - } - - setReply({ - to: username, - color: colorMXID(mEvent.getSender()), - body: parsedBody, - event: mEvent, - }); - } catch { - setReply({ - to: '** Unknown user **', - color: 'var(--tc-danger-normal)', - body: '*** Unable to load reply ***', - event: null, - }); - } - }; - loadReply(); - - return () => { - isMountedRef.current = false; - }; - }, []); - - const focusReply = (ev) => { - if (!ev.key || ev.key === ' ' || ev.key === 'Enter') { - if (ev.key) ev.preventDefault(); - if (reply?.event === null) return; - if (reply?.event.isRedacted()) return; - roomTimeline.loadEventTimeline(eventId); - } - }; - - return ( -
- {reply !== null && } -
- ); -}); -MessageReplyWrapper.propTypes = { - roomTimeline: PropTypes.shape({}).isRequired, - eventId: PropTypes.string.isRequired, -}; - -const MessageBody = React.memo(({ - senderName, - body, - isCustomHTML, - isEdited, - msgType, -}) => { - // if body is not string it is a React element. - if (typeof body !== 'string') return
{body}
; - - let content = null; - if (isCustomHTML) { - try { - content = twemojify( - sanitizeCustomHtml(initMatrix.matrixClient, body), - undefined, - true, - false, - true, - ); - } catch { - console.error('Malformed custom html: ', body); - content = twemojify(body, undefined); - } - } else { - content = twemojify(body, undefined, true); - } - - // Determine if this message should render with large emojis - // Criteria: - // - Contains only emoji - // - Contains no more than 10 emoji - let emojiOnly = false; - if (content.type === 'img') { - // If this messages contains only a single (inline) image - emojiOnly = true; - } else if (content.constructor.name === 'Array') { - // Otherwise, it might be an array of images / texb - - // Count the number of emojis - const nEmojis = content.filter((e) => e.type === 'img').length; - - // Make sure there's no text besides whitespace and variation selector U+FE0F - if (nEmojis <= 10 && content.every((element) => ( - (typeof element === 'object' && element.type === 'img') - || (typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element)) - ))) { - emojiOnly = true; - } - } - - if (!isCustomHTML) { - // If this is a plaintext message, wrap it in a

element (automatically applying - // white-space: pre-wrap) in order to preserve newlines - content = (

{content}

); - } - - return ( -
-
- { msgType === 'm.emote' && ( - <> - {'* '} - {twemojify(senderName)} - {' '} - - )} - { content } -
- { isEdited && (edited)} -
- ); -}); -MessageBody.defaultProps = { - isCustomHTML: false, - isEdited: false, - msgType: null, -}; -MessageBody.propTypes = { - senderName: PropTypes.string.isRequired, - body: PropTypes.node.isRequired, - isCustomHTML: PropTypes.bool, - isEdited: PropTypes.bool, - msgType: PropTypes.string, -}; - -function MessageEdit({ body, onSave, onCancel }) { - const editInputRef = useRef(null); - - useEffect(() => { - // makes the cursor end up at the end of the line instead of the beginning - editInputRef.current.value = ''; - editInputRef.current.value = body; - }, []); - - const handleKeyDown = (e) => { - if (e.key === 'Escape') { - e.preventDefault(); - onCancel(); - } - - if (e.key === 'Enter' && e.shiftKey === false) { - e.preventDefault(); - onSave(editInputRef.current.value, body); - } - }; - - return ( -
{ e.preventDefault(); onSave(editInputRef.current.value, body); }}> - -
- - -
-
- ); -} -MessageEdit.propTypes = { - body: PropTypes.string.isRequired, - onSave: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired, -}; - -function getMyEmojiEvent(emojiKey, eventId, roomTimeline) { - const mx = initMatrix.matrixClient; - const rEvents = roomTimeline.reactionTimeline.get(eventId); - let rEvent = null; - rEvents?.find((rE) => { - if (rE.getRelation() === null) return false; - if (rE.getRelation().key === emojiKey && rE.getSender() === mx.getUserId()) { - rEvent = rE; - return true; - } - return false; - }); - return rEvent; -} - -function toggleEmoji(roomId, eventId, emojiKey, shortcode, roomTimeline) { - const myAlreadyReactEvent = getMyEmojiEvent(emojiKey, eventId, roomTimeline); - if (myAlreadyReactEvent) { - const rId = myAlreadyReactEvent.getId(); - if (rId.startsWith('~')) return; - redactEvent(roomId, rId); - return; - } - sendReaction(roomId, eventId, emojiKey, shortcode); -} - -function pickEmoji(e, roomId, eventId, roomTimeline) { - openEmojiBoard(getEventCords(e), (emoji) => { - toggleEmoji(roomId, eventId, emoji.mxc ?? emoji.unicode, emoji.shortcodes[0], roomTimeline); - e.target.click(); - }); -} - -function genReactionMsg(userIds, reaction, shortcode) { - return ( - <> - {userIds.map((userId, index) => ( - - {twemojify(getUsername(userId))} - {index < userIds.length - 1 && ( - - {index === userIds.length - 2 ? ' and ' : ', '} - - )} - - ))} - {' reacted with '} - {twemojify(shortcode ? `:${shortcode}:` : reaction, { className: 'react-emoji' })} - - ); -} - -function MessageReaction({ - reaction, shortcode, count, users, isActive, onClick, -}) { - let customEmojiUrl = null; - if (reaction.match(/^mxc:\/\/\S+$/)) { - customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(reaction); - } - return ( - {users.length > 0 ? genReactionMsg(users, reaction, shortcode) : 'Unable to load who has reacted'}} - > - - - ); -} -MessageReaction.defaultProps = { - shortcode: undefined, -}; -MessageReaction.propTypes = { - reaction: PropTypes.node.isRequired, - shortcode: PropTypes.string, - count: PropTypes.number.isRequired, - users: PropTypes.arrayOf(PropTypes.string).isRequired, - isActive: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired, -}; - -function MessageReactionGroup({ roomTimeline, mEvent }) { - const { roomId, room, reactionTimeline } = roomTimeline; - const mx = initMatrix.matrixClient; - const reactions = {}; - const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId()); - - const eventReactions = reactionTimeline.get(mEvent.getId()); - const addReaction = (key, shortcode, count, senderId, isActive) => { - let reaction = reactions[key]; - if (reaction === undefined) { - reaction = { - count: 0, - users: [], - isActive: false, - }; - } - if (shortcode) reaction.shortcode = shortcode; - if (count) { - reaction.count = count; - } else { - reaction.users.push(senderId); - reaction.count = reaction.users.length; - if (isActive) reaction.isActive = isActive; - } - - reactions[key] = reaction; - }; - if (eventReactions) { - eventReactions.forEach((rEvent) => { - if (rEvent.getRelation() === null) return; - const reaction = rEvent.getRelation(); - const senderId = rEvent.getSender(); - const { shortcode } = rEvent.getContent(); - const isActive = senderId === mx.getUserId(); - - addReaction(reaction.key, shortcode, undefined, senderId, isActive); - }); - } else { - // Use aggregated reactions - const aggregatedReaction = mEvent.getServerAggregatedRelation('m.annotation')?.chunk; - if (!aggregatedReaction) return null; - aggregatedReaction.forEach((reaction) => { - if (reaction.type !== 'm.reaction') return; - addReaction(reaction.key, undefined, reaction.count, undefined, false); - }); - } - - return ( -
- { - Object.keys(reactions).map((key) => ( - { - toggleEmoji(roomId, mEvent.getId(), key, reactions[key].shortcode, roomTimeline); - }} - /> - )) - } - {canSendReaction && ( - { - pickEmoji(e, roomId, mEvent.getId(), roomTimeline); - }} - src={EmojiAddIC} - size="extra-small" - tooltip="Add reaction" - /> - )} -
- ); -} -MessageReactionGroup.propTypes = { - roomTimeline: PropTypes.shape({}).isRequired, - mEvent: PropTypes.shape({}).isRequired, -}; - -function isMedia(mE) { - return ( - mE.getContent()?.msgtype === 'm.file' - || mE.getContent()?.msgtype === 'm.image' - || mE.getContent()?.msgtype === 'm.audio' - || mE.getContent()?.msgtype === 'm.video' - || mE.getType() === 'm.sticker' - ); -} - -// if editedTimeline has mEventId then pass editedMEvent else pass mEvent to openViewSource -function handleOpenViewSource(mEvent, roomTimeline) { - const eventId = mEvent.getId(); - const { editedTimeline } = roomTimeline ?? {}; - let editedMEvent; - if (editedTimeline?.has(eventId)) { - const editedList = editedTimeline.get(eventId); - editedMEvent = editedList[editedList.length - 1]; - } - openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent); -} - -const MessageOptions = React.memo(({ - roomTimeline, mEvent, edit, reply, -}) => { - const { roomId, room } = roomTimeline; - const mx = initMatrix.matrixClient; - const senderId = mEvent.getSender(); - - const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel; - const canIRedact = room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel); - const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId()); - - return ( -
- {canSendReaction && ( - pickEmoji(e, roomId, mEvent.getId(), roomTimeline)} - src={EmojiAddIC} - size="extra-small" - tooltip="Add reaction" - /> - )} - reply()} - src={ReplyArrowIC} - size="extra-small" - tooltip="Reply" - /> - {(senderId === mx.getUserId() && !isMedia(mEvent)) && ( - edit(true)} - src={PencilIC} - size="extra-small" - tooltip="Edit" - /> - )} - ( - <> - Options - openReadReceipts(roomId, roomTimeline.getEventReaders(mEvent))} - > - Read receipts - - handleOpenViewSource(mEvent, roomTimeline)} - > - View source - - {(canIRedact || senderId === mx.getUserId()) && ( - <> - - { - const isConfirmed = await confirmDialog( - 'Delete message', - 'Are you sure that you want to delete this message?', - 'Delete', - 'danger', - ); - if (!isConfirmed) return; - redactEvent(roomId, mEvent.getId()); - }} - > - Delete - - - )} - - )} - render={(toggleMenu) => ( - - )} - /> -
- ); -}); -MessageOptions.propTypes = { - roomTimeline: PropTypes.shape({}).isRequired, - mEvent: PropTypes.shape({}).isRequired, - edit: PropTypes.func.isRequired, - reply: PropTypes.func.isRequired, -}; - -function genMediaContent(mE) { - const mx = initMatrix.matrixClient; - const mContent = mE.getContent(); - if (!mContent || !mContent.body) return Malformed event; - - let mediaMXC = mContent?.url; - const isEncryptedFile = typeof mediaMXC === 'undefined'; - if (isEncryptedFile) mediaMXC = mContent?.file?.url; - - let thumbnailMXC = mContent?.info?.thumbnail_url; - - if (typeof mediaMXC === 'undefined' || mediaMXC === '') return Malformed event; - - let msgType = mE.getContent()?.msgtype; - const safeMimetype = getBlobSafeMimeType(mContent.info?.mimetype); - if (mE.getType() === 'm.sticker') { - msgType = 'm.sticker'; - } else if (safeMimetype === 'application/octet-stream') { - msgType = 'm.file'; - } - - const blurhash = mContent?.info?.['xyz.amorgan.blurhash']; - - switch (msgType) { - case 'm.file': - return ( - - ); - case 'm.image': - return ( - - ); - case 'm.sticker': - return ( - - ); - case 'm.audio': - return ( - - ); - case 'm.video': - if (typeof thumbnailMXC === 'undefined') { - thumbnailMXC = mContent.info?.thumbnail_file?.url || null; - } - return ( - - ); - default: - return Malformed event; - } -} - -function getEditedBody(editedMEvent) { - const newContent = editedMEvent.getContent()['m.new_content']; - if (typeof newContent === 'undefined') return [null, false, null]; - - const isCustomHTML = newContent.format === 'org.matrix.custom.html'; - const parsedContent = parseReply(newContent.body); - if (parsedContent === null) { - return [newContent.body, isCustomHTML, newContent.formatted_body ?? null]; - } - return [parsedContent.body, isCustomHTML, newContent.formatted_body ?? null]; -} - -function Message({ - mEvent, isBodyOnly, roomTimeline, - focus, fullTime, isEdit, setEdit, cancelEdit, -}) { - const roomId = mEvent.getRoomId(); - const { editedTimeline, reactionTimeline } = roomTimeline ?? {}; - - const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')]; - if (focus) className.push('message--focus'); - const content = mEvent.getContent(); - const eventId = mEvent.getId(); - const msgType = content?.msgtype; - const senderId = mEvent.getSender(); - let { body } = content; - const username = mEvent.sender ? getUsernameOfRoomMember(mEvent.sender) : getUsername(senderId); - const avatarSrc = mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null; - let isCustomHTML = content.format === 'org.matrix.custom.html'; - let customHTML = isCustomHTML ? content.formatted_body : null; - - const edit = useCallback(() => { - setEdit(eventId); - }, []); - const reply = useCallback(() => { - replyTo(senderId, mEvent.getId(), body, customHTML); - }, [body, customHTML]); - - if (msgType === 'm.emote') className.push('message--type-emote'); - - const isEdited = roomTimeline ? editedTimeline.has(eventId) : false; - const haveReactions = roomTimeline - ? reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation') - : false; - const isReply = !!mEvent.replyEventId; - - if (isEdited) { - const editedList = editedTimeline.get(eventId); - const editedMEvent = editedList[editedList.length - 1]; - [body, isCustomHTML, customHTML] = getEditedBody(editedMEvent); - } - - if (isReply) { - body = parseReply(body)?.body ?? body; - customHTML = trimHTMLReply(customHTML); - } - - if (typeof body !== 'string') body = ''; - - return ( -
- { - isBodyOnly - ?
- : ( - - ) - } -
- {!isBodyOnly && ( - - )} - {roomTimeline && isReply && ( - - )} - {!isEdit && ( - - )} - {isEdit && ( - { - if (newBody !== oldBody) { - initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody); - } - cancelEdit(); - }} - onCancel={cancelEdit} - /> - )} - {haveReactions && ( - - )} - {roomTimeline && !isEdit && ( - - )} -
-
- ); -} -Message.defaultProps = { - isBodyOnly: false, - focus: false, - roomTimeline: null, - fullTime: false, - isEdit: false, - setEdit: null, - cancelEdit: null, -}; -Message.propTypes = { - mEvent: PropTypes.shape({}).isRequired, - isBodyOnly: PropTypes.bool, - roomTimeline: PropTypes.shape({}), - focus: PropTypes.bool, - fullTime: PropTypes.bool, - isEdit: PropTypes.bool, - setEdit: PropTypes.func, - cancelEdit: PropTypes.func, -}; - -export { Message, MessageReply, PlaceholderMessage }; diff --git a/src/app/molecules/message/Message.scss b/src/app/molecules/message/Message.scss deleted file mode 100644 index 5dda9c98d..000000000 --- a/src/app/molecules/message/Message.scss +++ /dev/null @@ -1,479 +0,0 @@ -@use '../../atoms/scroll/scrollbar'; -@use '../../partials/text'; -@use '../../partials/dir'; -@use '../../partials/screen'; - -.message, -.ph-msg { - padding: var(--sp-ultra-tight); - @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight)); - display: flex; - - &:hover { - background-color: var(--bg-surface-hover); - & .message__options { - display: flex; - } - } - - &__avatar-container { - padding-top: 6px; - @include dir.side(margin, 0, var(--sp-tight)); - - & .avatar-container { - transition: transform 200ms var(--fluid-push); - &:hover { - transform: translateY(-4px); - } - } - - & button { - cursor: pointer; - display: flex; - } - } - - &__main-container { - flex: 1; - min-width: 0; - - position: relative; - } -} - -.message { - &--full + &--full, - &--body-only + &--full, - & + .timeline-change, - .timeline-change + & { - margin-top: var(--sp-normal); - } - &__avatar-container { - width: var(--av-small); - } - &--focus { - --ltr: inset 2px 0 0 var(--bg-caution); - --rtl: inset -2px 0 0 var(--bg-caution); - @include dir.prop(box-shadow, var(--ltr), var(--rtl)); - background-color: var(--bg-caution-hover); - } -} - -.ph-msg { - &__avatar { - width: var(--av-small); - height: var(--av-small); - background-color: var(--bg-surface-hover); - border-radius: var(--bo-radius); - } - - &__header, - &__body > div { - margin: var(--sp-ultra-tight); - @include dir.side(margin, 0, var(--sp-extra-tight)); - height: var(--fs-b1); - width: 100%; - max-width: 100px; - background-color: var(--bg-surface-hover); - border-radius: calc(var(--bo-radius) / 2); - } - &__body { - display: flex; - flex-wrap: wrap; - } - &__body > div:nth-child(1n) { - max-width: 10%; - } - &__body > div:nth-child(2n) { - max-width: 50%; - } -} - -.message__reply, -.message__body, -.message__body__wrapper, -.message__edit, -.message__reactions { - max-width: calc(100% - 88px); - min-width: 0; - @include screen.smallerThan(tabletBreakpoint) { - max-width: 100%; - } -} - -.message__header { - display: flex; - align-items: baseline; - - & .message__profile { - min-width: 0; - color: var(--tc-surface-high); - @include dir.side(margin, 0, var(--sp-tight)); - - & > span { - @extend .cp-txt__ellipsis; - color: inherit; - } - & > span:last-child { - display: none; - } - &:hover { - & > span:first-child { - display: none; - } - & > span:last-child { - display: block; - } - } - } - - & .message__time { - flex: 1; - display: flex; - justify-content: flex-end; - & > .text { - white-space: nowrap; - color: var(--tc-surface-low); - } - } -} -.message__reply { - &-wrapper { - min-height: 20px; - cursor: pointer; - &:empty { - border-radius: calc(var(--bo-radius) / 2); - background-color: var(--bg-surface-hover); - max-width: 200px; - cursor: auto; - } - &:hover .text { - color: var(--tc-surface-high); - } - } - .text { - @extend .cp-txt__ellipsis; - color: var(--tc-surface-low); - } - .ic-raw { - width: 16px; - height: 14px; - } -} -.message__body { - word-break: break-word; - - & > .text > .message__body-plain { - white-space: pre-wrap; - } - - & a { - word-break: break-word; - } - & > .text > a { - white-space: initial !important; - } - - & > .text > p + p { - margin-top: var(--sp-normal); - } - - & span[data-mx-pill] { - background-color: hsla(0, 0%, 64%, 0.15); - padding: 0 2px; - border-radius: 4px; - cursor: pointer; - font-weight: var(--fw-medium); - &:hover { - background-color: hsla(0, 0%, 64%, 0.3); - color: var(--tc-surface-high); - } - - &[data-mx-ping] { - background-color: var(--bg-ping); - &:hover { - background-color: var(--bg-ping-hover); - } - } - } - - & span[data-mx-spoiler] { - border-radius: 4px; - background-color: rgba(124, 124, 124, 0.5); - color: transparent; - cursor: pointer; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - & > * { - opacity: 0; - } - } - - .data-mx-spoiler--visible { - background-color: var(--bg-surface-active) !important; - color: inherit !important; - user-select: initial !important; - & > * { - opacity: inherit !important; - } - } - &-edited { - color: var(--tc-surface-low); - } -} -.message__edit { - padding: var(--sp-extra-tight) 0; - &-btns button { - margin: var(--sp-tight) 0 0 0; - padding: var(--sp-ultra-tight) var(--sp-tight); - min-width: 0; - @include dir.side(margin, 0, var(--sp-tight)); - } -} -.message__reactions { - display: flex; - flex-wrap: wrap; - - & .ic-btn-surface { - display: none; - padding: var(--sp-ultra-tight); - margin-top: var(--sp-extra-tight); - } - &:hover .ic-btn-surface { - display: block; - } -} -.msg__reaction { - margin: var(--sp-extra-tight) 0 0 0; - @include dir.side(margin, 0, var(--sp-extra-tight)); - padding: 0 var(--sp-ultra-tight); - min-height: 26px; - display: inline-flex; - align-items: center; - color: var(--tc-surface-normal); - background-color: var(--bg-surface-low); - border: 1px solid var(--bg-surface-border); - border-radius: 4px; - cursor: pointer; - - & .react-emoji { - height: 16px; - margin: 2px; - } - &-count { - margin: 0 var(--sp-ultra-tight); - color: var(--tc-surface-normal); - } - &-tooltip .react-emoji { - width: 16px; - height: 16px; - margin: 0 var(--sp-ultra-tight); - margin-bottom: -2px; - } - - @media (hover: hover) { - &:hover { - background-color: var(--bg-surface-hover); - } - } - &:active { - background-color: var(--bg-surface-active); - } - - &--active { - background-color: var(--bg-caution-active); - - @media (hover: hover) { - &:hover { - background-color: var(--bg-caution-hover); - } - } - &:active { - background-color: var(--bg-caution-active); - } - } -} -.message__options { - position: absolute; - top: 0; - @include dir.prop(right, 60px, unset); - @include dir.prop(left, unset, 60px); - - z-index: 99; - transform: translateY(-100%); - - border-radius: var(--bo-radius); - box-shadow: var(--bs-surface-border); - background-color: var(--bg-surface-low); - display: none; -} - -// markdown formating -.message__body { - & h1, - h2, - h3, - h4, - h5, - h6 { - margin: 0; - margin-bottom: var(--sp-ultra-tight); - font-weight: var(--fw-medium); - &:first-child { - margin-top: 0; - } - &:last-child { - margin-bottom: 0; - } - } - & h1, - & h2 { - color: var(--tc-surface-high); - margin-top: var(--sp-normal); - font-size: var(--fs-h2); - line-height: var(--lh-h2); - letter-spacing: var(--ls-h2); - } - & h3, - & h4 { - color: var(--tc-surface-high); - margin-top: var(--sp-tight); - font-size: var(--fs-s1); - line-height: var(--lh-s1); - letter-spacing: var(--ls-s1); - } - & h5, - & h6 { - color: var(--tc-surface-high); - margin-top: var(--sp-extra-tight); - font-size: var(--fs-b1); - line-height: var(--lh-b1); - letter-spacing: var(--ls-b1); - } - & hr { - border-color: var(--bg-divider); - } - - .text img { - margin: var(--sp-ultra-tight) 0; - max-width: 296px; - border-radius: calc(var(--bo-radius) / 2); - } - - & p, - & pre, - & blockquote { - margin: 0; - padding: 0; - } - & pre, - & blockquote { - margin: var(--sp-ultra-tight) 0; - padding: var(--sp-extra-tight); - background-color: var(--bg-surface-hover) !important; - border-radius: calc(var(--bo-radius) / 2); - } - & pre { - div { - background: none !important; - margin: 0 !important; - } - span { - background: none !important; - } - .linenumber { - min-width: 2.25em !important; - } - } - & code { - padding: 0 !important; - color: var(--tc-code) !important; - white-space: pre-wrap; - @include scrollbar.scroll; - @include scrollbar.scroll__h; - @include scrollbar.scroll--auto-hide; - } - & pre { - width: fit-content; - max-width: 100%; - @include scrollbar.scroll; - @include scrollbar.scroll__h; - @include scrollbar.scroll--auto-hide; - & code { - color: var(--tc-surface-normal) !important; - white-space: pre; - } - } - & blockquote { - width: fit-content; - max-width: 100%; - @include dir.side(border, 4px solid var(--bg-surface-active), 0); - white-space: initial !important; - - & > * { - white-space: pre-wrap; - } - } - & ul, - & ol { - margin: var(--sp-ultra-tight) 0; - @include dir.side(padding, 24px, 0); - white-space: initial !important; - } - & ul.contains-task-list { - padding: 0; - list-style: none; - } - & table { - display: inline-block; - max-width: 100%; - white-space: normal !important; - background-color: var(--bg-surface-hover); - border-radius: calc(var(--bo-radius) / 2); - border-spacing: 0; - border: 1px solid var(--bg-surface-border); - @include scrollbar.scroll; - @include scrollbar.scroll__h; - @include scrollbar.scroll--auto-hide; - - & td, - & th { - padding: var(--sp-extra-tight); - border: 1px solid var(--bg-surface-border); - border-width: 0 1px 1px 0; - white-space: pre; - &:last-child { - border-width: 0; - border-bottom-width: 1px; - [dir='rtl'] & { - border-width: 0 1px 1px 0; - } - } - [dir='rtl'] &:first-child { - border-width: 0; - border-bottom-width: 1px; - } - } - & tbody tr:nth-child(2n + 1) { - background-color: var(--bg-surface-hover); - } - & tr:last-child td { - border-bottom-width: 0px !important; - } - } -} - -.message.message--type-emote { - .message__body { - font-style: italic; - - // Remove blockness of first `

` so that markdown emotes stay on one line. - p:first-of-type { - display: inline; - } - } -} diff --git a/src/app/molecules/message/TimelineChange.jsx b/src/app/molecules/message/TimelineChange.jsx deleted file mode 100644 index bc6e913f3..000000000 --- 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/message/TimelineChange.scss b/src/app/molecules/message/TimelineChange.scss deleted file mode 100644 index c066a9ae5..000000000 --- a/src/app/molecules/message/TimelineChange.scss +++ /dev/null @@ -1,37 +0,0 @@ -@use '../../partials/dir'; - -.timeline-change { - padding: var(--sp-ultra-tight); - @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight)); - - display: flex; - align-items: center; - width: 100%; - - &:hover { - background-color: var(--bg-surface-hover); - } - - &__avatar-container { - width: var(--av-small); - display: inline-flex; - justify-content: center; - align-items: center; - opacity: 0.38; - .ic-raw { - background-color: var(--tc-surface-low); - } - } - - & .text { - color: var(--tc-surface-low); - } - - &__content { - flex: 1; - min-width: 0; - - margin: 0 var(--sp-tight); - word-break: break-word; - } -} \ No newline at end of file diff --git a/src/app/molecules/people-selector/PeopleSelector.jsx b/src/app/molecules/people-selector/PeopleSelector.jsx index 8ea0587f5..7025aa7c4 100644 --- a/src/app/molecules/people-selector/PeopleSelector.jsx +++ b/src/app/molecules/people-selector/PeopleSelector.jsx @@ -2,16 +2,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import './PeopleSelector.scss'; -import { twemojify } from '../../../util/twemojify'; - import { blurOnBubbling } from '../../atoms/button/script'; import Text from '../../atoms/text/Text'; import Avatar from '../../atoms/avatar/Avatar'; -function PeopleSelector({ - avatarSrc, name, color, peopleRole, onClick, -}) { +function PeopleSelector({ avatarSrc, name, color, peopleRole, onClick }) { return (

); diff --git a/src/app/molecules/popup-window/PopupWindow.jsx b/src/app/molecules/popup-window/PopupWindow.jsx index 4179f49a8..55872d6ae 100644 --- a/src/app/molecules/popup-window/PopupWindow.jsx +++ b/src/app/molecules/popup-window/PopupWindow.jsx @@ -2,8 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import './PopupWindow.scss'; -import { twemojify } from '../../../util/twemojify'; - import Text from '../../atoms/text/Text'; import IconButton from '../../atoms/button/IconButton'; import { MenuItem } from '../../atoms/context-menu/ContextMenu'; @@ -13,19 +11,11 @@ import RawModal from '../../atoms/modal/RawModal'; import ChevronLeftIC from '../../../../public/res/ic/outlined/chevron-left.svg'; -function PWContentSelector({ - selected, variant, iconSrc, - type, onClick, children, -}) { +function PWContentSelector({ selected, variant, iconSrc, type, onClick, children }) { const pwcsClass = selected ? ' pw-content-selector--selected' : ''; return (
- + {children}
@@ -49,9 +39,16 @@ PWContentSelector.propTypes = { }; function PopupWindow({ - className, isOpen, title, contentTitle, - drawer, drawerOptions, contentOptions, - onAfterClose, onRequestClose, children, + className, + isOpen, + title, + contentTitle, + drawer, + drawerOptions, + contentOptions, + onAfterClose, + onRequestClose, + children, }) { const haveDrawer = drawer !== null; const cTitle = contentTitle !== null ? contentTitle : title; @@ -69,21 +66,26 @@ function PopupWindow({ {haveDrawer && (
- + - { - typeof title === 'string' - ? {twemojify(title)} - : title - } + {typeof title === 'string' ? ( + + {title} + + ) : ( + title + )} {drawerOptions}
-
- {drawer} -
+
{drawer}
@@ -91,19 +93,19 @@ function PopupWindow({
- { - typeof cTitle === 'string' - ? {twemojify(cTitle)} - : cTitle - } + {typeof cTitle === 'string' ? ( + + {cTitle} + + ) : ( + cTitle + )} {contentOptions}
-
- {children} -
+
{children}
diff --git a/src/app/molecules/room-notification/RoomNotification.jsx b/src/app/molecules/room-notification/RoomNotification.jsx index 4adb1169e..821ea5084 100644 --- a/src/app/molecules/room-notification/RoomNotification.jsx +++ b/src/app/molecules/room-notification/RoomNotification.jsx @@ -13,28 +13,33 @@ import BellIC from '../../../../public/res/ic/outlined/bell.svg'; import BellRingIC from '../../../../public/res/ic/outlined/bell-ring.svg'; import BellPingIC from '../../../../public/res/ic/outlined/bell-ping.svg'; import BellOffIC from '../../../../public/res/ic/outlined/bell-off.svg'; - -const items = [{ - iconSrc: BellIC, - text: 'Global', - type: cons.notifs.DEFAULT, -}, { - iconSrc: BellRingIC, - text: 'All messages', - type: cons.notifs.ALL_MESSAGES, -}, { - iconSrc: BellPingIC, - text: 'Mentions & Keywords', - type: cons.notifs.MENTIONS_AND_KEYWORDS, -}, { - iconSrc: BellOffIC, - text: 'Mute', - type: cons.notifs.MUTE, -}]; +import { getNotificationType } from '../../utils/room'; + +const items = [ + { + iconSrc: BellIC, + text: 'Global', + type: cons.notifs.DEFAULT, + }, + { + iconSrc: BellRingIC, + text: 'All messages', + type: cons.notifs.ALL_MESSAGES, + }, + { + iconSrc: BellPingIC, + text: 'Mentions & Keywords', + type: cons.notifs.MENTIONS_AND_KEYWORDS, + }, + { + iconSrc: BellOffIC, + text: 'Mute', + type: cons.notifs.MUTE, + }, +]; function setRoomNotifType(roomId, newType) { const mx = initMatrix.matrixClient; - const { notifications } = initMatrix; let roomPushRule; try { roomPushRule = mx.getRoomPushRule('global', roomId); @@ -47,22 +52,22 @@ function setRoomNotifType(roomId, newType) { if (roomPushRule) { promises.push(mx.deletePushRule('global', 'room', roomPushRule.rule_id)); } - promises.push(mx.addPushRule('global', 'override', roomId, { - conditions: [ - { - kind: 'event_match', - key: 'room_id', - pattern: roomId, - }, - ], - actions: [ - 'dont_notify', - ], - })); + promises.push( + mx.addPushRule('global', 'override', roomId, { + conditions: [ + { + kind: 'event_match', + key: 'room_id', + pattern: roomId, + }, + ], + actions: ['dont_notify'], + }) + ); return promises; } - const oldState = notifications.getNotiType(roomId); + const oldState = getNotificationType(mx, roomId); if (oldState === cons.notifs.MUTE) { promises.push(mx.deletePushRule('global', 'override', roomId)); } @@ -75,25 +80,27 @@ function setRoomNotifType(roomId, newType) { } if (newType === cons.notifs.MENTIONS_AND_KEYWORDS) { - promises.push(mx.addPushRule('global', 'room', roomId, { - actions: [ - 'dont_notify', - ], - })); + promises.push( + mx.addPushRule('global', 'room', roomId, { + actions: ['dont_notify'], + }) + ); promises.push(mx.setPushRuleEnabled('global', 'room', roomId, true)); return Promise.all(promises); } // cons.notifs.ALL_MESSAGES - promises.push(mx.addPushRule('global', 'room', roomId, { - actions: [ - 'notify', - { - set_tweak: 'sound', - value: 'default', - }, - ], - })); + promises.push( + mx.addPushRule('global', 'room', roomId, { + actions: [ + 'notify', + { + set_tweak: 'sound', + value: 'default', + }, + ], + }) + ); promises.push(mx.setPushRuleEnabled('global', 'room', roomId, true)); @@ -101,17 +108,20 @@ function setRoomNotifType(roomId, newType) { } function useNotifications(roomId) { - const { notifications } = initMatrix; - const [activeType, setActiveType] = useState(notifications.getNotiType(roomId)); + const mx = initMatrix.matrixClient; + const [activeType, setActiveType] = useState(getNotificationType(mx, roomId)); useEffect(() => { - setActiveType(notifications.getNotiType(roomId)); - }, [roomId]); - - const setNotification = useCallback((item) => { - if (item.type === activeType.type) return; - setActiveType(item.type); - setRoomNotifType(roomId, item.type); - }, [activeType, roomId]); + setActiveType(getNotificationType(mx, roomId)); + }, [mx, roomId]); + + const setNotification = useCallback( + (item) => { + if (item.type === activeType.type) return; + setActiveType(item.type); + setRoomNotifType(roomId, item.type); + }, + [activeType, roomId] + ); return [activeType, setNotification]; } @@ -120,21 +130,19 @@ function RoomNotification({ roomId }) { return (
- { - items.map((item) => ( - setNotification(item)} - > - - {item.text} - - - - )) - } + {items.map((item) => ( + setNotification(item)} + > + + {item.text} + + + + ))}
); } diff --git a/src/app/molecules/room-options/RoomOptions.jsx b/src/app/molecules/room-options/RoomOptions.jsx deleted file mode 100644 index af18d7120..000000000 --- a/src/app/molecules/room-options/RoomOptions.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { twemojify } from '../../../util/twemojify'; - -import initMatrix from '../../../client/initMatrix'; -import { openInviteUser } from '../../../client/action/navigation'; -import * as roomActions from '../../../client/action/room'; -import { markAsRead } from '../../../client/action/notifications'; - -import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu'; -import RoomNotification from '../room-notification/RoomNotification'; - -import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; -import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg'; -import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg'; - -import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; - -function RoomOptions({ roomId, afterOptionSelect }) { - const mx = initMatrix.matrixClient; - const room = mx.getRoom(roomId); - const canInvite = room?.canInvite(mx.getUserId()); - - const handleMarkAsRead = () => { - markAsRead(roomId); - afterOptionSelect(); - }; - - const handleInviteClick = () => { - openInviteUser(roomId); - afterOptionSelect(); - }; - const handleLeaveClick = async () => { - afterOptionSelect(); - const isConfirmed = await confirmDialog( - 'Leave room', - `Are you sure that you want to leave "${room.name}" room?`, - 'Leave', - 'danger', - ); - if (!isConfirmed) return; - roomActions.leave(roomId); - }; - - return ( -
- {twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)} - Mark as read - - Invite - - Leave - Notification - -
- ); -} - -RoomOptions.defaultProps = { - afterOptionSelect: null, -}; - -RoomOptions.propTypes = { - roomId: PropTypes.string.isRequired, - afterOptionSelect: PropTypes.func, -}; - -export default RoomOptions; diff --git a/src/app/molecules/room-profile/RoomProfile.jsx b/src/app/molecules/room-profile/RoomProfile.jsx index 218119845..15273ebfd 100644 --- a/src/app/molecules/room-profile/RoomProfile.jsx +++ b/src/app/molecules/room-profile/RoomProfile.jsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; +import { useAtomValue } from 'jotai'; +import Linkify from 'linkify-react'; import './RoomProfile.scss'; -import { twemojify } from '../../../util/twemojify'; - import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import colorMXID from '../../../util/colorMXID'; @@ -20,6 +20,8 @@ import PencilIC from '../../../../public/res/ic/outlined/pencil.svg'; import { useStore } from '../../hooks/useStore'; import { useForceUpdate } from '../../hooks/useForceUpdate'; import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; +import { mDirectAtom } from '../../state/mDirectList'; +import { LINKIFY_OPTS } from '../../plugins/react-custom-html-parser'; function RoomProfile({ roomId }) { const isMountStore = useStore(); @@ -31,9 +33,12 @@ function RoomProfile({ roomId }) { }); const mx = initMatrix.matrixClient; - const isDM = initMatrix.roomList.directs.has(roomId); + const mDirects = useAtomValue(mDirectAtom); + const isDM = mDirects.has(roomId); let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop'); - avatarSrc = isDM ? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc; + avatarSrc = isDM + ? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') + : avatarSrc; const room = mx.getRoom(roomId); const { currentState } = room; const roomName = room.name; @@ -47,15 +52,14 @@ function RoomProfile({ roomId }) { useEffect(() => { isMountStore.setItem(true); - const { roomList } = initMatrix; - const handleProfileUpdate = (rId) => { - if (roomId !== rId) return; + const handleStateEvent = (mEvent) => { + if (mEvent.event.room_id !== roomId) return; forceUpdate(); }; - roomList.on(cons.events.roomList.ROOM_PROFILE_UPDATED, handleProfileUpdate); + mx.on('RoomState.events', handleStateEvent); return () => { - roomList.removeListener(cons.events.roomList.ROOM_PROFILE_UPDATED, handleProfileUpdate); + mx.removeListener('RoomState.events', handleStateEvent); isMountStore.setItem(false); setStatus({ msg: null, @@ -122,7 +126,7 @@ function RoomProfile({ roomId }) { 'Remove avatar', 'Are you sure that you want to remove room avatar?', 'Remove', - 'caution', + 'caution' ); if (isConfirmed) { await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, ''); @@ -132,15 +136,45 @@ function RoomProfile({ roomId }) { const renderEditNameAndTopic = () => (
- {canChangeName && } - {canChangeTopic && } - {(!canChangeName || !canChangeTopic) && {`You have permission to change ${room.isSpaceRoom() ? 'space' : 'room'} ${canChangeName ? 'name' : 'topic'} only.`}} - { status.type === cons.status.IN_FLIGHT && {status.msg}} - { status.type === cons.status.SUCCESS && {status.msg}} - { status.type === cons.status.ERROR && {status.msg}} - { status.type !== cons.status.IN_FLIGHT && ( + {canChangeName && ( + + )} + {canChangeTopic && ( + + )} + {(!canChangeName || !canChangeTopic) && ( + {`You have permission to change ${ + room.isSpaceRoom() ? 'space' : 'room' + } ${canChangeName ? 'name' : 'topic'} only.`} + )} + {status.type === cons.status.IN_FLIGHT && {status.msg}} + {status.type === cons.status.SUCCESS && ( + + {status.msg} + + )} + {status.type === cons.status.ERROR && ( + + {status.msg} + + )} + {status.type !== cons.status.IN_FLIGHT && (
- +
)} @@ -148,10 +182,15 @@ function RoomProfile({ roomId }) { ); const renderNameAndTopic = () => ( -
+
- {twemojify(roomName)} - { (canChangeName || canChangeTopic) && ( + + {roomName} + + {(canChangeName || canChangeTopic) && ( {room.getCanonicalAlias() || room.roomId} - {roomTopic && {twemojify(roomTopic, undefined, true)}} + {roomTopic && ( + + {roomTopic} + + )}
); return (
- { !canChangeAvatar && } - { canChangeAvatar && ( + {!canChangeAvatar && ( + + )} + {canChangeAvatar && ( { - mountStore.setItem(true) - }, [roomId]); - - useEffect(() => { - if (searchData?.results?.length > 0) { - roomIdToBackup.set(roomId, searchData); - } else { - roomIdToBackup.delete(roomId); - } - }, [searchData]); - - const search = async (term) => { - setSearchData(null); - if (term === '') { - setStatus({ type: cons.status.PRE_FLIGHT, term: null }); - return; - } - setStatus({ type: cons.status.IN_FLIGHT, term }); - const body = { - search_categories: { - room_events: { - search_term: term, - filter: { - limit: 10, - rooms: [roomId], - }, - order_by: 'recent', - event_context: { - before_limit: 0, - after_limit: 0, - include_profile: true, - }, - }, - }, - }; - try { - const res = await mx.search({ body }); - const data = mx.processRoomEventsSearch({ - _query: body, - results: [], - highlights: [], - }, res); - if (!mountStore.getItem()) return; - setStatus({ type: cons.status.SUCCESS, term }); - setSearchData(data); - if (!mountStore.getItem()) return; - } catch (error) { - setSearchData(null); - setStatus({ type: cons.status.ERROR, term }); - } - }; - - const paginate = async () => { - if (searchData === null) return; - const term = searchData._query.search_categories.room_events.search_term; - - setStatus({ type: cons.status.IN_FLIGHT, term }); - try { - const data = await mx.backPaginateRoomEventsSearch(searchData); - if (!mountStore.getItem()) return; - setStatus({ type: cons.status.SUCCESS, term }); - setSearchData(data); - } catch (error) { - if (!mountStore.getItem()) return; - setSearchData(null); - setStatus({ type: cons.status.ERROR, term }); - } - }; - - return [searchData, search, paginate, status]; -} - -function RoomSearch({ roomId }) { - const [searchData, search, paginate, status] = useRoomSearch(roomId); - const mx = initMatrix.matrixClient; - const isRoomEncrypted = mx.isRoomEncrypted(roomId); - const searchTerm = searchData?._query.search_categories.room_events.search_term ?? ''; - - const handleSearch = (e) => { - e.preventDefault(); - if (isRoomEncrypted) return; - const searchTermInput = e.target.elements['room-search-input']; - const term = searchTermInput.value.trim(); - - search(term); - }; - - const renderTimeline = (timeline) => ( -
- { timeline.map((mEvent) => { - const id = mEvent.getId(); - return ( - - - - - ); - })} -
- ); - - return ( -
- - Room search -
- - -
- {searchData?.results.length > 0 && ( - {`${searchData.count} results for "${searchTerm}"`} - )} - {!isRoomEncrypted && searchData === null && ( -
- {status.type === cons.status.IN_FLIGHT && } - {status.type === cons.status.IN_FLIGHT && Searching room messages...} - {status.type === cons.status.PRE_FLIGHT && } - {status.type === cons.status.PRE_FLIGHT && Search room messages} - {status.type === cons.status.ERROR && Failed to search messages} -
- )} - - {!isRoomEncrypted && searchData?.results.length === 0 && ( -
- No results found -
- )} - {isRoomEncrypted && ( -
- Search does not work in encrypted room -
- )} - - {searchData?.results.length > 0 && ( - <> -
- {searchData.results.map((searchResult) => { - const { timeline } = searchResult.context; - return renderTimeline(timeline); - })} -
- {searchData?.next_batch && ( -
- {status.type !== cons.status.IN_FLIGHT && ( - - )} - {status.type === cons.status.IN_FLIGHT && } -
- )} - - )} -
- ); -} - -RoomSearch.propTypes = { - roomId: PropTypes.string.isRequired, -}; - -export default RoomSearch; diff --git a/src/app/molecules/room-search/RoomSearch.scss b/src/app/molecules/room-search/RoomSearch.scss deleted file mode 100644 index a40945ef4..000000000 --- a/src/app/molecules/room-search/RoomSearch.scss +++ /dev/null @@ -1,62 +0,0 @@ -@use '../../partials/flex'; -@use '../../partials/dir'; - -.room-search { - &__form { - & div:nth-child(2) { - display: flex; - align-items: flex-end; - padding: var(--sp-normal);; - - & .input-container { - @extend .cp-fx__item-one; - @include dir.side(margin, 0, var(--sp-normal)); - } - & button { - height: 46px; - } - } - & .context-menu__header { - margin-bottom: 0; - } - & > .text { - padding: 0 var(--sp-normal) var(--sp-tight); - } - } - - &__help { - height: 248px; - @extend .cp-fx__column--c-c; - - & .ic-raw { - opacity: .5; - } - .text { - margin-top: var(--sp-normal); - } - } - &__more { - margin-bottom: var(--sp-normal); - @extend .cp-fx__row--c-c; - button { - width: 100%; - } - } - &__result-item { - padding: var(--sp-tight) var(--sp-normal); - display: flex; - align-items: flex-start; - - .message { - @include dir.side(margin, 0, var(--sp-normal)); - @extend .cp-fx__item-one; - padding: 0; - &:hover { - background-color: transparent; - } - & .message__time { - flex: 0; - } - } - } -} \ No newline at end of file diff --git a/src/app/molecules/room-selector/RoomSelector.jsx b/src/app/molecules/room-selector/RoomSelector.jsx index fa6daa9e9..f865c95d0 100644 --- a/src/app/molecules/room-selector/RoomSelector.jsx +++ b/src/app/molecules/room-selector/RoomSelector.jsx @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import './RoomSelector.scss'; -import { twemojify } from '../../../util/twemojify'; import colorMXID from '../../../util/colorMXID'; import Text from '../../atoms/text/Text'; @@ -11,8 +10,13 @@ import NotificationBadge from '../../atoms/badge/NotificationBadge'; import { blurOnBubbling } from '../../atoms/button/script'; function RoomSelectorWrapper({ - isSelected, isMuted, isUnread, onClick, - content, options, onContextMenu, + isSelected, + isMuted, + isUnread, + onClick, + content, + options, + onContextMenu, }) { const classes = ['room-selector']; if (isMuted) classes.push('room-selector--muted'); @@ -50,16 +54,26 @@ RoomSelectorWrapper.propTypes = { }; function RoomSelector({ - name, parentName, roomId, imageSrc, iconSrc, - isSelected, isMuted, isUnread, notificationCount, isAlert, - options, onClick, onContextMenu, + name, + parentName, + roomId, + imageSrc, + iconSrc, + isSelected, + isMuted, + isUnread, + notificationCount, + isAlert, + options, + onClick, + onContextMenu, }) { return ( - {twemojify(name)} + {name} {parentName && ( {' — '} - {twemojify(parentName)} + {parentName} )} - { isUnread && ( + {isUnread && ( )} - )} + } options={options} onClick={onClick} onContextMenu={onContextMenu} @@ -110,10 +124,7 @@ RoomSelector.propTypes = { isSelected: PropTypes.bool, isMuted: PropTypes.bool, isUnread: PropTypes.bool.isRequired, - notificationCount: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]).isRequired, + notificationCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, isAlert: PropTypes.bool.isRequired, options: PropTypes.node, onClick: PropTypes.func.isRequired, diff --git a/src/app/molecules/room-tile/RoomTile.jsx b/src/app/molecules/room-tile/RoomTile.jsx index 95481ecec..2e0a63f60 100644 --- a/src/app/molecules/room-tile/RoomTile.jsx +++ b/src/app/molecules/room-tile/RoomTile.jsx @@ -2,46 +2,35 @@ import React from 'react'; import PropTypes from 'prop-types'; import './RoomTile.scss'; -import { twemojify } from '../../../util/twemojify'; - import colorMXID from '../../../util/colorMXID'; import Text from '../../atoms/text/Text'; import Avatar from '../../atoms/avatar/Avatar'; -function RoomTile({ - avatarSrc, name, id, - inviterName, memberCount, desc, options, -}) { +function RoomTile({ avatarSrc, name, id, inviterName, memberCount, desc, options }) { return (
- +
- {twemojify(name)} + {name} - { - inviterName !== null - ? `Invited by ${inviterName} to ${id}${memberCount === null ? '' : ` • ${memberCount} members`}` - : id + (memberCount === null ? '' : ` • ${memberCount} members`) - } + {inviterName !== null + ? `Invited by ${inviterName} to ${id}${ + memberCount === null ? '' : ` • ${memberCount} members` + }` + : id + (memberCount === null ? '' : ` • ${memberCount} members`)} - { - desc !== null && (typeof desc === 'string') - ? {twemojify(desc, undefined, true)} - : desc - } + {desc !== null && typeof desc === 'string' ? ( + + {desc} + + ) : ( + desc + )}
- { options !== null && ( -
- {options} -
- )} + {options !== null &&
{options}
}
); } @@ -58,10 +47,7 @@ RoomTile.propTypes = { name: PropTypes.string.isRequired, id: PropTypes.string.isRequired, inviterName: PropTypes.string, - memberCount: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), + memberCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), desc: PropTypes.node, options: PropTypes.node, }; diff --git a/src/app/molecules/sidebar-avatar/SidebarAvatar.jsx b/src/app/molecules/sidebar-avatar/SidebarAvatar.jsx deleted file mode 100644 index bc8b7c8f2..000000000 --- a/src/app/molecules/sidebar-avatar/SidebarAvatar.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './SidebarAvatar.scss'; - -import { twemojify } from '../../../util/twemojify'; - -import Text from '../../atoms/text/Text'; -import Tooltip from '../../atoms/tooltip/Tooltip'; -import { blurOnBubbling } from '../../atoms/button/script'; - -const SidebarAvatar = React.forwardRef(({ - className, tooltip, active, onClick, - onContextMenu, avatar, notificationBadge, -}, ref) => { - const classes = ['sidebar-avatar']; - if (active) classes.push('sidebar-avatar--active'); - if (className) classes.push(className); - return ( - {twemojify(tooltip)}} - placement="right" - > - - - ); -}); -SidebarAvatar.defaultProps = { - className: null, - active: false, - onClick: null, - onContextMenu: null, - notificationBadge: null, -}; - -SidebarAvatar.propTypes = { - className: PropTypes.string, - tooltip: PropTypes.string.isRequired, - active: PropTypes.bool, - onClick: PropTypes.func, - onContextMenu: PropTypes.func, - avatar: PropTypes.node.isRequired, - notificationBadge: PropTypes.node, -}; - -export default SidebarAvatar; diff --git a/src/app/molecules/sidebar-avatar/SidebarAvatar.scss b/src/app/molecules/sidebar-avatar/SidebarAvatar.scss deleted file mode 100644 index d76dbc86d..000000000 --- a/src/app/molecules/sidebar-avatar/SidebarAvatar.scss +++ /dev/null @@ -1,64 +0,0 @@ -@use '../../partials/dir'; - -.sidebar-avatar { - position: relative; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - - & .notification-badge { - position: absolute; - @include dir.prop(left, unset, 0); - @include dir.prop(right, 0, unset); - top: 0; - box-shadow: 0 0 0 2px var(--bg-surface-low); - @include dir.prop(transform, translate(20%, -20%), translate(-20%, -20%)); - - margin: 0 !important; - } - & .avatar-container, - & .notification-badge { - transition: transform 200ms var(--fluid-push); - } - &:hover .avatar-container { - @include dir.prop(transform, translateX(4px), translateX(-4px)); - } - &:hover .notification-badge { - --ltr: translate(calc(20% + 4px), -20%); - --rtl: translate(calc(-20% - 4px), -20%); - @include dir.prop(transform, var(--ltr), var(--rtl)); - } - &:focus { - outline: none; - } - &:active .avatar-container { - box-shadow: var(--bs-surface-outline); - } - - &:hover::before, - &:focus::before, - &--active::before { - content: ""; - display: block; - position: absolute; - @include dir.prop(left, -11px, unset); - @include dir.prop(right, unset, -11px); - top: 50%; - transform: translateY(-50%); - - width: 3px; - height: 12px; - background-color: var(--tc-surface-high); - @include dir.prop(border-radius, 0 4px 4px 0, 4px 0 0 4px); - transition: height 200ms linear; - } - &--active:hover::before, - &--active:focus::before, - &--active::before { - height: 28px; - } - &--active .avatar-container { - background-color: var(--bg-surface); - } -} \ No newline at end of file diff --git a/src/app/molecules/space-add-existing/SpaceAddExisting.jsx b/src/app/molecules/space-add-existing/SpaceAddExisting.jsx index 547c0af5b..3895ac756 100644 --- a/src/app/molecules/space-add-existing/SpaceAddExisting.jsx +++ b/src/app/molecules/space-add-existing/SpaceAddExisting.jsx @@ -1,9 +1,8 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; +import { useAtomValue } from 'jotai'; import './SpaceAddExisting.scss'; -import { twemojify } from '../../../util/twemojify'; - import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; @@ -24,6 +23,9 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import SearchIC from '../../../../public/res/ic/outlined/search.svg'; import { useStore } from '../../hooks/useStore'; +import { roomToParentsAtom } from '../../state/room/roomToParents'; +import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList'; +import { allRoomsAtom } from '../../state/room-list/roomList'; function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) { const mountStore = useStore(roomId); @@ -33,7 +35,10 @@ function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) { const [selected, setSelected] = useState([]); const [searchIds, setSearchIds] = useState(null); const mx = initMatrix.matrixClient; - const { spaces, rooms, directs, roomIdToParents } = initMatrix.roomList; + const roomIdToParents = useAtomValue(roomToParentsAtom); + const spaces = useSpaces(mx, allRoomsAtom); + const rooms = useRooms(mx, allRoomsAtom); + const directs = useDirects(mx, allRoomsAtom); useEffect(() => { const roomIds = onlySpaces ? [...spaces] : [...rooms, ...directs]; @@ -217,7 +222,7 @@ function SpaceAddExisting() { className="space-add-existing" title={ - {room && twemojify(room.name)} + {room && room.name} {' '} — add existing {data?.spaces ? 'spaces' : 'rooms'} diff --git a/src/app/molecules/space-options/SpaceOptions.jsx b/src/app/molecules/space-options/SpaceOptions.jsx deleted file mode 100644 index 0c166c6a9..000000000 --- a/src/app/molecules/space-options/SpaceOptions.jsx +++ /dev/null @@ -1,128 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { twemojify } from '../../../util/twemojify'; - -import initMatrix from '../../../client/initMatrix'; -import { openSpaceSettings, openSpaceManage, openInviteUser } from '../../../client/action/navigation'; -import { markAsRead } from '../../../client/action/notifications'; -import { leave } from '../../../client/action/room'; -import { - createSpaceShortcut, - deleteSpaceShortcut, - categorizeSpace, - unCategorizeSpace, -} from '../../../client/action/accountData'; - -import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu'; - -import CategoryIC from '../../../../public/res/ic/outlined/category.svg'; -import CategoryFilledIC from '../../../../public/res/ic/filled/category.svg'; -import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; -import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg'; -import SettingsIC from '../../../../public/res/ic/outlined/settings.svg'; -import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg'; -import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg'; -import PinIC from '../../../../public/res/ic/outlined/pin.svg'; -import PinFilledIC from '../../../../public/res/ic/filled/pin.svg'; - -import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; - -function SpaceOptions({ roomId, afterOptionSelect }) { - const mx = initMatrix.matrixClient; - const { roomList } = initMatrix; - const room = mx.getRoom(roomId); - const canInvite = room?.canInvite(mx.getUserId()); - const isPinned = initMatrix.accountData.spaceShortcut.has(roomId); - const isCategorized = initMatrix.accountData.categorizedSpaces.has(roomId); - - const handleMarkAsRead = () => { - const spaceChildren = roomList.getCategorizedSpaces([roomId]); - spaceChildren?.forEach((childIds) => { - childIds?.forEach((childId) => { - markAsRead(childId); - }); - }); - afterOptionSelect(); - }; - const handleInviteClick = () => { - openInviteUser(roomId); - afterOptionSelect(); - }; - const handlePinClick = () => { - if (isPinned) deleteSpaceShortcut(roomId); - else createSpaceShortcut(roomId); - afterOptionSelect(); - }; - const handleCategorizeClick = () => { - if (isCategorized) unCategorizeSpace(roomId); - else categorizeSpace(roomId); - afterOptionSelect(); - }; - const handleSettingsClick = () => { - openSpaceSettings(roomId); - afterOptionSelect(); - }; - const handleManageRoom = () => { - openSpaceManage(roomId); - afterOptionSelect(); - }; - - const handleLeaveClick = async () => { - afterOptionSelect(); - const isConfirmed = await confirmDialog( - 'Leave space', - `Are you sure that you want to leave "${room.name}" space?`, - 'Leave', - 'danger', - ); - if (!isConfirmed) return; - leave(roomId); - }; - - return ( -
- {twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)} - Mark as read - - {isCategorized ? 'Uncategorize subspaces' : 'Categorize subspaces'} - - - {isPinned ? 'Unpin from sidebar' : 'Pin to sidebar'} - - - Invite - - Manage rooms - Settings - - Leave - -
- ); -} - -SpaceOptions.defaultProps = { - afterOptionSelect: null, -}; - -SpaceOptions.propTypes = { - roomId: PropTypes.string.isRequired, - afterOptionSelect: PropTypes.func, -}; - -export default SpaceOptions; diff --git a/src/app/molecules/sso-buttons/SSOButtons.jsx b/src/app/molecules/sso-buttons/SSOButtons.jsx deleted file mode 100644 index 0a653be91..000000000 --- a/src/app/molecules/sso-buttons/SSOButtons.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './SSOButtons.scss'; - -import { createTemporaryClient, startSsoLogin } from '../../../client/action/auth'; - -import Button from '../../atoms/button/Button'; - -function SSOButtons({ type, identityProviders, baseUrl }) { - const tempClient = createTemporaryClient(baseUrl); - function handleClick(id) { - startSsoLogin(baseUrl, type, id); - } - return ( -
- {identityProviders - .sort((idp, idp2) => { - if (typeof idp.icon !== 'string') return -1; - return idp.name.toLowerCase() > idp2.name.toLowerCase() ? 1 : -1; - }) - .map((idp) => ( - idp.icon - ? ( - - ) : - ))} -
- ); -} - -SSOButtons.propTypes = { - identityProviders: PropTypes.arrayOf( - PropTypes.shape({}), - ).isRequired, - baseUrl: PropTypes.string.isRequired, - type: PropTypes.oneOf(['sso', 'cas']).isRequired, -}; - -export default SSOButtons; diff --git a/src/app/molecules/sso-buttons/SSOButtons.scss b/src/app/molecules/sso-buttons/SSOButtons.scss deleted file mode 100644 index 065067042..000000000 --- a/src/app/molecules/sso-buttons/SSOButtons.scss +++ /dev/null @@ -1,25 +0,0 @@ -.sso-buttons { - display: flex; - justify-content: center; - flex-wrap: wrap; -} - -.sso-btn { - margin: var(--sp-tight); - display: inline-flex; - justify-content: center; - - cursor: pointer; - - &__img { - height: var(--av-small); - width: var(--av-small); - } - &__text-only { - margin-top: var(--sp-normal); - flex-basis: 100%; - & .text { - color: var(--tc-link); - } - } -} \ No newline at end of file diff --git a/src/app/organisms/create-room/CreateRoom.jsx b/src/app/organisms/create-room/CreateRoom.jsx index 15be02d29..ff00cca19 100644 --- a/src/app/organisms/create-room/CreateRoom.jsx +++ b/src/app/organisms/create-room/CreateRoom.jsx @@ -2,11 +2,10 @@ import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import './CreateRoom.scss'; -import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; -import { selectRoom, openReusableContextMenu } from '../../../client/action/navigation'; +import { openReusableContextMenu } from '../../../client/action/navigation'; import * as roomActions from '../../../client/action/room'; import { isRoomAliasAvailable, getIdServer } from '../../../util/matrixUtil'; import { getEventCords } from '../../../util/common'; @@ -32,12 +31,14 @@ import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg'; import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg'; import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; +import { useRoomNavigate } from '../../hooks/useRoomNavigate'; function CreateRoomContent({ isSpace, parentId, onRequestClose }) { const [joinRule, setJoinRule] = useState(parentId ? 'restricted' : 'invite'); const [isEncrypted, setIsEncrypted] = useState(true); const [isCreatingRoom, setIsCreatingRoom] = useState(false); const [creatingError, setCreatingError] = useState(null); + const { navigateRoom, navigateSpace } = useRoomNavigate(); const [isValidAddress, setIsValidAddress] = useState(null); const [addressValue, setAddressValue] = useState(undefined); @@ -48,25 +49,6 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) { const mx = initMatrix.matrixClient; const userHs = getIdServer(mx.getUserId()); - useEffect(() => { - const { roomList } = initMatrix; - const onCreated = (roomId) => { - setIsCreatingRoom(false); - setCreatingError(null); - setIsValidAddress(null); - setAddressValue(undefined); - - if (!mx.getRoom(roomId)?.isSpaceRoom()) { - selectRoom(roomId); - } - onRequestClose(); - }; - roomList.on(cons.events.roomList.ROOM_CREATED, onCreated); - return () => { - roomList.removeListener(cons.events.roomList.ROOM_CREATED, onCreated); - }; - }, []); - const handleSubmit = async (evt) => { evt.preventDefault(); const { target } = evt; @@ -87,16 +69,26 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) { const powerLevel = roleIndex === 1 ? 101 : undefined; try { - await roomActions.createRoom({ + const data = await roomActions.createRoom({ name, topic, joinRule, alias: roomAlias, - isEncrypted: (isSpace || joinRule === 'public') ? false : isEncrypted, + isEncrypted: isSpace || joinRule === 'public' ? false : isEncrypted, powerLevel, isSpace, parentId, }); + setIsCreatingRoom(false); + setCreatingError(null); + setIsValidAddress(null); + setAddressValue(undefined); + onRequestClose(); + if (isSpace) { + navigateSpace(data.room_id); + } else { + navigateRoom(data.room_id); + } } catch (e) { if (e.message === 'M_UNKNOWN: Invalid characters in room alias') { setCreatingError('ERROR: Invalid characters in address'); @@ -131,36 +123,35 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) { const joinRules = ['invite', 'restricted', 'public']; const joinRuleShortText = ['Private', 'Restricted', 'Public']; - const joinRuleText = ['Private (invite only)', 'Restricted (space member can join)', 'Public (anyone can join)']; + const joinRuleText = [ + 'Private (invite only)', + 'Restricted (space member can join)', + 'Public (anyone can join)', + ]; const jrRoomIC = [HashLockIC, HashIC, HashGlobeIC]; const jrSpaceIC = [SpaceLockIC, SpaceIC, SpaceGlobeIC]; const handleJoinRule = (evt) => { - openReusableContextMenu( - 'bottom', - getEventCords(evt, '.btn-surface'), - (closeMenu) => ( - <> - Visibility (who can join) - { - joinRules.map((rule) => ( - { closeMenu(); setJoinRule(rule); }} - disabled={!parentId && rule === 'restricted'} - > - { joinRuleText[joinRules.indexOf(rule)] } - - )) - } - - ), - ); + openReusableContextMenu('bottom', getEventCords(evt, '.btn-surface'), (closeMenu) => ( + <> + Visibility (who can join) + {joinRules.map((rule) => ( + { + closeMenu(); + setJoinRule(rule); + }} + disabled={!parentId && rule === 'restricted'} + > + {joinRuleText[joinRules.indexOf(rule)]} + + ))} + + )); }; return ( @@ -168,50 +159,64 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
{joinRuleShortText[joinRules.indexOf(joinRule)]} - )} - content={{`Select who can join this ${isSpace ? 'space' : 'room'}.`}} + } + content={ + {`Select who can join this ${isSpace ? 'space' : 'room'}.`} + } /> {joinRule === 'public' && (
- {isSpace ? 'Space address' : 'Room address'} + + {isSpace ? 'Space address' : 'Room address'} +
# {`:${userHs}`}
- {isValidAddress === false && {`#${addressValue}:${userHs} is already in use`}} + {isValidAddress === false && ( + + {`#${addressValue}:${userHs} is already in use`} + + )}
)} {!isSpace && joinRule !== 'public' && ( } - content={You can’t disable this later. Bridges & most bots won’t work yet.} + content={ + + You can’t disable this later. Bridges & most bots won’t work yet. + + } /> )} - )} - content={( + } + content={ Selecting Admin sets 100 power level whereas Founder sets 101. - )} + } />
@@ -231,7 +236,11 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) { {`Creating ${isSpace ? 'space' : 'room'}...`}
)} - {typeof creatingError === 'string' && {creatingError}} + {typeof creatingError === 'string' && ( + + {creatingError} + + )}
); @@ -275,27 +284,22 @@ function CreateRoom() { return ( - {parentId ? twemojify(room.name) : 'Home'} + {parentId ? room.name : 'Home'} {` — create ${isSpace ? 'space' : 'room'}`} - )} + } contentOptions={} onRequestClose={onRequestClose} > - { - create - ? ( - - ) :
- } + {create ? ( + + ) : ( +
+ )}
); } diff --git a/src/app/organisms/emoji-board/EmojiBoard.jsx b/src/app/organisms/emoji-board/EmojiBoard.jsx deleted file mode 100644 index 84c413064..000000000 --- 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/EmojiBoard.scss b/src/app/organisms/emoji-board/EmojiBoard.scss deleted file mode 100644 index 683026f0e..000000000 --- a/src/app/organisms/emoji-board/EmojiBoard.scss +++ /dev/null @@ -1,137 +0,0 @@ -@use '../../partials/flex'; -@use '../../partials/text'; -@use '../../partials/dir'; - -.emoji-board { - --emoji-board-height: 390px; - --emoji-board-width: 286px; - display: flex; - max-width: 90vw; - max-height: 90vh; - - &__content { - @extend .cp-fx__item-one; - @extend .cp-fx__column; - height: var(--emoji-board-height); - width: var(--emoji-board-width); - } - & > .scrollbar { - width: initial; - height: var(--emoji-board-height); - } - &__nav { - @extend .cp-fx__column; - justify-content: center; - - min-height: 100%; - padding: 4px 6px; - @include dir.side(border, none, 1px solid var(--bg-surface-border)); - - position: relative; - - & .ic-btn-surface { - opacity: 0.8; - } - } - &__nav-custom, - &__nav-twemoji { - @extend .cp-fx__column; - } - &__nav-twemoji { - background-color: var(--bg-surface); - position: sticky; - bottom: -70%; - z-index: 999; - } -} - -.emoji-board__content__search { - padding: var(--sp-extra-tight); - position: relative; - - & .ic-raw { - position: absolute; - @include dir.prop(left, var(--sp-normal), unset); - @include dir.prop(right, unset, var(--sp-normal)); - top: var(--sp-normal); - transform: translateY(1px); - } - - & .input-container { - & .input { - min-width: 100%; - width: 0; - padding: var(--sp-extra-tight) 36px; - border-radius: calc(var(--bo-radius) / 2); - } - } -} -.emoji-board__content__emojis { - @extend .cp-fx__item-one; - @extend .cp-fx__column; -} -.emoji-board__content__info { - margin: 0 var(--sp-extra-tight); - padding: var(--sp-tight) var(--sp-extra-tight); - border-top: 1px solid var(--bg-surface-border); - - display: flex; - align-items: center; - - & > div:first-child { - line-height: 0; - .emoji { - width: 32px; - height: 32px; - object-fit: contain; - } - } - & > p:last-child { - @extend .cp-fx__item-one; - @extend .cp-txt__ellipsis; - margin: 0 var(--sp-tight); - } -} - -.emoji-row { - display: flex; -} - -.emoji-group { - --emoji-padding: 6px; - position: relative; - margin-bottom: var(--sp-normal); - - &__header { - position: sticky; - top: 0; - z-index: 99; - background-color: var(--bg-surface); - - @include dir.side(margin, var(--sp-extra-tight), 0); - padding: var(--sp-extra-tight) var(--sp-ultra-tight); - text-transform: uppercase; - box-shadow: 0 -4px 0 0 var(--bg-surface); - border-bottom: 1px solid var(--bg-surface-border); - } - & .emoji-set { - --left-margin: calc(var(--sp-normal) - var(--emoji-padding)); - --right-margin: calc(var(--sp-extra-tight) - var(--emoji-padding)); - margin: var(--sp-extra-tight); - @include dir.side(margin, var(--left-margin), var(--right-margin)); - } - & .emoji { - max-width: 38px; - max-height: 38px; - width: 100%; - height: 100%; - overflow: hidden; - object-fit: contain; - padding: var(--emoji-padding); - cursor: pointer; - &:hover { - background-color: var(--bg-surface-hover); - border-radius: var(--bo-radius); - } - } -} \ No newline at end of file diff --git a/src/app/organisms/emoji-board/EmojiBoardOpener.jsx b/src/app/organisms/emoji-board/EmojiBoardOpener.jsx deleted file mode 100644 index 32b7a8370..000000000 --- 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/custom-emoji.js b/src/app/organisms/emoji-board/custom-emoji.js index 09b605228..4ca2088f7 100644 --- a/src/app/organisms/emoji-board/custom-emoji.js +++ b/src/app/organisms/emoji-board/custom-emoji.js @@ -1,8 +1,6 @@ -import { emojis } from './emoji'; - // https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md -class ImagePack { +export class ImagePack { static parsePack(eventId, packContent) { if (!eventId || typeof packContent?.images !== 'object') { return null; @@ -141,127 +139,4 @@ class ImagePack { } } -function getGlobalImagePacks(mx) { - const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent(); - if (typeof globalContent !== 'object') return []; - - const { rooms } = globalContent; - if (typeof rooms !== 'object') return []; - - const roomIds = Object.keys(rooms); - - const packs = roomIds.flatMap((roomId) => { - if (typeof rooms[roomId] !== 'object') return []; - const room = mx.getRoom(roomId); - if (!room) return []; - const stateKeys = Object.keys(rooms[roomId]); - - return stateKeys.map((stateKey) => { - const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey); - const pack = ImagePack.parsePack(data?.getId(), data?.getContent()); - if (pack) { - pack.displayName ??= room.name; - pack.avatarUrl ??= room.getMxcAvatarUrl(); - } - return pack; - }).filter((pack) => pack !== null); - }); - - return packs; -} - -function getUserImagePack(mx) { - const accountDataEmoji = mx.getAccountData('im.ponies.user_emotes'); - if (!accountDataEmoji) { - return null; - } - - const userImagePack = ImagePack.parsePack(mx.getUserId(), accountDataEmoji.event.content); - if (userImagePack) userImagePack.displayName ??= 'Personal Emoji'; - return userImagePack; -} - -function getRoomImagePacks(room) { - const dataEvents = room.currentState.getStateEvents('im.ponies.room_emotes'); - - return dataEvents - .map((data) => { - const pack = ImagePack.parsePack(data?.getId(), data?.getContent()); - if (pack) { - pack.displayName ??= room.name; - pack.avatarUrl ??= room.getMxcAvatarUrl(); - } - return pack; - }) - .filter((pack) => pack !== null); -} - -/** - * @param {MatrixClient} mx Provide if you want to include user personal/global pack - * @param {Room[]} rooms Provide rooms if you want to include rooms pack - * @returns {ImagePack[]} packs - */ -function getRelevantPacks(mx, rooms) { - const userPack = mx ? getUserImagePack(mx) : []; - const globalPacks = mx ? getGlobalImagePacks(mx) : []; - const globalPackIds = new Set(globalPacks.map((pack) => pack.id)); - const roomsPack = rooms?.flatMap(getRoomImagePacks) ?? []; - - return [].concat( - userPack ?? [], - globalPacks, - roomsPack.filter((pack) => !globalPackIds.has(pack.id)), - ); -} - -function getShortcodeToEmoji(mx, rooms) { - const allEmoji = new Map(); - - emojis.forEach((emoji) => { - if (Array.isArray(emoji.shortcodes)) { - emoji.shortcodes.forEach((shortcode) => { - allEmoji.set(shortcode, emoji); - }); - } else { - allEmoji.set(emoji.shortcodes, emoji); - } - }); - - getRelevantPacks(mx, rooms) - .flatMap((pack) => pack.getEmojis()) - .forEach((emoji) => { - allEmoji.set(emoji.shortcode, emoji); - }); - - return allEmoji; -} - -function getShortcodeToCustomEmoji(room) { - const allEmoji = new Map(); - - getRelevantPacks(room.client, [room]) - .flatMap((pack) => pack.getEmojis()) - .forEach((emoji) => { - allEmoji.set(emoji.shortcode, emoji); - }); - - return allEmoji; -} - -function getEmojiForCompletion(mx, rooms) { - const allEmoji = new Map(); - getRelevantPacks(mx, rooms) - .flatMap((pack) => pack.getEmojis()) - .forEach((emoji) => { - allEmoji.set(emoji.shortcode, emoji); - }); - - return Array.from(allEmoji.values()).concat(emojis.filter((e) => !allEmoji.has(e.shortcode))); -} -export { - ImagePack, - getUserImagePack, getGlobalImagePacks, getRoomImagePacks, - getShortcodeToEmoji, getShortcodeToCustomEmoji, - getRelevantPacks, getEmojiForCompletion, -}; diff --git a/src/app/organisms/emoji-board/emoji.js b/src/app/organisms/emoji-board/emoji.js deleted file mode 100644 index 3cbd0b881..000000000 --- a/src/app/organisms/emoji-board/emoji.js +++ /dev/null @@ -1,69 +0,0 @@ -import emojisData from 'emojibase-data/en/compact.json'; -import joypixels from 'emojibase-data/en/shortcodes/joypixels.json'; -import emojibase from 'emojibase-data/en/shortcodes/emojibase.json'; - -const emojiGroups = [{ - name: 'Smileys & people', - order: 0, - emojis: [], -}, { - name: 'Animals & nature', - order: 1, - emojis: [], -}, { - name: 'Food & drinks', - order: 2, - emojis: [], -}, { - name: 'Activity', - order: 3, - emojis: [], -}, { - name: 'Travel & places', - order: 4, - emojis: [], -}, { - name: 'Objects', - order: 5, - emojis: [], -}, { - name: 'Symbols', - order: 6, - emojis: [], -}, { - name: 'Flags', - order: 7, - emojis: [], -}]; -Object.freeze(emojiGroups); - -function addEmoji(emoji, order) { - emojiGroups[order].emojis.push(emoji); -} -function addToGroup(emoji) { - if (emoji.group === 0 || emoji.group === 1) addEmoji(emoji, 0); - else if (emoji.group === 3) addEmoji(emoji, 1); - else if (emoji.group === 4) addEmoji(emoji, 2); - else if (emoji.group === 6) addEmoji(emoji, 3); - else if (emoji.group === 5) addEmoji(emoji, 4); - else if (emoji.group === 7) addEmoji(emoji, 5); - else if (emoji.group === 8 || typeof emoji.group === 'undefined') addEmoji(emoji, 6); - else if (emoji.group === 9) addEmoji(emoji, 7); -} - -const emojis = []; -emojisData.forEach((emoji) => { - const myShortCodes = joypixels[emoji.hexcode] || emojibase[emoji.hexcode]; - if (!myShortCodes) return; - const em = { - ...emoji, - shortcode: Array.isArray(myShortCodes) ? myShortCodes[0] : myShortCodes, - shortcodes: myShortCodes, - }; - addToGroup(em); - emojis.push(em); -}); - -export { - emojis, emojiGroups, -}; diff --git a/src/app/organisms/emoji-board/recent.js b/src/app/organisms/emoji-board/recent.js deleted file mode 100644 index dff67fb0b..000000000 --- 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/emoji-verification/EmojiVerification.jsx b/src/app/organisms/emoji-verification/EmojiVerification.jsx index 3ae1f2948..1b543c052 100644 --- a/src/app/organisms/emoji-verification/EmojiVerification.jsx +++ b/src/app/organisms/emoji-verification/EmojiVerification.jsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import './EmojiVerification.scss'; -import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; @@ -30,8 +29,9 @@ function EmojiVerificationContent({ data, requestClose }) { const beginVerification = async () => { if ( - isCrossVerified(mx.deviceId) - && (mx.getCrossSigningId() === null || await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing') === false) + isCrossVerified(mx.deviceId) && + (mx.getCrossSigningId() === null || + (await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing')) === false) ) { if (!hasPrivateKey(getDefaultSSKey())) { const keyData = await accessSecretStorage('Emoji verification'); @@ -106,16 +106,20 @@ function EmojiVerificationContent({ data, requestClose }) { {sas.sas.emoji.map((emoji, i) => ( // eslint-disable-next-line react/no-array-index-key
- {twemojify(emoji[0])} + {emoji[0]} {emoji[1]}
))}
- {process ? renderWait() : ( + {process ? ( + renderWait() + ) : ( <> - - + + )}
@@ -127,9 +131,7 @@ function EmojiVerificationContent({ data, requestClose }) { return (
Please accept the request from other device. -
- {renderWait()} -
+
{renderWait()}
); } @@ -138,11 +140,13 @@ function EmojiVerificationContent({ data, requestClose }) {
Click accept to start the verification process.
- { - process - ? renderWait() - : - } + {process ? ( + renderWait() + ) : ( + + )}
); @@ -180,19 +184,19 @@ function EmojiVerification() { Emoji verification - )} + } contentOptions={} onRequestClose={requestClose} > - { - data !== null - ? - :
- } + {data !== null ? ( + + ) : ( +
+ )}
); } diff --git a/src/app/organisms/invite-list/InviteList.jsx b/src/app/organisms/invite-list/InviteList.jsx deleted file mode 100644 index 231928fee..000000000 --- a/src/app/organisms/invite-list/InviteList.jsx +++ /dev/null @@ -1,145 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import './InviteList.scss'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import * as roomActions from '../../../client/action/room'; -import { selectRoom, selectTab } from '../../../client/action/navigation'; - -import Text from '../../atoms/text/Text'; -import Button from '../../atoms/button/Button'; -import IconButton from '../../atoms/button/IconButton'; -import Spinner from '../../atoms/spinner/Spinner'; -import PopupWindow from '../../molecules/popup-window/PopupWindow'; -import RoomTile from '../../molecules/room-tile/RoomTile'; - -import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; - -function InviteList({ isOpen, onRequestClose }) { - const [procInvite, changeProcInvite] = useState(new Set()); - - function acceptInvite(roomId, isDM) { - procInvite.add(roomId); - changeProcInvite(new Set(Array.from(procInvite))); - roomActions.join(roomId, isDM); - } - function rejectInvite(roomId, isDM) { - procInvite.add(roomId); - changeProcInvite(new Set(Array.from(procInvite))); - roomActions.leave(roomId, isDM); - } - function updateInviteList(roomId) { - if (procInvite.has(roomId)) procInvite.delete(roomId); - changeProcInvite(new Set(Array.from(procInvite))); - - const rl = initMatrix.roomList; - const totalInvites = rl.inviteDirects.size + rl.inviteRooms.size + rl.inviteSpaces.size; - const room = initMatrix.matrixClient.getRoom(roomId); - const isRejected = room === null || room?.getMyMembership() !== 'join'; - if (!isRejected) { - if (room.isSpaceRoom()) selectTab(roomId); - else selectRoom(roomId); - onRequestClose(); - } - if (totalInvites === 0) onRequestClose(); - } - - useEffect(() => { - initMatrix.roomList.on(cons.events.roomList.INVITELIST_UPDATED, updateInviteList); - - return () => { - initMatrix.roomList.removeListener(cons.events.roomList.INVITELIST_UPDATED, updateInviteList); - }; - }, [procInvite]); - - function renderRoomTile(roomId) { - const mx = initMatrix.matrixClient; - const myRoom = mx.getRoom(roomId); - if (!myRoom) return null; - const roomName = myRoom.name; - let roomAlias = myRoom.getCanonicalAlias(); - if (!roomAlias) roomAlias = myRoom.roomId; - const inviterName = myRoom.getMember(mx.getUserId())?.events?.member?.getSender?.() ?? ''; - return ( - ) - : ( -
- - -
- ) - } - /> - ); - } - - return ( - } - onRequestClose={onRequestClose} - > -
- { initMatrix.roomList.inviteDirects.size !== 0 && ( -
- Direct Messages -
- )} - { - Array.from(initMatrix.roomList.inviteDirects).map((roomId) => { - const myRoom = initMatrix.matrixClient.getRoom(roomId); - if (myRoom === null) return null; - const roomName = myRoom.name; - return ( - ) - : ( -
- - -
- ) - } - /> - ); - }) - } - { initMatrix.roomList.inviteSpaces.size !== 0 && ( -
- Spaces -
- )} - { Array.from(initMatrix.roomList.inviteSpaces).map(renderRoomTile) } - - { initMatrix.roomList.inviteRooms.size !== 0 && ( -
- Rooms -
- )} - { Array.from(initMatrix.roomList.inviteRooms).map(renderRoomTile) } -
-
- ); -} - -InviteList.propTypes = { - isOpen: PropTypes.bool.isRequired, - onRequestClose: PropTypes.func.isRequired, -}; - -export default InviteList; diff --git a/src/app/organisms/invite-list/InviteList.scss b/src/app/organisms/invite-list/InviteList.scss deleted file mode 100644 index da1968c30..000000000 --- a/src/app/organisms/invite-list/InviteList.scss +++ /dev/null @@ -1,26 +0,0 @@ -@use '../../partials/dir'; - -.invites-content { - @include dir.side(margin, var(--sp-normal), var(--sp-extra-tight)); - - &__subheading { - margin-top: var(--sp-extra-loose); - - & .text { - text-transform: uppercase; - } - &:first-child { - margin-top: var(--sp-tight); - } - } - - & .room-tile { - margin-top: var(--sp-normal); - &__options { - align-self: flex-end; - } - } - & .invite-btn__container .btn-surface { - @include dir.side(margin, 0, var(--sp-normal)); - } -} \ No newline at end of file diff --git a/src/app/organisms/invite-user/InviteUser.jsx b/src/app/organisms/invite-user/InviteUser.jsx index 751951024..10f55f9f6 100644 --- a/src/app/organisms/invite-user/InviteUser.jsx +++ b/src/app/organisms/invite-user/InviteUser.jsx @@ -3,10 +3,8 @@ import PropTypes from 'prop-types'; import './InviteUser.scss'; import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; import * as roomActions from '../../../client/action/room'; -import { selectRoom } from '../../../client/action/navigation'; -import { hasDMWith, hasDevices } from '../../../util/matrixUtil'; +import { hasDevices } from '../../../util/matrixUtil'; import Text from '../../atoms/text/Text'; import Button from '../../atoms/button/Button'; @@ -18,10 +16,10 @@ import RoomTile from '../../molecules/room-tile/RoomTile'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import UserIC from '../../../../public/res/ic/outlined/user.svg'; +import { useRoomNavigate } from '../../hooks/useRoomNavigate'; +import { getDMRoomFor } from '../../utils/matrix'; -function InviteUser({ - isOpen, roomId, searchTerm, onRequestClose, -}) { +function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) { const [isSearching, updateIsSearching] = useState(false); const [searchQuery, updateSearchQuery] = useState({}); const [users, updateUsers] = useState([]); @@ -37,6 +35,7 @@ function InviteUser({ const usernameRef = useRef(null); const mx = initMatrix.matrixClient; + const { navigateRoom } = useRoomNavigate(); function getMapCopy(myMap) { const newMap = new Map(); @@ -76,11 +75,13 @@ function InviteUser({ if (isInputUserId) { try { const result = await mx.getProfileInfo(inputUsername); - updateUsers([{ - user_id: inputUsername, - display_name: result.displayname, - avatar_url: result.avatar_url, - }]); + updateUsers([ + { + user_id: inputUsername, + display_name: result.displayname, + avatar_url: result.avatar_url, + }, + ]); } catch (e) { updateSearchQuery({ error: `${inputUsername} not found!` }); } @@ -105,9 +106,9 @@ function InviteUser({ async function createDM(userId) { if (mx.getUserId() === userId) return; - const dmRoomId = hasDMWith(userId); + const dmRoomId = getDMRoomFor(mx, userId)?.roomId; if (dmRoomId) { - selectRoom(dmRoomId); + navigateRoom(dmRoomId); onRequestClose(); return; } @@ -120,6 +121,7 @@ function InviteUser({ const result = await roomActions.createDM(userId, await hasDevices(userId)); roomIdToUserId.set(result.room_id, userId); updateRoomIdToUserId(getMapCopy(roomIdToUserId)); + onDMCreated(result.room_id); } catch (e) { deleteUserFromProc(userId); if (typeof e.message === 'string') procUserError.set(userId, e.message); @@ -150,7 +152,13 @@ function InviteUser({ function renderUserList() { const renderOptions = (userId) => { - const messageJSX = (message, isPositive) => {message}; + const messageJSX = (message, isPositive) => ( + + + {message} + + + ); if (mx.getUserId() === userId) return null; if (procUsers.has(userId)) { @@ -158,7 +166,16 @@ function InviteUser({ } if (createdDM.has(userId)) { // eslint-disable-next-line max-len - return ; + return ( + + ); } if (invitedUserIds.has(userId)) { return messageJSX('Invited', true); @@ -178,13 +195,23 @@ function InviteUser({ } } } - return (typeof roomId === 'string') - ? - : ; + return typeof roomId === 'string' ? ( + + ) : ( + + ); }; const renderError = (userId) => { if (!procUserError.has(userId)) return null; - return {procUserError.get(userId)}; + return ( + + {procUserError.get(userId)} + + ); }; return users.map((user) => { @@ -193,7 +220,11 @@ function InviteUser({ return ( { - initMatrix.roomList.on(cons.events.roomList.ROOM_CREATED, onDMCreated); - return () => { - initMatrix.roomList.removeListener(cons.events.roomList.ROOM_CREATED, onDMCreated); - }; - }, [isOpen, procUsers, createdDM, roomIdToUserId]); - return ( } onRequestClose={onRequestClose} >
-
{ e.preventDefault(); searchUser(usernameRef.current.value); }}> + { + e.preventDefault(); + searchUser(usernameRef.current.value); + }} + > - +
- { - typeof searchQuery.username !== 'undefined' && isSearching && ( -
- - {`Searching for user "${searchQuery.username}"...`} -
- ) - } - { - typeof searchQuery.username !== 'undefined' && !isSearching && ( - {`Search result for user "${searchQuery.username}"`} - ) - } - { - searchQuery.error && {searchQuery.error} - } + {typeof searchQuery.username !== 'undefined' && isSearching && ( +
+ + {`Searching for user "${searchQuery.username}"...`} +
+ )} + {typeof searchQuery.username !== 'undefined' && !isSearching && ( + {`Search result for user "${searchQuery.username}"`} + )} + {searchQuery.error && ( + + {searchQuery.error} + + )}
- { users.length !== 0 && ( -
- {renderUserList()} -
- )} + {users.length !== 0 &&
{renderUserList()}
}
); diff --git a/src/app/organisms/join-alias/JoinAlias.jsx b/src/app/organisms/join-alias/JoinAlias.jsx index bc0a8adb7..9fa5542df 100644 --- a/src/app/organisms/join-alias/JoinAlias.jsx +++ b/src/app/organisms/join-alias/JoinAlias.jsx @@ -6,7 +6,6 @@ import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; import { join } from '../../../client/action/room'; -import { selectRoom, selectTab } from '../../../client/action/navigation'; import Text from '../../atoms/text/Text'; import IconButton from '../../atoms/button/IconButton'; @@ -18,36 +17,24 @@ import Dialog from '../../molecules/dialog/Dialog'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import { useStore } from '../../hooks/useStore'; +import { useRoomNavigate } from '../../hooks/useRoomNavigate'; const ALIAS_OR_ID_REG = /^[#|!].+:.+\..+$/; function JoinAliasContent({ term, requestClose }) { const [process, setProcess] = useState(false); const [error, setError] = useState(undefined); - const [lastJoinId, setLastJoinId] = useState(undefined); const mx = initMatrix.matrixClient; const mountStore = useStore(); + const { navigateRoom } = useRoomNavigate(); + const openRoom = (roomId) => { - const room = mx.getRoom(roomId); - if (!room) return; - if (room.isSpaceRoom()) selectTab(roomId); - else selectRoom(roomId); + navigateRoom(roomId); requestClose(); }; - useEffect(() => { - const handleJoin = (roomId) => { - if (lastJoinId !== roomId) return; - openRoom(roomId); - }; - initMatrix.roomList.on(cons.events.roomList.ROOM_JOINED, handleJoin); - return () => { - initMatrix.roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleJoin); - }; - }, [lastJoinId]); - const handleSubmit = async (e) => { e.preventDefault(); mountStore.setItem(true); @@ -70,13 +57,14 @@ function JoinAliasContent({ term, requestClose }) { } catch (err) { if (!mountStore.getItem()) return; setProcess(false); - setError(`Unable to find room/space with ${alias}. Either room/space is private or doesn't exist.`); + setError( + `Unable to find room/space with ${alias}. Either room/space is private or doesn't exist.` + ); } } try { const roomId = await join(alias, false, via); if (!mountStore.getItem()) return; - setLastJoinId(roomId); openRoom(roomId); } catch { if (!mountStore.getItem()) return; @@ -87,24 +75,23 @@ function JoinAliasContent({ term, requestClose }) { return (
- - {error && {error}} + + {error && ( + + {error} + + )}
- { - process - ? ( - <> - - {process} - - ) - : - } + {process ? ( + <> + + {process} + + ) : ( + + )}
); @@ -141,13 +128,15 @@ function JoinAlias() { return ( Join with address - )} + title={ + + Join with address + + } contentOptions={} onRequestClose={requestClose} > - { data ? :
} + {data ? :
}
); } diff --git a/src/app/organisms/navigation/Directs.jsx b/src/app/organisms/navigation/Directs.jsx deleted file mode 100644 index e65c8afcf..000000000 --- a/src/app/organisms/navigation/Directs.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import navigation from '../../../client/state/navigation'; -import Postie from '../../../util/Postie'; -import { roomIdByActivity } from '../../../util/sort'; - -import RoomsCategory from './RoomsCategory'; - -const drawerPostie = new Postie(); -function Directs({ size }) { - const mx = initMatrix.matrixClient; - const { roomList, notifications } = initMatrix; - const [directIds, setDirectIds] = useState([]); - - useEffect(() => setDirectIds([...roomList.directs].sort(roomIdByActivity)), [size]); - - useEffect(() => { - const handleTimeline = (event, room, toStartOfTimeline, removed, data) => { - if (!roomList.directs.has(room.roomId)) return; - if (!data.liveEvent) return; - if (directIds[0] === room.roomId) return; - const newDirectIds = [room.roomId]; - directIds.forEach((id) => { - if (id === room.roomId) return; - newDirectIds.push(id); - }); - setDirectIds(newDirectIds); - }; - mx.on('Room.timeline', handleTimeline); - return () => { - mx.removeListener('Room.timeline', handleTimeline); - }; - }, [directIds]); - - useEffect(() => { - const selectorChanged = (selectedRoomId, prevSelectedRoomId) => { - if (!drawerPostie.hasTopic('selector-change')) return; - const addresses = []; - if (drawerPostie.hasSubscriber('selector-change', selectedRoomId)) addresses.push(selectedRoomId); - if (drawerPostie.hasSubscriber('selector-change', prevSelectedRoomId)) addresses.push(prevSelectedRoomId); - if (addresses.length === 0) return; - drawerPostie.post('selector-change', addresses, selectedRoomId); - }; - - const notiChanged = (roomId, total, prevTotal) => { - if (total === prevTotal) return; - if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) { - drawerPostie.post('unread-change', roomId); - } - }; - - navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged); - notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged); - notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged); - return () => { - navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged); - notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged); - notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged); - }; - }, []); - - return ; -} -Directs.propTypes = { - size: PropTypes.number.isRequired, -}; - -export default Directs; diff --git a/src/app/organisms/navigation/Drawer.jsx b/src/app/organisms/navigation/Drawer.jsx deleted file mode 100644 index 0795e4692..000000000 --- a/src/app/organisms/navigation/Drawer.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import './Drawer.scss'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import navigation from '../../../client/state/navigation'; - -import Text from '../../atoms/text/Text'; -import ScrollView from '../../atoms/scroll/ScrollView'; - -import DrawerHeader from './DrawerHeader'; -import DrawerBreadcrumb from './DrawerBreadcrumb'; -import Home from './Home'; -import Directs from './Directs'; - -import { useForceUpdate } from '../../hooks/useForceUpdate'; -import { useSelectedTab } from '../../hooks/useSelectedTab'; -import { useSelectedSpace } from '../../hooks/useSelectedSpace'; - -function useSystemState() { - const [systemState, setSystemState] = useState(null); - - useEffect(() => { - const handleSystemState = (state) => { - if (state === 'ERROR' || state === 'RECONNECTING' || state === 'STOPPED') { - setSystemState({ status: 'Connection lost!' }); - } - if (systemState !== null) setSystemState(null); - }; - initMatrix.matrixClient.on('sync', handleSystemState); - return () => { - initMatrix.matrixClient.removeListener('sync', handleSystemState); - }; - }, [systemState]); - - return [systemState]; -} - -function Drawer() { - const [systemState] = useSystemState(); - const [selectedTab] = useSelectedTab(); - const [spaceId] = useSelectedSpace(); - const [, forceUpdate] = useForceUpdate(); - const scrollRef = useRef(null); - const { roomList } = initMatrix; - - useEffect(() => { - const handleUpdate = () => { - forceUpdate(); - }; - roomList.on(cons.events.roomList.ROOMLIST_UPDATED, handleUpdate); - return () => { - roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, handleUpdate); - }; - }, []); - - useEffect(() => { - requestAnimationFrame(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = 0; - } - }); - }, [selectedTab]); - - return ( -
- -
- {navigation.selectedSpacePath.length > 1 && selectedTab !== cons.tabs.DIRECTS && ( - - )} -
- -
- {selectedTab !== cons.tabs.DIRECTS ? ( - - ) : ( - - )} -
-
-
-
- {systemState !== null && ( -
- {systemState.status} -
- )} -
- ); -} - -export default Drawer; diff --git a/src/app/organisms/navigation/Drawer.scss b/src/app/organisms/navigation/Drawer.scss deleted file mode 100644 index 4e54c5fa2..000000000 --- a/src/app/organisms/navigation/Drawer.scss +++ /dev/null @@ -1,56 +0,0 @@ -@use '../../partials/flex'; -@use '../../partials/dir'; - -.drawer { - @extend .cp-fx__column; - @extend .cp-fx__item-one; - min-width: 0; - @include dir.side(border, - none, - 1px solid var(--bg-surface-border), - ); - - & .header { - padding: var(--sp-extra-tight); - & > .header__title-wrapper { - @include dir.side(margin, var(--sp-ultra-tight), 0); - } - } - - &__content-wrapper { - @extend .cp-fx__item-one; - @extend .cp-fx__column; - } - - &__state { - padding: var(--sp-extra-tight); - border-top: 1px solid var(--bg-surface-border); - @extend .cp-fx__row--c-c; - - & .text { - color: var(--tc-danger-high); - } - } -} -.rooms__wrapper { - @extend .cp-fx__item-one; - position: relative; -} - -.rooms-container { - padding-bottom: var(--sp-extra-loose); - - &::before { - position: absolute; - top: 0; - z-index: 99; - content: ''; - display: inline-block; - width: 100%; - height: 8px; - background-image: linear-gradient( - to bottom, - var(--bg-surface-low), - var(--bg-surface-low-transparent)); - } -} diff --git a/src/app/organisms/navigation/DrawerBreadcrumb.jsx b/src/app/organisms/navigation/DrawerBreadcrumb.jsx deleted file mode 100644 index face349d4..000000000 --- a/src/app/organisms/navigation/DrawerBreadcrumb.jsx +++ /dev/null @@ -1,142 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import './DrawerBreadcrumb.scss'; - -import { twemojify } from '../../../util/twemojify'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import { selectTab, selectSpace } from '../../../client/action/navigation'; -import navigation from '../../../client/state/navigation'; -import { abbreviateNumber } from '../../../util/common'; - -import Text from '../../atoms/text/Text'; -import RawIcon from '../../atoms/system-icons/RawIcon'; -import Button from '../../atoms/button/Button'; -import ScrollView from '../../atoms/scroll/ScrollView'; -import NotificationBadge from '../../atoms/badge/NotificationBadge'; - -import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg'; - -function DrawerBreadcrumb({ spaceId }) { - const [, forceUpdate] = useState({}); - const scrollRef = useRef(null); - const { roomList, notifications, accountData } = initMatrix; - const mx = initMatrix.matrixClient; - const spacePath = navigation.selectedSpacePath; - - function onNotiChanged(roomId, total, prevTotal) { - if (total === prevTotal) return; - if (navigation.selectedSpacePath.includes(roomId)) { - forceUpdate({}); - } - if (navigation.selectedSpacePath[0] === cons.tabs.HOME) { - if (!roomList.isOrphan(roomId)) return; - if (roomList.directs.has(roomId)) return; - forceUpdate({}); - } - } - - useEffect(() => { - requestAnimationFrame(() => { - if (scrollRef?.current === null) return; - scrollRef.current.scrollLeft = scrollRef.current.scrollWidth; - }); - notifications.on(cons.events.notifications.NOTI_CHANGED, onNotiChanged); - return () => { - notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotiChanged); - }; - }, [spaceId]); - - function getHomeNotiExcept(childId) { - const orphans = roomList.getOrphans() - .filter((id) => (id !== childId)) - .filter((id) => !accountData.spaceShortcut.has(id)); - - let noti = null; - - orphans.forEach((roomId) => { - if (!notifications.hasNoti(roomId)) return; - if (noti === null) noti = { total: 0, highlight: 0 }; - const childNoti = notifications.getNoti(roomId); - noti.total += childNoti.total; - noti.highlight += childNoti.highlight; - }); - - return noti; - } - - function getNotiExcept(roomId, childId) { - if (!notifications.hasNoti(roomId)) return null; - - const noti = notifications.getNoti(roomId); - if (!notifications.hasNoti(childId)) return noti; - if (noti.from === null) return noti; - - const childNoti = notifications.getNoti(childId); - - let noOther = true; - let total = 0; - let highlight = 0; - noti.from.forEach((fromId) => { - if (childNoti.from.has(fromId)) return; - noOther = false; - const fromNoti = notifications.getNoti(fromId); - total += fromNoti.total; - highlight += fromNoti.highlight; - }); - - if (noOther) return null; - return { total, highlight }; - } - - return ( -
- -
- { - spacePath.map((id, index) => { - const noti = (id !== cons.tabs.HOME && index < spacePath.length) - ? getNotiExcept(id, (index === spacePath.length - 1) ? null : spacePath[index + 1]) - : getHomeNotiExcept((index === spacePath.length - 1) ? null : spacePath[index + 1]); - - return ( - - { index !== 0 && } - - - ); - }) - } -
-
- -
- ); -} - -DrawerBreadcrumb.defaultProps = { - spaceId: null, -}; - -DrawerBreadcrumb.propTypes = { - spaceId: PropTypes.string, -}; - -export default DrawerBreadcrumb; diff --git a/src/app/organisms/navigation/DrawerBreadcrumb.scss b/src/app/organisms/navigation/DrawerBreadcrumb.scss deleted file mode 100644 index 0b7bacceb..000000000 --- a/src/app/organisms/navigation/DrawerBreadcrumb.scss +++ /dev/null @@ -1,66 +0,0 @@ -@use '../../partials/text'; -@use '../../partials/dir'; - -.drawer-breadcrumb__wrapper { - height: var(--header-height); - position: relative; -} - -.drawer-breadcrumb { - display: flex; - align-items: center; - height: 100%; - margin: 0 var(--sp-extra-tight); - - &::before, - &::after { - flex-shrink: 0; - position: absolute; - right: 0; - z-index: 99; - - content: ''; - display: inline-block; - min-width: 8px; - width: 8px; - height: 100%; - background-image: linear-gradient( - to right, - var(--bg-surface-low-transparent), - var(--bg-surface-low) - ); - } - &::before { - left: 0; - right: unset; - background-image: linear-gradient( - to left, - var(--bg-surface-low-transparent), - var(--bg-surface-low) - ); - } - - & > * { - flex-shrink: 0; - } - - & > .btn-surface { - min-width: 0; - padding: var(--sp-extra-tight) 10px; - white-space: nowrap; - box-shadow: none; - & p { - @extend .cp-txt__ellipsis; - max-width: 86px; - } - - & .notification-badge { - @include dir.side(margin, var(--sp-extra-tight), 0); - } - } - - &__btn--selected { - box-shadow: var(--bs-surface-border) !important; - background-color: var(--bg-surface); - } -} \ No newline at end of file diff --git a/src/app/organisms/navigation/DrawerHeader.jsx b/src/app/organisms/navigation/DrawerHeader.jsx deleted file mode 100644 index e8782e384..000000000 --- a/src/app/organisms/navigation/DrawerHeader.jsx +++ /dev/null @@ -1,159 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './DrawerHeader.scss'; - -import { twemojify } from '../../../util/twemojify'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import { - openPublicRooms, openCreateRoom, openSpaceManage, openJoinAlias, - openSpaceAddExisting, openInviteUser, openReusableContextMenu, -} from '../../../client/action/navigation'; -import { getEventCords } from '../../../util/common'; - -import { blurOnBubbling } from '../../atoms/button/script'; - -import Text from '../../atoms/text/Text'; -import RawIcon from '../../atoms/system-icons/RawIcon'; -import Header, { TitleWrapper } from '../../atoms/header/Header'; -import IconButton from '../../atoms/button/IconButton'; -import { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu'; -import SpaceOptions from '../../molecules/space-options/SpaceOptions'; - -import PlusIC from '../../../../public/res/ic/outlined/plus.svg'; -import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg'; -import HashGlobeIC from '../../../../public/res/ic/outlined/hash-globe.svg'; -import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg'; -import SpacePlusIC from '../../../../public/res/ic/outlined/space-plus.svg'; -import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg'; - -export function HomeSpaceOptions({ spaceId, afterOptionSelect }) { - const mx = initMatrix.matrixClient; - const room = mx.getRoom(spaceId); - const canManage = room - ? room.currentState.maySendStateEvent('m.space.child', mx.getUserId()) - : true; - - return ( - <> - Add rooms or spaces - { afterOptionSelect(); openCreateRoom(true, spaceId); }} - disabled={!canManage} - > - Create new space - - { afterOptionSelect(); openCreateRoom(false, spaceId); }} - disabled={!canManage} - > - Create new room - - { !spaceId && ( - { afterOptionSelect(); openPublicRooms(); }} - > - Explore public rooms - - )} - { !spaceId && ( - { afterOptionSelect(); openJoinAlias(); }} - > - Join with address - - )} - { spaceId && ( - { afterOptionSelect(); openSpaceAddExisting(spaceId); }} - disabled={!canManage} - > - Add existing - - )} - { spaceId && ( - { afterOptionSelect(); openSpaceManage(spaceId); }} - iconSrc={HashSearchIC} - > - Manage rooms - - )} - - ); -} -HomeSpaceOptions.defaultProps = { - spaceId: null, -}; -HomeSpaceOptions.propTypes = { - spaceId: PropTypes.string, - afterOptionSelect: PropTypes.func.isRequired, -}; - -function DrawerHeader({ selectedTab, spaceId }) { - const mx = initMatrix.matrixClient; - const tabName = selectedTab !== cons.tabs.DIRECTS ? 'Home' : 'Direct messages'; - - const isDMTab = selectedTab === cons.tabs.DIRECTS; - const room = mx.getRoom(spaceId); - const spaceName = isDMTab ? null : (room?.name || null); - - const openSpaceOptions = (e) => { - e.preventDefault(); - openReusableContextMenu( - 'bottom', - getEventCords(e, '.header'), - (closeMenu) => , - ); - }; - - const openHomeSpaceOptions = (e) => { - e.preventDefault(); - openReusableContextMenu( - 'right', - getEventCords(e, '.ic-btn'), - (closeMenu) => , - ); - }; - - return ( -
- {spaceName ? ( - - ) : ( - - {tabName} - - )} - - { isDMTab && openInviteUser()} tooltip="Start DM" src={PlusIC} size="small" /> } - { !isDMTab && } -
- ); -} - -DrawerHeader.defaultProps = { - spaceId: null, -}; -DrawerHeader.propTypes = { - selectedTab: PropTypes.string.isRequired, - spaceId: PropTypes.string, -}; - -export default DrawerHeader; diff --git a/src/app/organisms/navigation/DrawerHeader.scss b/src/app/organisms/navigation/DrawerHeader.scss deleted file mode 100644 index 9ed17e4bd..000000000 --- a/src/app/organisms/navigation/DrawerHeader.scss +++ /dev/null @@ -1,28 +0,0 @@ -@use '../../partials/flex'; -@use '../../partials/dir'; - -.drawer-header__btn { - min-width: 0; - @extend .cp-fx__row--s-c; - @include dir.side(margin, 0, auto); - padding: var(--sp-ultra-tight); - border-radius: calc(var(--bo-radius) / 2); - cursor: pointer; - - & .header__title-wrapper { - @include dir.side(margin, 0, var(--sp-extra-tight)); - } - - @media (hover:hover) { - &:hover { - background-color: var(--bg-surface-hover); - box-shadow: var(--bs-surface-outline); - } - } - &:focus, - &:active { - background-color: var(--bg-surface-active); - box-shadow: var(--bs-surface-outline); - outline: none; - } -} \ No newline at end of file diff --git a/src/app/organisms/navigation/Home.jsx b/src/app/organisms/navigation/Home.jsx deleted file mode 100644 index 6bfa6c0dd..000000000 --- a/src/app/organisms/navigation/Home.jsx +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import navigation from '../../../client/state/navigation'; -import Postie from '../../../util/Postie'; -import { roomIdByActivity, roomIdByAtoZ } from '../../../util/sort'; - -import RoomsCategory from './RoomsCategory'; - -import { useCategorizedSpaces } from '../../hooks/useCategorizedSpaces'; - -const drawerPostie = new Postie(); -function Home({ spaceId }) { - const mx = initMatrix.matrixClient; - const { roomList, notifications, accountData } = initMatrix; - const { spaces, rooms, directs } = roomList; - useCategorizedSpaces(); - const isCategorized = accountData.categorizedSpaces.has(spaceId); - - let categories = null; - let spaceIds = []; - let roomIds = []; - let directIds = []; - - if (spaceId) { - const spaceChildIds = roomList.getSpaceChildren(spaceId) ?? []; - spaceIds = spaceChildIds.filter((roomId) => spaces.has(roomId)); - roomIds = spaceChildIds.filter((roomId) => rooms.has(roomId)); - directIds = spaceChildIds.filter((roomId) => directs.has(roomId)); - } else { - spaceIds = roomList.getOrphanSpaces().filter((id) => !accountData.spaceShortcut.has(id)); - roomIds = roomList.getOrphanRooms(); - } - - if (isCategorized) { - categories = roomList.getCategorizedSpaces(spaceIds); - categories.delete(spaceId); - } - - useEffect(() => { - const selectorChanged = (selectedRoomId, prevSelectedRoomId) => { - if (!drawerPostie.hasTopic('selector-change')) return; - const addresses = []; - if (drawerPostie.hasSubscriber('selector-change', selectedRoomId)) addresses.push(selectedRoomId); - if (drawerPostie.hasSubscriber('selector-change', prevSelectedRoomId)) addresses.push(prevSelectedRoomId); - if (addresses.length === 0) return; - drawerPostie.post('selector-change', addresses, selectedRoomId); - }; - - const notiChanged = (roomId, total, prevTotal) => { - if (total === prevTotal) return; - if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) { - drawerPostie.post('unread-change', roomId); - } - }; - - navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged); - notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged); - notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged); - return () => { - navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged); - notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged); - notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged); - }; - }, []); - - return ( - <> - { !isCategorized && spaceIds.length !== 0 && ( - - )} - - { roomIds.length !== 0 && ( - - )} - - { directIds.length !== 0 && ( - - )} - - { isCategorized && [...categories.keys()].sort(roomIdByAtoZ).map((catId) => { - const rms = []; - const dms = []; - categories.get(catId).forEach((id) => { - if (directs.has(id)) dms.push(id); - else rms.push(id); - }); - rms.sort(roomIdByAtoZ); - dms.sort(roomIdByActivity); - return ( - - ); - })} - - ); -} -Home.defaultProps = { - spaceId: null, -}; -Home.propTypes = { - spaceId: PropTypes.string, -}; - -export default Home; diff --git a/src/app/organisms/navigation/Navigation.jsx b/src/app/organisms/navigation/Navigation.jsx deleted file mode 100644 index 24bd1bd2d..000000000 --- a/src/app/organisms/navigation/Navigation.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import './Navigation.scss'; - -import SideBar from './SideBar'; -import Drawer from './Drawer'; - -function Navigation() { - return ( -
- - -
- ); -} - -export default Navigation; diff --git a/src/app/organisms/navigation/Navigation.scss b/src/app/organisms/navigation/Navigation.scss deleted file mode 100644 index 4a932c79d..000000000 --- a/src/app/organisms/navigation/Navigation.scss +++ /dev/null @@ -1,7 +0,0 @@ -.navigation { - width: 100%; - height: 100%; - background-color: var(--bg-surface-low); - - display: flex; -} \ No newline at end of file diff --git a/src/app/organisms/navigation/RoomsCategory.jsx b/src/app/organisms/navigation/RoomsCategory.jsx deleted file mode 100644 index b5666512d..000000000 --- a/src/app/organisms/navigation/RoomsCategory.jsx +++ /dev/null @@ -1,92 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import './RoomsCategory.scss'; - -import initMatrix from '../../../client/initMatrix'; -import { selectSpace, selectRoom, openReusableContextMenu } from '../../../client/action/navigation'; -import { getEventCords } from '../../../util/common'; - -import Text from '../../atoms/text/Text'; -import RawIcon from '../../atoms/system-icons/RawIcon'; -import IconButton from '../../atoms/button/IconButton'; -import Selector from './Selector'; -import SpaceOptions from '../../molecules/space-options/SpaceOptions'; -import { HomeSpaceOptions } from './DrawerHeader'; - -import PlusIC from '../../../../public/res/ic/outlined/plus.svg'; -import HorizontalMenuIC from '../../../../public/res/ic/outlined/horizontal-menu.svg'; -import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg'; -import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg'; - -function RoomsCategory({ - spaceId, name, hideHeader, roomIds, drawerPostie, -}) { - const { spaces, directs } = initMatrix.roomList; - const [isOpen, setIsOpen] = useState(true); - - const openSpaceOptions = (e) => { - e.preventDefault(); - openReusableContextMenu( - 'bottom', - getEventCords(e, '.header'), - (closeMenu) => , - ); - }; - - const openHomeSpaceOptions = (e) => { - e.preventDefault(); - openReusableContextMenu( - 'right', - getEventCords(e, '.ic-btn'), - (closeMenu) => , - ); - }; - - const renderSelector = (roomId) => { - const isSpace = spaces.has(roomId); - const isDM = directs.has(roomId); - - return ( - (isSpace ? selectSpace(roomId) : selectRoom(roomId))} - /> - ); - }; - - return ( -
- {!hideHeader && ( -
- - {spaceId && } - {spaceId && } -
- )} - {(isOpen || hideHeader) && ( -
- {roomIds.map(renderSelector)} -
- )} -
- ); -} -RoomsCategory.defaultProps = { - spaceId: null, - hideHeader: false, -}; -RoomsCategory.propTypes = { - spaceId: PropTypes.string, - name: PropTypes.string.isRequired, - hideHeader: PropTypes.bool, - roomIds: PropTypes.arrayOf(PropTypes.string).isRequired, - drawerPostie: PropTypes.shape({}).isRequired, -}; - -export default RoomsCategory; diff --git a/src/app/organisms/navigation/RoomsCategory.scss b/src/app/organisms/navigation/RoomsCategory.scss deleted file mode 100644 index 841290c5e..000000000 --- a/src/app/organisms/navigation/RoomsCategory.scss +++ /dev/null @@ -1,54 +0,0 @@ -@use '../../partials/flex'; -@use '../../partials/dir'; -@use '../../partials/text'; - -.room-category { - &__header, - &__toggle { - display: flex; - align-items: center; - } - &__header { - margin-top: var(--sp-extra-tight); - - & .ic-btn { - padding: var(--sp-ultra-tight); - border-radius: 4px; - @include dir.side(margin, 0, 5px); - & .ic-raw { - width: 16px; - height: 16px; - background-color: var(--ic-surface-low); - } - } - } - &__toggle { - @extend .cp-fx__item-one; - padding: var(--sp-extra-tight) var(--sp-tight); - cursor: pointer; - - & .ic-raw { - flex-shrink: 0; - width: 12px; - height: 12px; - background-color: var(--ic-surface-low); - @include dir.side(margin, 0, var(--sp-ultra-tight)); - } - & .text { - text-transform: uppercase; - @extend .cp-txt__ellipsis; - } - &:hover .text { - color: var(--tc-surface-normal); - } - } - - &__content:first-child { - margin-top: var(--sp-extra-tight); - } - - & .room-selector { - width: calc(100% - var(--sp-extra-tight)); - @include dir.side(margin, auto, 0); - } -} \ No newline at end of file diff --git a/src/app/organisms/navigation/Selector.jsx b/src/app/organisms/navigation/Selector.jsx deleted file mode 100644 index cb1086eaa..000000000 --- a/src/app/organisms/navigation/Selector.jsx +++ /dev/null @@ -1,93 +0,0 @@ -/* eslint-disable react/prop-types */ -import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import navigation from '../../../client/state/navigation'; -import { openReusableContextMenu } from '../../../client/action/navigation'; -import { getEventCords, abbreviateNumber } from '../../../util/common'; -import { joinRuleToIconSrc } from '../../../util/matrixUtil'; - -import IconButton from '../../atoms/button/IconButton'; -import RoomSelector from '../../molecules/room-selector/RoomSelector'; -import RoomOptions from '../../molecules/room-options/RoomOptions'; -import SpaceOptions from '../../molecules/space-options/SpaceOptions'; - -import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg'; - -import { useForceUpdate } from '../../hooks/useForceUpdate'; - -function Selector({ - roomId, isDM, drawerPostie, onClick, -}) { - const mx = initMatrix.matrixClient; - const noti = initMatrix.notifications; - const room = mx.getRoom(roomId); - - let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; - if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; - - const isMuted = noti.getNotiType(roomId) === cons.notifs.MUTE; - - const [, forceUpdate] = useForceUpdate(); - - useEffect(() => { - const unSub1 = drawerPostie.subscribe('selector-change', roomId, forceUpdate); - const unSub2 = drawerPostie.subscribe('unread-change', roomId, forceUpdate); - return () => { - unSub1(); - unSub2(); - }; - }, []); - - const openOptions = (e) => { - e.preventDefault(); - openReusableContextMenu( - 'right', - getEventCords(e, '.room-selector'), - room.isSpaceRoom() - ? (closeMenu) => - : (closeMenu) => , - ); - }; - - return ( - - )} - /> - ); -} - -Selector.defaultProps = { - isDM: true, -}; - -Selector.propTypes = { - roomId: PropTypes.string.isRequired, - isDM: PropTypes.bool, - drawerPostie: PropTypes.shape({}).isRequired, - onClick: PropTypes.func.isRequired, -}; - -export default Selector; diff --git a/src/app/organisms/navigation/SideBar.jsx b/src/app/organisms/navigation/SideBar.jsx deleted file mode 100644 index 531869656..000000000 --- a/src/app/organisms/navigation/SideBar.jsx +++ /dev/null @@ -1,390 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import './SideBar.scss'; - -import { DndProvider, useDrag, useDrop } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import colorMXID from '../../../util/colorMXID'; -import { - selectTab, openShortcutSpaces, openInviteList, - openSearch, openSettings, openReusableContextMenu, -} from '../../../client/action/navigation'; -import { moveSpaceShortcut } from '../../../client/action/accountData'; -import { abbreviateNumber, getEventCords } from '../../../util/common'; -import { isCrossVerified } from '../../../util/matrixUtil'; - -import Avatar from '../../atoms/avatar/Avatar'; -import NotificationBadge from '../../atoms/badge/NotificationBadge'; -import ScrollView from '../../atoms/scroll/ScrollView'; -import SidebarAvatar from '../../molecules/sidebar-avatar/SidebarAvatar'; -import SpaceOptions from '../../molecules/space-options/SpaceOptions'; - -import HomeIC from '../../../../public/res/ic/outlined/home.svg'; -import UserIC from '../../../../public/res/ic/outlined/user.svg'; -import AddPinIC from '../../../../public/res/ic/outlined/add-pin.svg'; -import SearchIC from '../../../../public/res/ic/outlined/search.svg'; -import InviteIC from '../../../../public/res/ic/outlined/invite.svg'; -import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg'; - -import { useSelectedTab } from '../../hooks/useSelectedTab'; -import { useDeviceList } from '../../hooks/useDeviceList'; - -import { tabText as settingTabText } from '../settings/Settings'; - -function useNotificationUpdate() { - const { notifications } = initMatrix; - const [, forceUpdate] = useState({}); - useEffect(() => { - function onNotificationChanged(roomId, total, prevTotal) { - if (total === prevTotal) return; - forceUpdate({}); - } - notifications.on(cons.events.notifications.NOTI_CHANGED, onNotificationChanged); - return () => { - notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotificationChanged); - }; - }, []); -} - -function ProfileAvatarMenu() { - const mx = initMatrix.matrixClient; - const [profile, setProfile] = useState({ - avatarUrl: null, - displayName: mx.getUser(mx.getUserId()).displayName, - }); - - useEffect(() => { - const user = mx.getUser(mx.getUserId()); - const setNewProfile = (avatarUrl, displayName) => setProfile({ - avatarUrl: avatarUrl || null, - displayName: displayName || profile.displayName, - }); - const onAvatarChange = (event, myUser) => { - setNewProfile(myUser.avatarUrl, myUser.displayName); - }; - mx.getProfileInfo(mx.getUserId()).then((info) => { - setNewProfile(info.avatar_url, info.displayname); - }); - user.on('User.avatarUrl', onAvatarChange); - return () => { - user.removeListener('User.avatarUrl', onAvatarChange); - }; - }, []); - - return ( - - )} - /> - ); -} - -function CrossSigninAlert() { - const deviceList = useDeviceList(); - const unverified = deviceList?.filter((device) => isCrossVerified(device.device_id) === false); - - if (!unverified?.length) return null; - - return ( - openSettings(settingTabText.SECURITY)} - avatar={} - /> - ); -} - -function FeaturedTab() { - const { roomList, accountData, notifications } = initMatrix; - const [selectedTab] = useSelectedTab(); - useNotificationUpdate(); - - function getHomeNoti() { - const orphans = roomList.getOrphans(); - let noti = null; - - orphans.forEach((roomId) => { - if (accountData.spaceShortcut.has(roomId)) return; - if (!notifications.hasNoti(roomId)) return; - if (noti === null) noti = { total: 0, highlight: 0 }; - const childNoti = notifications.getNoti(roomId); - noti.total += childNoti.total; - noti.highlight += childNoti.highlight; - }); - - return noti; - } - function getDMsNoti() { - if (roomList.directs.size === 0) return null; - let noti = null; - - [...roomList.directs].forEach((roomId) => { - if (!notifications.hasNoti(roomId)) return; - if (noti === null) noti = { total: 0, highlight: 0 }; - const childNoti = notifications.getNoti(roomId); - noti.total += childNoti.total; - noti.highlight += childNoti.highlight; - }); - - return noti; - } - - const dmsNoti = getDMsNoti(); - const homeNoti = getHomeNoti(); - - return ( - <> - selectTab(cons.tabs.HOME)} - avatar={} - notificationBadge={homeNoti ? ( - 0} - content={abbreviateNumber(homeNoti.total) || null} - /> - ) : null} - /> - selectTab(cons.tabs.DIRECTS)} - avatar={} - notificationBadge={dmsNoti ? ( - 0} - content={abbreviateNumber(dmsNoti.total) || null} - /> - ) : null} - /> - - ); -} - -function DraggableSpaceShortcut({ - isActive, spaceId, index, moveShortcut, onDrop, -}) { - const mx = initMatrix.matrixClient; - const { notifications } = initMatrix; - const room = mx.getRoom(spaceId); - const shortcutRef = useRef(null); - const avatarRef = useRef(null); - - const openSpaceOptions = (e, sId) => { - e.preventDefault(); - openReusableContextMenu( - 'right', - getEventCords(e, '.sidebar-avatar'), - (closeMenu) => , - ); - }; - - const [, drop] = useDrop({ - accept: 'SPACE_SHORTCUT', - collect(monitor) { - return { - handlerId: monitor.getHandlerId(), - }; - }, - drop(item) { - onDrop(item.index, item.spaceId); - }, - hover(item, monitor) { - if (!shortcutRef.current) return; - - const dragIndex = item.index; - const hoverIndex = index; - if (dragIndex === hoverIndex) return; - - const hoverBoundingRect = shortcutRef.current?.getBoundingClientRect(); - const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; - const clientOffset = monitor.getClientOffset(); - const hoverClientY = clientOffset.y - hoverBoundingRect.top; - - if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { - return; - } - if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { - return; - } - moveShortcut(dragIndex, hoverIndex); - // eslint-disable-next-line no-param-reassign - item.index = hoverIndex; - }, - }); - const [{ isDragging }, drag] = useDrag({ - type: 'SPACE_SHORTCUT', - item: () => ({ spaceId, index }), - collect: (monitor) => ({ - isDragging: monitor.isDragging(), - }), - }); - - drag(avatarRef); - drop(shortcutRef); - - if (shortcutRef.current) { - if (isDragging) shortcutRef.current.style.opacity = 0; - else shortcutRef.current.style.opacity = 1; - } - - return ( - selectTab(spaceId)} - onContextMenu={(e) => openSpaceOptions(e, spaceId)} - avatar={( - - )} - notificationBadge={notifications.hasNoti(spaceId) ? ( - 0} - content={abbreviateNumber(notifications.getTotalNoti(spaceId)) || null} - /> - ) : null} - /> - ); -} - -DraggableSpaceShortcut.propTypes = { - spaceId: PropTypes.string.isRequired, - isActive: PropTypes.bool.isRequired, - index: PropTypes.number.isRequired, - moveShortcut: PropTypes.func.isRequired, - onDrop: PropTypes.func.isRequired, -}; - -function SpaceShortcut() { - const { accountData } = initMatrix; - const [selectedTab] = useSelectedTab(); - useNotificationUpdate(); - const [spaceShortcut, setSpaceShortcut] = useState([...accountData.spaceShortcut]); - - useEffect(() => { - const handleShortcut = () => setSpaceShortcut([...accountData.spaceShortcut]); - accountData.on(cons.events.accountData.SPACE_SHORTCUT_UPDATED, handleShortcut); - return () => { - accountData.removeListener(cons.events.accountData.SPACE_SHORTCUT_UPDATED, handleShortcut); - }; - }, []); - - const moveShortcut = (dragIndex, hoverIndex) => { - const dragSpaceId = spaceShortcut[dragIndex]; - const newShortcuts = [...spaceShortcut]; - newShortcuts.splice(dragIndex, 1); - newShortcuts.splice(hoverIndex, 0, dragSpaceId); - setSpaceShortcut(newShortcuts); - }; - - const handleDrop = (dragIndex, dragSpaceId) => { - if ([...accountData.spaceShortcut][dragIndex] === dragSpaceId) return; - moveSpaceShortcut(dragSpaceId, dragIndex); - }; - - return ( - - { - spaceShortcut.map((shortcut, index) => ( - - )) - } - - ); -} - -function useTotalInvites() { - const { roomList } = initMatrix; - const totalInviteCount = () => roomList.inviteRooms.size - + roomList.inviteSpaces.size - + roomList.inviteDirects.size; - const [totalInvites, updateTotalInvites] = useState(totalInviteCount()); - - useEffect(() => { - const onInviteListChange = () => { - updateTotalInvites(totalInviteCount()); - }; - roomList.on(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange); - return () => { - roomList.removeListener(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange); - }; - }, []); - - return [totalInvites]; -} - -function SideBar() { - const [totalInvites] = useTotalInvites(); - - return ( -
-
- -
-
- -
-
-
- - openShortcutSpaces()} - avatar={} - /> -
-
- -
-
-
-
- openSearch()} - avatar={} - /> - { totalInvites !== 0 && ( - openInviteList()} - avatar={} - notificationBadge={} - /> - )} - - -
-
-
- ); -} - -export default SideBar; diff --git a/src/app/organisms/navigation/SideBar.scss b/src/app/organisms/navigation/SideBar.scss deleted file mode 100644 index 401947a41..000000000 --- a/src/app/organisms/navigation/SideBar.scss +++ /dev/null @@ -1,75 +0,0 @@ -@use '../../partials/flex'; -@use '../../partials/dir'; - -.sidebar { - @extend .cp-fx__column; - - width: var(--navigation-sidebar-width); - height: 100%; - background-color: var(--bg-surface-extra-low); - @include dir.side(border, none, 1px solid var(--bg-surface-border)); - - &__scrollable, - &__sticky { - width: 100%; - } - - &__scrollable { - @extend .cp-fx__item-one; - } -} - -.scrollable-content { - &::after { - content: ''; - display: block; - width: 100%; - height: 8px; - - background: transparent; - background-image: linear-gradient( - to top, - var(--bg-surface-extra-low), - var(--bg-surface-extra-low-transparent) - ); - position: sticky; - bottom: -1px; - left: 0; - } -} - -.featured-container, -.space-container, -.sticky-container { - @extend .cp-fx__column--c-c; - - padding: var(--sp-ultra-tight) 0; - - & > .sidebar-avatar, - & > .avatar-container { - margin: calc(var(--sp-tight) / 2) 0; - } -} -.sidebar-divider { - margin: auto; - width: 24px; - height: 1px; - background-color: var(--bg-surface-border); -} - -.sidebar__cross-signin-alert .avatar-container { - box-shadow: var(--bs-danger-border); - animation-name: pushRight; - animation-duration: 400ms; - animation-iteration-count: 30; - animation-direction: alternate; -} - -@keyframes pushRight { - from { - transform: translateX(4px) scale(1); - } - to { - transform: translateX(0) scale(1); - } -} diff --git a/src/app/organisms/profile-editor/ProfileEditor.jsx b/src/app/organisms/profile-editor/ProfileEditor.jsx index bb7359da3..c21c82fa4 100644 --- a/src/app/organisms/profile-editor/ProfileEditor.jsx +++ b/src/app/organisms/profile-editor/ProfileEditor.jsx @@ -1,6 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; -import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; import colorMXID from '../../../util/colorMXID'; @@ -22,7 +21,9 @@ function ProfileEditor({ userId }) { const user = mx.getUser(mx.getUserId()); const displayNameRef = useRef(null); - const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null); + const [avatarSrc, setAvatarSrc] = useState( + user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null + ); const [username, setUsername] = useState(user.displayName); const [disabled, setDisabled] = useState(true); @@ -44,7 +45,7 @@ function ProfileEditor({ userId }) { 'Remove avatar', 'Are you sure that you want to remove avatar?', 'Remove', - 'caution', + 'caution' ); if (isConfirmed) { mx.setAvatarUrl(''); @@ -79,7 +80,10 @@ function ProfileEditor({ userId }) {
{ e.preventDefault(); saveDisplayName(); }} + onSubmit={(e) => { + e.preventDefault(); + saveDisplayName(); + }} > - +
); @@ -95,7 +101,9 @@ function ProfileEditor({ userId }) { const renderInfo = () => (
- {twemojify(username) ?? userId} + + {username ?? userId} + handleAvatarUpload(null)} /> - { - isEditing ? renderForm() : renderInfo() - } + {isEditing ? renderForm() : renderInfo()}
); } diff --git a/src/app/organisms/profile-viewer/ProfileViewer.jsx b/src/app/organisms/profile-viewer/ProfileViewer.jsx index b6ce426e0..b19c9c867 100644 --- a/src/app/organisms/profile-viewer/ProfileViewer.jsx +++ b/src/app/organisms/profile-viewer/ProfileViewer.jsx @@ -2,16 +2,17 @@ import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import './ProfileViewer.scss'; -import { twemojify } from '../../../util/twemojify'; - import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; -import { selectRoom, openReusableContextMenu } from '../../../client/action/navigation'; +import { openReusableContextMenu } from '../../../client/action/navigation'; import * as roomActions from '../../../client/action/room'; import { - getUsername, getUsernameOfRoomMember, getPowerLabel, hasDMWith, hasDevices, + getUsername, + getUsernameOfRoomMember, + getPowerLabel, + hasDevices, } from '../../../util/matrixUtil'; import { getEventCords } from '../../../util/common'; import colorMXID from '../../../util/colorMXID'; @@ -33,26 +34,24 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import { useForceUpdate } from '../../hooks/useForceUpdate'; import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; +import { useRoomNavigate } from '../../hooks/useRoomNavigate'; +import { getDMRoomFor } from '../../utils/matrix'; -function ModerationTools({ - roomId, userId, -}) { +function ModerationTools({ roomId, userId }) { const mx = initMatrix.matrixClient; const room = mx.getRoom(roomId); const roomMember = room.getMember(userId); const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0; const powerLevel = roomMember?.powerLevel || 0; - const canIKick = ( - roomMember?.membership === 'join' - && room.currentState.hasSufficientPowerLevelFor('kick', myPowerLevel) - && powerLevel < myPowerLevel - ); - const canIBan = ( - ['join', 'leave'].includes(roomMember?.membership) - && room.currentState.hasSufficientPowerLevelFor('ban', myPowerLevel) - && powerLevel < myPowerLevel - ); + const canIKick = + roomMember?.membership === 'join' && + room.currentState.hasSufficientPowerLevelFor('kick', myPowerLevel) && + powerLevel < myPowerLevel; + const canIBan = + ['join', 'leave'].includes(roomMember?.membership) && + room.currentState.hasSufficientPowerLevelFor('ban', myPowerLevel) && + powerLevel < myPowerLevel; const handleKick = (e) => { e.preventDefault(); @@ -120,13 +119,14 @@ function SessionInfo({ userId }) {
{devices === null && Loading sessions...} {devices?.length === 0 && No session found.} - {devices !== null && (devices.map((device) => ( - - )))} + {devices !== null && + devices.map((device) => ( + + ))}
); } @@ -137,7 +137,11 @@ function SessionInfo({ userId }) { onClick={() => setIsVisible(!isVisible)} iconSrc={isVisible ? ChevronBottomIC : ChevronRightIC} > - {`View ${devices?.length > 0 ? `${devices.length} ${devices.length == 1 ? 'session' : 'sessions'}` : 'sessions'}`} + {`View ${ + devices?.length > 0 + ? `${devices.length} ${devices.length == 1 ? 'session' : 'sessions'}` + : 'sessions' + }`} {renderSessionChips()}
@@ -155,6 +159,7 @@ function ProfileFooter({ roomId, userId, onRequestClose }) { const isMountedRef = useRef(true); const mx = initMatrix.matrixClient; + const { navigateRoom } = useRoomNavigate(); const room = mx.getRoom(roomId); const member = room.getMember(userId); const isInvitable = member?.membership !== 'join' && member?.membership !== 'ban'; @@ -164,25 +169,18 @@ function ProfileFooter({ roomId, userId, onRequestClose }) { const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0; const userPL = room.getMember(userId)?.powerLevel || 0; - const canIKick = room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel; + const canIKick = + room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel; const isBanned = member?.membership === 'ban'; const onCreated = (dmRoomId) => { if (isMountedRef.current === false) return; setIsCreatingDM(false); - selectRoom(dmRoomId); + navigateRoom(dmRoomId); onRequestClose(); }; - useEffect(() => { - const { roomList } = initMatrix; - roomList.on(cons.events.roomList.ROOM_CREATED, onCreated); - return () => { - isMountedRef.current = false; - roomList.removeListener(cons.events.roomList.ROOM_CREATED, onCreated); - }; - }, []); useEffect(() => { setIsUserIgnored(initMatrix.matrixClient.isUserIgnored(userId)); setIsIgnoring(false); @@ -191,9 +189,9 @@ function ProfileFooter({ roomId, userId, onRequestClose }) { const openDM = async () => { // Check and open if user already have a DM with userId. - const dmRoomId = hasDMWith(userId); + const dmRoomId = getDMRoomFor(mx, userId)?.roomId; if (dmRoomId) { - selectRoom(dmRoomId); + navigateRoom(dmRoomId); onRequestClose(); return; } @@ -201,7 +199,8 @@ function ProfileFooter({ roomId, userId, onRequestClose }) { // Create new DM try { setIsCreatingDM(true); - await roomActions.createDM(userId, await hasDevices(userId)); + const result = await roomActions.createDM(userId, await hasDevices(userId)); + onCreated(result.room_id); } catch { if (isMountedRef.current === false) return; setIsCreatingDM(false); @@ -246,31 +245,19 @@ function ProfileFooter({ roomId, userId, onRequestClose }) { return (
- - { isBanned && canIKick && ( - )} - { (isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && ( - )}
); @@ -326,8 +311,8 @@ function useRerenderOnProfileChange(roomId, userId) { useEffect(() => { const handleProfileChange = (mEvent, member) => { if ( - mEvent.getRoomId() === roomId - && (member.userId === userId || member.userId === mx.getUserId()) + mEvent.getRoomId() === roomId && + (member.userId === userId || member.userId === mx.getUserId()) ) { forceUpdate(); } @@ -352,20 +337,22 @@ function ProfileViewer() { const roomMember = room.getMember(userId); const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(userId); const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl; - const avatarUrl = (avatarMxc && avatarMxc !== 'null') ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null; + const avatarUrl = + avatarMxc && avatarMxc !== 'null' ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null; const powerLevel = roomMember?.powerLevel || 0; const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0; - const canChangeRole = ( - room.currentState.maySendEvent('m.room.power_levels', mx.getUserId()) - && (powerLevel < myPowerLevel || userId === mx.getUserId()) - ); + const canChangeRole = + room.currentState.maySendEvent('m.room.power_levels', mx.getUserId()) && + (powerLevel < myPowerLevel || userId === mx.getUserId()); const handleChangePowerLevel = async (newPowerLevel) => { if (newPowerLevel === powerLevel) return; - const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?'; - const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?'; + const SHARED_POWER_MSG = + 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?'; + const DEMOTING_MYSELF_MSG = + 'You will not be able to undo this change as you are demoting yourself. Are you sure?'; const isSharedPower = newPowerLevel === myPowerLevel; const isDemotingMyself = userId === mx.getUserId(); @@ -374,7 +361,7 @@ function ProfileViewer() { 'Change power level', isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG, 'Change', - 'caution', + 'caution' ); if (!isConfirmed) return; roomActions.setPowerLevel(roomId, userId, newPowerLevel); @@ -384,20 +371,16 @@ function ProfileViewer() { }; const handlePowerSelector = (e) => { - openReusableContextMenu( - 'bottom', - getEventCords(e, '.btn-surface'), - (closeMenu) => ( - { - closeMenu(); - handleChangePowerLevel(pl); - }} - /> - ), - ); + openReusableContextMenu('bottom', getEventCords(e, '.btn-surface'), (closeMenu) => ( + { + closeMenu(); + handleChangePowerLevel(pl); + }} + /> + )); }; return ( @@ -405,8 +388,10 @@ function ProfileViewer() {
- {twemojify(username)} - {twemojify(userId)} + + {username} + + {userId}
Role @@ -420,7 +405,7 @@ function ProfileViewer() {
- { userId !== mx.getUserId() && ( + {userId !== mx.getUserId() && ( )}
diff --git a/src/app/organisms/public-rooms/PublicRooms.jsx b/src/app/organisms/public-rooms/PublicRooms.jsx deleted file mode 100644 index d1674c32f..000000000 --- a/src/app/organisms/public-rooms/PublicRooms.jsx +++ /dev/null @@ -1,295 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import './PublicRooms.scss'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import { selectRoom, selectTab } from '../../../client/action/navigation'; -import * as roomActions from '../../../client/action/room'; - -import Text from '../../atoms/text/Text'; -import Button from '../../atoms/button/Button'; -import IconButton from '../../atoms/button/IconButton'; -import Spinner from '../../atoms/spinner/Spinner'; -import Input from '../../atoms/input/Input'; -import PopupWindow from '../../molecules/popup-window/PopupWindow'; -import RoomTile from '../../molecules/room-tile/RoomTile'; - -import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; -import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg'; - -const SEARCH_LIMIT = 20; - -function TryJoinWithAlias({ alias, onRequestClose }) { - const [status, setStatus] = useState({ - isJoining: false, - error: null, - roomId: null, - tempRoomId: null, - }); - function handleOnRoomAdded(roomId) { - if (status.tempRoomId !== null && status.tempRoomId !== roomId) return; - setStatus({ - isJoining: false, error: null, roomId, tempRoomId: null, - }); - } - - useEffect(() => { - initMatrix.roomList.on(cons.events.roomList.ROOM_JOINED, handleOnRoomAdded); - return () => { - initMatrix.roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleOnRoomAdded); - }; - }, [status]); - - async function joinWithAlias() { - setStatus({ - isJoining: true, error: null, roomId: null, tempRoomId: null, - }); - try { - const roomId = await roomActions.join(alias, false); - setStatus({ - isJoining: true, error: null, roomId: null, tempRoomId: roomId, - }); - } catch (e) { - setStatus({ - isJoining: false, - error: `Unable to join ${alias}. Either room is private or doesn't exist.`, - roomId: null, - tempRoomId: null, - }); - } - } - - return ( -
- {status.roomId === null && !status.isJoining && status.error === null && ( - - )} - {status.isJoining && ( - <> - - {`Joining ${alias}...`} - - )} - {status.roomId !== null && ( - - )} - {status.error !== null && {status.error}} -
- ); -} - -TryJoinWithAlias.propTypes = { - alias: PropTypes.string.isRequired, - onRequestClose: PropTypes.func.isRequired, -}; - -function PublicRooms({ isOpen, searchTerm, onRequestClose }) { - const [isSearching, updateIsSearching] = useState(false); - const [isViewMore, updateIsViewMore] = useState(false); - const [publicRooms, updatePublicRooms] = useState([]); - const [nextBatch, updateNextBatch] = useState(undefined); - const [searchQuery, updateSearchQuery] = useState({}); - const [joiningRooms, updateJoiningRooms] = useState(new Set()); - - const roomNameRef = useRef(null); - const hsRef = useRef(null); - const userId = initMatrix.matrixClient.getUserId(); - - async function searchRooms(viewMore) { - let inputRoomName = roomNameRef?.current?.value || searchTerm; - let isInputAlias = false; - if (typeof inputRoomName === 'string') { - isInputAlias = inputRoomName[0] === '#' && inputRoomName.indexOf(':') > 1; - } - const hsFromAlias = (isInputAlias) ? inputRoomName.slice(inputRoomName.indexOf(':') + 1) : null; - let inputHs = hsFromAlias || hsRef?.current?.value; - - if (typeof inputHs !== 'string') inputHs = userId.slice(userId.indexOf(':') + 1); - if (typeof inputRoomName !== 'string') inputRoomName = ''; - - if (isSearching) return; - if (viewMore !== true - && inputRoomName === searchQuery.name - && inputHs === searchQuery.homeserver - ) return; - - updateSearchQuery({ - name: inputRoomName, - homeserver: inputHs, - }); - if (isViewMore !== viewMore) updateIsViewMore(viewMore); - updateIsSearching(true); - - try { - const result = await initMatrix.matrixClient.publicRooms({ - server: inputHs, - limit: SEARCH_LIMIT, - since: viewMore ? nextBatch : undefined, - include_all_networks: true, - filter: { - generic_search_term: inputRoomName, - }, - }); - - const totalRooms = viewMore ? publicRooms.concat(result.chunk) : result.chunk; - updatePublicRooms(totalRooms); - updateNextBatch(result.next_batch); - updateIsSearching(false); - updateIsViewMore(false); - if (totalRooms.length === 0) { - updateSearchQuery({ - error: inputRoomName === '' - ? `No public rooms on ${inputHs}` - : `No result found for "${inputRoomName}" on ${inputHs}`, - alias: isInputAlias ? inputRoomName : null, - }); - } - } catch (e) { - updatePublicRooms([]); - let err = 'Something went wrong!'; - if (e?.httpStatus >= 400 && e?.httpStatus < 500) { - err = e.message; - } - updateSearchQuery({ - error: err, - alias: isInputAlias ? inputRoomName : null, - }); - updateIsSearching(false); - updateNextBatch(undefined); - updateIsViewMore(false); - } - } - - useEffect(() => { - if (isOpen) searchRooms(); - }, [isOpen]); - - function handleOnRoomAdded(roomId) { - if (joiningRooms.has(roomId)) { - joiningRooms.delete(roomId); - updateJoiningRooms(new Set(Array.from(joiningRooms))); - } - } - useEffect(() => { - initMatrix.roomList.on(cons.events.roomList.ROOM_JOINED, handleOnRoomAdded); - return () => { - initMatrix.roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleOnRoomAdded); - }; - }, [joiningRooms]); - - function handleViewRoom(roomId) { - const room = initMatrix.matrixClient.getRoom(roomId); - if (room.isSpaceRoom()) selectTab(roomId); - else selectRoom(roomId); - onRequestClose(); - } - - function joinRoom(roomIdOrAlias) { - joiningRooms.add(roomIdOrAlias); - updateJoiningRooms(new Set(Array.from(joiningRooms))); - roomActions.join(roomIdOrAlias, false); - } - - function renderRoomList(rooms) { - return rooms.map((room) => { - const alias = typeof room.canonical_alias === 'string' ? room.canonical_alias : room.room_id; - const name = typeof room.name === 'string' ? room.name : alias; - const isJoined = initMatrix.matrixClient.getRoom(room.room_id)?.getMyMembership() === 'join'; - return ( - - {isJoined && } - {!isJoined && (joiningRooms.has(room.room_id) ? : )} - - )} - /> - ); - }); - } - - return ( - } - onRequestClose={onRequestClose} - > -
-
{ e.preventDefault(); searchRooms(); }}> -
- - -
- -
-
- { - typeof searchQuery.name !== 'undefined' && isSearching && ( - searchQuery.name === '' - ? ( -
- - {`Loading public rooms from ${searchQuery.homeserver}...`} -
- ) - : ( -
- - {`Searching for "${searchQuery.name}" on ${searchQuery.homeserver}...`} -
- ) - ) - } - { - typeof searchQuery.name !== 'undefined' && !isSearching && ( - searchQuery.name === '' - ? {`Public rooms on ${searchQuery.homeserver}.`} - : {`Search result for "${searchQuery.name}" on ${searchQuery.homeserver}.`} - ) - } - { searchQuery.error && ( - <> - {searchQuery.error} - {typeof searchQuery.alias === 'string' && ( - - )} - - )} -
- { publicRooms.length !== 0 && ( -
- { renderRoomList(publicRooms) } -
- )} - { publicRooms.length !== 0 && publicRooms.length % SEARCH_LIMIT === 0 && ( -
- { isViewMore !== true && ( - - )} - { isViewMore && } -
- )} -
-
- ); -} - -PublicRooms.defaultProps = { - searchTerm: undefined, -}; - -PublicRooms.propTypes = { - isOpen: PropTypes.bool.isRequired, - searchTerm: PropTypes.string, - onRequestClose: PropTypes.func.isRequired, -}; - -export default PublicRooms; diff --git a/src/app/organisms/public-rooms/PublicRooms.scss b/src/app/organisms/public-rooms/PublicRooms.scss deleted file mode 100644 index dc55c9473..000000000 --- a/src/app/organisms/public-rooms/PublicRooms.scss +++ /dev/null @@ -1,85 +0,0 @@ -@use '../../partials/dir'; - -.public-rooms { - @include dir.side(margin, var(--sp-normal), var(--sp-extra-tight)); - margin-top: var(--sp-extra-tight); - - &__form { - display: flex; - align-items: flex-end; - - & .btn-primary { - padding: { - top: 11px; - bottom: 11px; - } - } - } - &__input-wrapper { - flex: 1; - min-width: 0; - - display: flex; - @include dir.side(margin, 0, var(--sp-normal)); - - & > div:first-child { - flex: 1; - min-width: 0; - - & .input { - @include dir.prop(border-radius, - var(--bo-radius) 0 0 var(--bo-radius), - 0 var(--bo-radius) var(--bo-radius) 0, - ); - } - } - - & > div:last-child .input { - width: 120px; - @include dir.prop(border-left-width, 0, 1px); - @include dir.prop(border-right-width, 1px, 0); - @include dir.prop(border-radius, - 0 var(--bo-radius) var(--bo-radius) 0, - var(--bo-radius) 0 0 var(--bo-radius), - ); - } - } - - &__search-status { - margin-top: var(--sp-extra-loose); - margin-bottom: var(--sp-tight); - & .donut-spinner { - margin: 0 var(--sp-tight); - } - - .try-join-with-alias { - margin-top: var(--sp-normal); - } - } - &__search-error { - color: var(--bg-danger); - } - &__content { - border-top: 1px solid var(--bg-surface-border); - } - &__view-more { - margin-top: var(--sp-loose); - @include dir.side(margin, calc(var(--av-normal) + var(--sp-normal)), 0); - } - - & .room-tile { - margin-top: var(--sp-normal); - &__options { - align-self: flex-end; - } - } -} - -.try-join-with-alias { - display: flex; - align-items: center; - - & >.text:nth-child(2) { - margin: 0 var(--sp-normal); - } -} \ No newline at end of file diff --git a/src/app/organisms/pw/Dialogs.jsx b/src/app/organisms/pw/Dialogs.jsx index a51d07e14..cc77cf18e 100644 --- a/src/app/organisms/pw/Dialogs.jsx +++ b/src/app/organisms/pw/Dialogs.jsx @@ -1,11 +1,8 @@ import React from 'react'; -import ReadReceipts from '../read-receipts/ReadReceipts'; import ProfileViewer from '../profile-viewer/ProfileViewer'; -import ShortcutSpaces from '../shortcut-spaces/ShortcutSpaces'; import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExisting'; import Search from '../search/Search'; -import ViewSource from '../view-source/ViewSource'; import CreateRoom from '../create-room/CreateRoom'; import JoinAlias from '../join-alias/JoinAlias'; import EmojiVerification from '../emoji-verification/EmojiVerification'; @@ -15,10 +12,7 @@ import ReusableDialog from '../../molecules/dialog/ReusableDialog'; function Dialogs() { return ( <> - - - diff --git a/src/app/organisms/pw/Windows.jsx b/src/app/organisms/pw/Windows.jsx index 835b70334..3ee997695 100644 --- a/src/app/organisms/pw/Windows.jsx +++ b/src/app/organisms/pw/Windows.jsx @@ -3,35 +3,18 @@ import React, { useState, useEffect } from 'react'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; -import InviteList from '../invite-list/InviteList'; -import PublicRooms from '../public-rooms/PublicRooms'; import InviteUser from '../invite-user/InviteUser'; import Settings from '../settings/Settings'; import SpaceSettings from '../space-settings/SpaceSettings'; -import SpaceManage from '../space-manage/SpaceManage'; import RoomSettings from '../room/RoomSettings'; function Windows() { - const [isInviteList, changeInviteList] = useState(false); - const [publicRooms, changePublicRooms] = useState({ - isOpen: false, - searchTerm: undefined, - }); const [inviteUser, changeInviteUser] = useState({ isOpen: false, roomId: undefined, term: undefined, }); - function openInviteList() { - changeInviteList(true); - } - function openPublicRooms(searchTerm) { - changePublicRooms({ - isOpen: true, - searchTerm, - }); - } function openInviteUser(roomId, searchTerm) { changeInviteUser({ isOpen: true, @@ -41,24 +24,14 @@ function Windows() { } useEffect(() => { - navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList); - navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms); navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser); return () => { - navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList); - navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms); navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser); }; }, []); return ( <> - changeInviteList(false)} /> - changePublicRooms({ isOpen: false, searchTerm: undefined })} - /> - ); } diff --git a/src/app/organisms/read-receipts/ReadReceipts.jsx b/src/app/organisms/read-receipts/ReadReceipts.jsx deleted file mode 100644 index 1e648e0b6..000000000 --- a/src/app/organisms/read-receipts/ReadReceipts.jsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import navigation from '../../../client/state/navigation'; -import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil'; -import colorMXID from '../../../util/colorMXID'; - -import IconButton from '../../atoms/button/IconButton'; -import PeopleSelector from '../../molecules/people-selector/PeopleSelector'; -import Dialog from '../../molecules/dialog/Dialog'; - -import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; -import { openProfileViewer } from '../../../client/action/navigation'; - -function ReadReceipts() { - const [isOpen, setIsOpen] = useState(false); - const [readers, setReaders] = useState([]); - const [roomId, setRoomId] = useState(null); - - useEffect(() => { - const loadReadReceipts = (rId, userIds) => { - setReaders(userIds); - setRoomId(rId); - setIsOpen(true); - }; - navigation.on(cons.events.navigation.READRECEIPTS_OPENED, loadReadReceipts); - return () => { - navigation.removeListener(cons.events.navigation.READRECEIPTS_OPENED, loadReadReceipts); - }; - }, []); - - const handleAfterClose = () => { - setReaders([]); - setRoomId(null); - }; - - function renderPeople(userId) { - const room = initMatrix.matrixClient.getRoom(roomId); - const member = room.getMember(userId); - const getUserDisplayName = () => { - if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId)); - return getUsername(userId); - }; - return ( - { - setIsOpen(false); - openProfileViewer(userId, roomId); - }} - avatarSrc={member?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 24, 24, 'crop')} - name={getUserDisplayName(userId)} - color={colorMXID(userId)} - /> - ); - } - - return ( - setIsOpen(false)} - contentOptions={ setIsOpen(false)} tooltip="Close" />} - > -
- { - readers.map(renderPeople) - } -
-
- ); -} - -export default ReadReceipts; diff --git a/src/app/organisms/room/EventLimit.js b/src/app/organisms/room/EventLimit.js deleted file mode 100644 index de87da377..000000000 --- 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 8f983247a..000000000 --- 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/PeopleDrawer.scss b/src/app/organisms/room/PeopleDrawer.scss deleted file mode 100644 index cfc5f6c98..000000000 --- a/src/app/organisms/room/PeopleDrawer.scss +++ /dev/null @@ -1,93 +0,0 @@ -@use '../../partials/flex'; -@use '../../partials/dir'; - -.people-drawer { - @extend .cp-fx__column; - width: var(--people-drawer-width); - background-color: var(--bg-surface-low); - @include dir.side(border, 1px solid var(--bg-surface-border), none); - - &__member-count { - color: var(--tc-surface-low); - } - - &__content-wrapper { - @extend .cp-fx__item-one; - @extend .cp-fx__column; - } - - &__scrollable { - @extend .cp-fx__item-one; - } - - &__noresult { - padding: var(--sp-extra-tight) var(--sp-normal); - text-align: center; - } - - &__sticky { - & .people-search { - --search-input-height: 40px; - min-height: var(--search-input-height); - - margin: 0 var(--sp-extra-tight); - - position: relative; - bottom: var(--sp-normal); - display: flex; - align-items: center; - - & > .ic-raw, - & > .ic-btn { - position: absolute; - z-index: 99; - } - & > .ic-raw { - @include dir.prop(left, var(--sp-tight), unset); - @include dir.prop(right, unset, var(--sp-tight)); - } - & > .ic-btn { - @include dir.prop(right, 2px, unset); - @include dir.prop(left, unset, 2px); - } - & .input-container { - flex: 1; - } - & .input { - padding: 0 44px; - height: var(--search-input-height); - } - } - } -} - -.people-drawer__content { - padding-top: var(--sp-extra-tight); - padding-bottom: calc(2 * var(--sp-normal)); - - & .people-selector { - padding: var(--sp-extra-tight); - border-radius: var(--bo-radius); - &__container { - @include dir.side(margin, var(--sp-extra-tight), 0); - } - } - - & .segmented-controls { - display: flex; - margin-bottom: var(--sp-extra-tight); - @include dir.side(margin, var(--sp-extra-tight), 0); - } - & .segment-btn { - flex: 1; - padding: var(--sp-ultra-tight) 0; - } -} -.people-drawer__load-more { - padding: var(--sp-normal) 0 0; - @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight)); - - & .btn-surface { - width: 100%; - } -} \ No newline at end of file diff --git a/src/app/organisms/room/Room.scss b/src/app/organisms/room/Room.scss deleted file mode 100644 index 69f8f9dd3..000000000 --- a/src/app/organisms/room/Room.scss +++ /dev/null @@ -1,20 +0,0 @@ -@use '../../partials/flex'; -@use '../../partials/screen'; - -.room { - @extend .cp-fx__row; - height: 100%; - flex-grow: 1; - - &__content { - @extend .cp-fx__item-one; - position: relative; - overflow: hidden; - } -} - -.room .people-drawer { - @include screen.smallerThan(tabletBreakpoint) { - display: none; - } -} diff --git a/src/app/organisms/room/RoomSettings.jsx b/src/app/organisms/room/RoomSettings.jsx index 1e617ae7e..2b8f28e66 100644 --- a/src/app/organisms/room/RoomSettings.jsx +++ b/src/app/organisms/room/RoomSettings.jsx @@ -5,7 +5,6 @@ import './RoomSettings.scss'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; -import * as roomActions from '../../../client/action/room'; import Text from '../../atoms/text/Text'; import Tabs from '../../atoms/tabs/Tabs'; @@ -86,7 +85,7 @@ function GeneralSettings({ roomId }) { 'danger' ); if (!isConfirmed) return; - roomActions.leave(roomId); + mx.leave(roomId); }} iconSrc={LeaveArrowIC} > diff --git a/src/app/organisms/room/RoomView.scss b/src/app/organisms/room/RoomView.scss deleted file mode 100644 index c70c2b092..000000000 --- a/src/app/organisms/room/RoomView.scss +++ /dev/null @@ -1,46 +0,0 @@ -@use '../../partials/flex'; -@use '../../partials/screen'; -@use '../../partials/dir'; - -.room-view { - @extend .cp-fx__column; - background-color: var(--bg-surface); - height: 100%; - width: 100%; - position: absolute; - top: 0; - z-index: 999; - box-shadow: none; - - transition: transform 200ms var(--fluid-slide-down); - - &--dropped { - transform: translateY(calc(100% - var(--header-height))); - border-radius: var(--bo-radius) var(--bo-radius) 0 0; - box-shadow: var(--bs-popup); - } - - & .header { - @include screen.smallerThan(mobileBreakpoint) { - padding: 0 var(--sp-tight); - } - } - - &__content-wrapper { - @extend .cp-fx__item-one; - @extend .cp-fx__column; - } - - &__scrollable { - @extend .cp-fx__item-one; - position: relative; - } - - &__sticky { - position: relative; - background: var(--bg-surface); - } - &__editor { - padding: 0 var(--sp-normal); - } -} diff --git a/src/app/organisms/room/RoomViewCmdBar.jsx b/src/app/organisms/room/RoomViewCmdBar.jsx deleted file mode 100644 index 0d21123bd..000000000 --- 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 3f03fb06a..000000000 --- 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 5726fe119..000000000 --- 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/RoomViewContent.scss b/src/app/organisms/room/RoomViewContent.scss deleted file mode 100644 index 1afd187ef..000000000 --- a/src/app/organisms/room/RoomViewContent.scss +++ /dev/null @@ -1,30 +0,0 @@ -@use '../../partials/dir'; - -.room-view__content { - min-height: 100%; - display: flex; - flex-direction: column; - justify-content: flex-end; - - & .timeline__wrapper { - --typing-noti-height: 28px; - min-height: 0; - min-width: 0; - padding-bottom: var(--typing-noti-height); - - & .message, - & .ph-msg, - & .timeline-change { - @include dir.prop(border-radius, - 0 var(--bo-radius) var(--bo-radius) 0, - var(--bo-radius) 0 0 var(--bo-radius), - ); - } - - & > .divider { - margin: var(--sp-extra-tight); - @include dir.side(margin, var(--sp-normal), var(--sp-extra-tight)); - @include dir.side(padding, calc(var(--av-small) + var(--sp-tight)), 0); - } - } -} \ No newline at end of file diff --git a/src/app/organisms/room/RoomViewFloating.jsx b/src/app/organisms/room/RoomViewFloating.jsx deleted file mode 100644 index d027aff21..000000000 --- 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/RoomViewFloating.scss b/src/app/organisms/room/RoomViewFloating.scss deleted file mode 100644 index 75802175a..000000000 --- a/src/app/organisms/room/RoomViewFloating.scss +++ /dev/null @@ -1,125 +0,0 @@ - @use '../../partials/flex'; -@use '../../partials/text'; -@use '../../partials/dir'; - -.room-view { - &__typing { - display: flex; - padding: var(--sp-ultra-tight) var(--sp-normal); - background: var(--bg-surface); - transition: transform 200ms ease-in-out; - - & b { - color: var(--tc-surface-high); - } - - & .text { - @extend .cp-txt__ellipsis; - @extend .cp-fx__item-one; - - margin: 0 var(--sp-tight); - } - - &--open { - transform: translateY(-99%); - box-shadow: 0 4px 0 0 var(--bg-surface); - & .bouncing-loader { - & > *, - &::after, - &::before { - animation: bouncing-loader 0.6s infinite alternate; - } - } - } - } - - .bouncing-loader { - transform: translateY(2px); - margin: 0 calc(var(--sp-ultra-tight) / 2); - } - .bouncing-loader > div, - .bouncing-loader::before, - .bouncing-loader::after { - display: inline-block; - width: 8px; - height: 8px; - background: var(--tc-surface-high); - border-radius: 50%; - } - - - .bouncing-loader::before, - .bouncing-loader::after { - content: ""; - } - - .bouncing-loader > div { - margin: 0 4px; - } - - .bouncing-loader > div { - animation-delay: 0.2s; - } - - .bouncing-loader::after { - animation-delay: 0.4s; - } - - @keyframes bouncing-loader { - to { - opacity: 0.1; - transform: translate3d(0, -4px, 0); - } - } - - &__STB, - &__unread { - overflow: hidden; - background-color: var(--bg-surface-low); - border-radius: var(--bo-radius); - - & button { - justify-content: flex-start; - border-radius: 0; - box-shadow: none; - padding: 6px var(--sp-tight); - & .ic-raw { - width: 16px; - height: 16px; - } - } - } - - &__STB { - position: absolute; - @include dir.prop(left, 50%, unset); - @include dir.prop(right, unset, 50%); - bottom: 0; - box-shadow: var(--bs-surface-border); - transition: transform 200ms ease-in-out; - transform: translate(-50%, 100%); - - &--open { - transform: translate(-50%, -28px); - } - } - - &__unread { - position: absolute; - top: var(--sp-extra-tight); - @include dir.prop(left, var(--sp-normal), unset); - @include dir.prop(right, unset, var(--sp-normal)); - z-index: 999; - - display: none; - width: calc(100% - var(--sp-extra-loose)); - box-shadow: 0 0 2px 0 rgba(0, 0, 0, 20%); - - &--open { - display: flex; - } - & button:first-child { - @extend .cp-fx__item-one; - } - } -} \ No newline at end of file diff --git a/src/app/organisms/room/RoomViewHeader.jsx b/src/app/organisms/room/RoomViewHeader.jsx deleted file mode 100644 index 6571241eb..000000000 --- a/src/app/organisms/room/RoomViewHeader.jsx +++ /dev/null @@ -1,132 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import './RoomViewHeader.scss'; - -import { twemojify } from '../../../util/twemojify'; -import { blurOnBubbling } from '../../atoms/button/script'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import navigation from '../../../client/state/navigation'; -import { - toggleRoomSettings, - openReusableContextMenu, - openNavigation, -} from '../../../client/action/navigation'; -import colorMXID from '../../../util/colorMXID'; -import { getEventCords } from '../../../util/common'; - -import { tabText } from './RoomSettings'; -import Text from '../../atoms/text/Text'; -import RawIcon from '../../atoms/system-icons/RawIcon'; -import IconButton from '../../atoms/button/IconButton'; -import Header, { TitleWrapper } from '../../atoms/header/Header'; -import Avatar from '../../atoms/avatar/Avatar'; -import RoomOptions from '../../molecules/room-options/RoomOptions'; - -import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg'; -import SearchIC from '../../../../public/res/ic/outlined/search.svg'; -import UserIC from '../../../../public/res/ic/outlined/user.svg'; -import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg'; -import BackArrowIC from '../../../../public/res/ic/outlined/chevron-left.svg'; - -import { useForceUpdate } from '../../hooks/useForceUpdate'; -import { useSetSetting } from '../../state/hooks/settings'; -import { settingsAtom } from '../../state/settings'; - -function RoomViewHeader({ roomId }) { - const [, forceUpdate] = useForceUpdate(); - const mx = initMatrix.matrixClient; - const isDM = initMatrix.roomList.directs.has(roomId); - const room = mx.getRoom(roomId); - const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); - let avatarSrc = room.getAvatarUrl(mx.baseUrl, 36, 36, 'crop'); - avatarSrc = isDM - ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') - : avatarSrc; - const roomName = room.name; - - const roomHeaderBtnRef = useRef(null); - useEffect(() => { - const settingsToggle = (isVisibile) => { - const rawIcon = roomHeaderBtnRef.current.lastElementChild; - rawIcon.style.transform = isVisibile ? 'rotateX(180deg)' : 'rotateX(0deg)'; - }; - navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle); - return () => { - navigation.removeListener(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle); - }; - }, []); - - useEffect(() => { - const { roomList } = initMatrix; - const handleProfileUpdate = (rId) => { - if (roomId !== rId) return; - forceUpdate(); - }; - - roomList.on(cons.events.roomList.ROOM_PROFILE_UPDATED, handleProfileUpdate); - return () => { - roomList.removeListener(cons.events.roomList.ROOM_PROFILE_UPDATED, handleProfileUpdate); - }; - }, [roomId]); - - const openRoomOptions = (e) => { - openReusableContextMenu('bottom', getEventCords(e, '.ic-btn'), (closeMenu) => ( - - )); - }; - - return ( -
- openNavigation()} - /> - - {mx.isRoomEncrypted(roomId) === false && ( - toggleRoomSettings(tabText.SEARCH)} - tooltip="Search" - src={SearchIC} - /> - )} - { - setPeopleDrawer((t) => !t); - }} - tooltip="People" - src={UserIC} - /> - toggleRoomSettings(tabText.MEMBERS)} - tooltip="Members" - src={UserIC} - /> - -
- ); -} -RoomViewHeader.propTypes = { - roomId: PropTypes.string.isRequired, -}; - -export default RoomViewHeader; diff --git a/src/app/organisms/room/RoomViewHeader.scss b/src/app/organisms/room/RoomViewHeader.scss deleted file mode 100644 index fc19c064c..000000000 --- a/src/app/organisms/room/RoomViewHeader.scss +++ /dev/null @@ -1,47 +0,0 @@ -@use '../../partials/flex'; -@use '../../partials/dir'; -@use '../../partials/screen'; - -.room-header__btn { - min-width: 0; - @extend .cp-fx__row--s-c; - @include dir.side(margin, 0, auto); - border-radius: var(--bo-radius); - cursor: pointer; - - & .ic-raw { - @include dir.side(margin, 0, var(--sp-extra-tight)); - transition: transform 200ms ease-in-out; - } - @media (hover:hover) { - &:hover { - background-color: var(--bg-surface-hover); - box-shadow: var(--bs-surface-outline); - } - } - &:focus, - &:active { - background-color: var(--bg-surface-active); - box-shadow: var(--bs-surface-outline); - outline: none; - } -} - -.room-header__drawer-btn { - @include screen.smallerThan(tabletBreakpoint) { - display: none; - } -} -.room-header__members-btn { - @include screen.biggerThan(tabletBreakpoint) { - display: none; - } -} - -.room-header__back-btn { - @include dir.side(margin, 0, var(--sp-tight)); - - @include screen.biggerThan(mobileBreakpoint) { - display: none; - } -} diff --git a/src/app/organisms/room/RoomViewInput.jsx b/src/app/organisms/room/RoomViewInput.jsx deleted file mode 100644 index 3fb780a4d..000000000 --- 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/RoomViewInput.scss b/src/app/organisms/room/RoomViewInput.scss deleted file mode 100644 index 9fb7c4dea..000000000 --- a/src/app/organisms/room/RoomViewInput.scss +++ /dev/null @@ -1,108 +0,0 @@ -@use '../../partials/dir'; - -.room-input { - padding: var(--sp-extra-tight) calc(var(--sp-normal) - 2px); - display: flex; - min-height: 56px; - - &__alert { - margin: auto; - padding: 0 var(--sp-tight); - text-align: center; - } - - &__input-container { - flex: 1; - min-width: 0; - display: flex; - align-items: center; - - margin: 0 calc(var(--sp-tight) - 2px); - background-color: var(--bg-surface-low); - box-shadow: var(--bs-surface-border); - border-radius: var(--bo-radius); - - & > .ic-raw { - transform: scale(0.8); - margin: 0 var(--sp-extra-tight); - } - - & .scrollbar { - max-height: 50vh; - flex: 1; - - &:first-child { - @include dir.side(margin, var(--sp-tight), 0); - } - } - } - - &__textarea-wrapper { - min-height: 40px; - display: flex; - align-items: center; - - & textarea { - resize: none; - width: 100%; - min-width: 0; - min-height: 100%; - padding: var(--sp-ultra-tight) 0; - - &::placeholder { - color: var(--tc-surface-low); - } - &:focus { - outline: none; - } - } - } -} - -.room-attachment { - --side-spacing: calc(var(--sp-normal) + var(--av-small) + var(--sp-tight)); - display: flex; - align-items: center; - @include dir.side(margin, var(--side-spacing), 0); - margin-top: var(--sp-extra-tight); - line-height: 0; - - &__preview > img { - max-height: 40px; - border-radius: var(--bo-radius); - max-width: 150px; - } - &__icon { - padding: var(--sp-extra-tight); - background-color: var(--bg-surface-low); - box-shadow: var(--bs-surface-border); - border-radius: var(--bo-radius); - } - &__info { - flex: 1; - min-width: 0; - margin: 0 var(--sp-tight); - } - - &__option button { - transition: transform 200ms ease-in-out; - transform: translateY(-48px); - & .ic-raw { - transition: transform 200ms ease-in-out; - transform: rotate(45deg); - background-color: var(--bg-caution); - } - } -} - -.room-reply { - display: flex; - align-items: center; - background-color: var(--bg-surface-low); - border-bottom: 1px solid var(--bg-surface-border); - - & .ic-btn-surface { - @include dir.side(margin, 17px, 13px); - border-radius: 0; - } -} \ No newline at end of file diff --git a/src/app/organisms/room/TimelineScroll.js b/src/app/organisms/room/TimelineScroll.js deleted file mode 100644 index ccdc9a97c..000000000 --- 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 463f9d945..000000000 --- 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/commands.scss b/src/app/organisms/room/commands.scss deleted file mode 100644 index 628393781..000000000 --- a/src/app/organisms/room/commands.scss +++ /dev/null @@ -1,10 +0,0 @@ -.commands-dialog { - & > * { - padding: var(--sp-tight) var(--sp-normal); - border-bottom: 1px solid var(--bg-surface-border); - &:last-child { - border-bottom: none; - margin-bottom: var(--sp-extra-loose); - } - } -} \ No newline at end of file diff --git a/src/app/organisms/room/common.jsx b/src/app/organisms/room/common.jsx deleted file mode 100644 index 28974a85d..000000000 --- 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/search/Search.jsx b/src/app/organisms/search/Search.jsx index 66b685114..c9d1d991f 100644 --- a/src/app/organisms/search/Search.jsx +++ b/src/app/organisms/search/Search.jsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; +import { useAtomValue } from 'jotai'; import './Search.scss'; import initMatrix from '../../../client/initMatrix'; @@ -19,6 +20,11 @@ import RoomSelector from '../../molecules/room-selector/RoomSelector'; import SearchIC from '../../../../public/res/ic/outlined/search.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; +import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList'; +import { roomToUnreadAtom } from '../../state/room/roomToUnread'; +import { roomToParentsAtom } from '../../state/room/roomToParents'; +import { allRoomsAtom } from '../../state/room-list/roomList'; +import { mDirectAtom } from '../../state/mDirectList'; function useVisiblityToggle(setResult) { const [isOpen, setIsOpen] = useState(false); @@ -48,9 +54,8 @@ function useVisiblityToggle(setResult) { return [isOpen, requestClose]; } -function mapRoomIds(roomIds) { +function mapRoomIds(roomIds, directs, roomIdToParents) { const mx = initMatrix.matrixClient; - const { directs, roomIdToParents } = initMatrix.roomList; return roomIds.map((roomId) => { const room = mx.getRoom(roomId); @@ -62,7 +67,7 @@ function mapRoomIds(roomIds) { let type = 'room'; if (room.isSpaceRoom()) type = 'space'; - else if (directs.has(roomId)) type = 'direct'; + else if (directs.includes(roomId)) type = 'direct'; return { type, @@ -81,6 +86,12 @@ function Search() { const searchRef = useRef(null); const mx = initMatrix.matrixClient; const { navigateRoom, navigateSpace } = useRoomNavigate(); + const mDirects = useAtomValue(mDirectAtom); + const spaces = useSpaces(mx, allRoomsAtom); + const rooms = useRooms(mx, allRoomsAtom, mDirects); + const directs = useDirects(mx, allRoomsAtom, mDirects); + const roomToUnread = useAtomValue(roomToUnreadAtom); + const roomToParents = useAtomValue(roomToParentsAtom); const handleSearchResults = (chunk, term) => { setResult({ @@ -97,7 +108,6 @@ function Search() { return; } - const { spaces, rooms, directs } = initMatrix.roomList; let ids = null; if (prefix) { @@ -109,15 +119,15 @@ function Search() { } ids.sort(roomIdByActivity); - const mappedIds = mapRoomIds(ids); + const mappedIds = mapRoomIds(ids, directs, roomToParents); asyncSearch.setup(mappedIds, { keys: 'name', isContain: true, limit: 20 }); if (prefix) handleSearchResults(mappedIds, prefix); else asyncSearch.search(term); }; const loadRecentRooms = () => { - const { recentRooms } = navigation; - handleSearchResults(mapRoomIds(recentRooms).reverse()); + const recentRooms = []; + handleSearchResults(mapRoomIds(recentRooms, directs, roomToParents).reverse()); }; const handleAfterOpen = () => { @@ -169,7 +179,6 @@ function Search() { } }; - const noti = initMatrix.notifications; const renderRoomSelector = (item) => { let imageSrc = null; let iconSrc = null; @@ -188,9 +197,9 @@ function Search() { roomId={item.roomId} imageSrc={imageSrc} iconSrc={iconSrc} - isUnread={noti.hasNoti(item.roomId)} - notificationCount={noti.getTotalNoti(item.roomId)} - isAlert={noti.getHighlightNoti(item.roomId) > 0} + isUnread={roomToUnread.has(item.roomId)} + notificationCount={roomToUnread.get(item.roomId)?.total ?? 0} + isAlert={roomToUnread.get(item.roomId)?.highlight > 0} onClick={() => openItem(item.roomId, item.type)} /> ); diff --git a/src/app/organisms/settings/CrossSigning.jsx b/src/app/organisms/settings/CrossSigning.jsx index 563e31522..9d848d5ae 100644 --- a/src/app/organisms/settings/CrossSigning.jsx +++ b/src/app/organisms/settings/CrossSigning.jsx @@ -3,7 +3,6 @@ import React, { useState } from 'react'; import './CrossSigning.scss'; import FileSaver from 'file-saver'; import { Formik } from 'formik'; -import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; import { openReusableDialog } from '../../../client/action/navigation'; @@ -22,15 +21,17 @@ import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus'; const failedDialog = () => { const renderFailure = (requestClose) => (
- {twemojify('❌')} + Failed to setup cross signing. Please try again.
); openReusableDialog( - Setup cross signing, - renderFailure, + + Setup cross signing + , + renderFailure ); }; @@ -48,11 +49,11 @@ const securityKeyDialog = (key) => { const renderSecurityKey = () => (
Please save this security key somewhere safe. - - {key.encodedPrivateKey} - + {key.encodedPrivateKey}
- +
@@ -62,8 +63,10 @@ const securityKeyDialog = (key) => { downloadKey(); openReusableDialog( - Security Key, - () => renderSecurityKey(), + + Security Key + , + () => renderSecurityKey() ); }; @@ -112,7 +115,7 @@ function CrossSigningSetup() { errors.phrase = 'Phrase must contain 8-127 characters with no space.'; } if (values.confirmPhrase.length > 0 && values.confirmPhrase !== values.phrase) { - errors.confirmPhrase = 'Phrase don\'t match.'; + errors.confirmPhrase = "Phrase don't match."; } return errors; }; @@ -121,10 +124,14 @@ function CrossSigningSetup() {
- We will generate a Security Key, - which you can use to manage messages backup and session verification. + We will generate a Security Key, which you can use to manage messages backup and + session verification. - {genWithPhrase !== false && } + {genWithPhrase !== false && ( + + )} {genWithPhrase === false && }
OR @@ -133,9 +140,7 @@ function CrossSigningSetup() { onSubmit={(values) => setup(values.phrase)} validate={validator} > - {({ - values, errors, handleChange, handleSubmit, - }) => ( + {({ values, errors, handleChange, handleSubmit }) => (
Alternatively you can also set a Security Phrase - so you don't have to remember long Security Key, - and optionally save the Key as backup. + so you don't have to remember long Security Key, and optionally save the Key as + backup. - {errors.phrase && {errors.phrase}} + {errors.phrase && ( + + {errors.phrase} + + )} - {errors.confirmPhrase && {errors.confirmPhrase}} - {genWithPhrase !== true && } + {errors.confirmPhrase && ( + + {errors.confirmPhrase} + + )} + {genWithPhrase !== true && ( + + )} {genWithPhrase === true && } )} @@ -177,31 +194,36 @@ function CrossSigningSetup() { const setupDialog = () => { openReusableDialog( - Setup cross signing, - () => , + + Setup cross signing + , + () => ); }; function CrossSigningReset() { return (
- {twemojify('✋🧑‍🚒🤚')} + ✋🧑‍🚒🤚 Resetting cross-signing keys is permanent. - Anyone you have verified with will see security alerts and your message backup will be lost. - You almost certainly do not want to do this, - unless you have lost Security Key or Phrase and - every session you can cross-sign from. + Anyone you have verified with will see security alerts and your message backup will be lost. + You almost certainly do not want to do this, unless you have lost Security Key or{' '} + Phrase and every session you can cross-sign from. - +
); } const resetDialog = () => { openReusableDialog( - Reset cross signing, - () => , + + Reset cross signing + , + () => ); }; @@ -210,12 +232,23 @@ function CrossSignin() { return ( Setup to verify and keep track of all your sessions. Also required to backup encrypted message.} - options={( - isCSEnabled - ? - : - )} + content={ + + Setup to verify and keep track of all your sessions. Also required to backup encrypted + message. + + } + options={ + isCSEnabled ? ( + + ) : ( + + ) + } /> ); } diff --git a/src/app/organisms/settings/KeyBackup.jsx b/src/app/organisms/settings/KeyBackup.jsx index 75f032bc3..b4f2125e2 100644 --- a/src/app/organisms/settings/KeyBackup.jsx +++ b/src/app/organisms/settings/KeyBackup.jsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import './KeyBackup.scss'; -import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; import { openReusableDialog } from '../../../client/action/navigation'; @@ -34,10 +33,7 @@ function CreateKeyBackupDialog({ keyData }) { let info; try { - info = await mx.prepareKeyBackupVersion( - null, - { secureSecretStorage: true }, - ); + info = await mx.prepareKeyBackupVersion(null, { secureSecretStorage: true }); info = await mx.createKeyBackupVersion(info); await mx.scheduleAllGroupSessionsForBackup(); if (!mountStore.getItem()) return; @@ -65,7 +61,7 @@ function CreateKeyBackupDialog({ keyData }) { )} {done === true && ( <> - {twemojify('✅')} + Successfully created backup )} @@ -104,12 +100,9 @@ function RestoreKeyBackupDialog({ keyData }) { try { const backupInfo = await mx.getKeyBackupVersion(); - const info = await mx.restoreKeyBackupWithSecretStorage( - backupInfo, - undefined, - undefined, - { progressCallback }, - ); + const info = await mx.restoreKeyBackupWithSecretStorage(backupInfo, undefined, undefined, { + progressCallback, + }); if (!mountStore.getItem()) return; setStatus({ done: `Successfully restored backup keys (${info.imported}/${info.total}).` }); } catch (e) { @@ -138,7 +131,7 @@ function RestoreKeyBackupDialog({ keyData }) { )} {status.done && ( <> - {twemojify('✅')} + {status.done} )} @@ -176,14 +169,16 @@ function DeleteKeyBackupDialog({ requestClose }) { return (
- {twemojify('🗑')} + 🗑 Deleting key backup is permanent. All encrypted messages keys stored on server will be deleted. - { - isDeleting - ? - : - } + {isDeleting ? ( + + ) : ( + + )}
); } @@ -224,9 +219,11 @@ function KeyBackup() { if (keyData === null) return; openReusableDialog( - Create Key Backup, + + Create Key Backup + , () => , - () => fetchKeyBackupVersion(), + () => fetchKeyBackupVersion() ); }; @@ -235,29 +232,44 @@ function KeyBackup() { if (keyData === null) return; openReusableDialog( - Restore Key Backup, - () => , + + Restore Key Backup + , + () => ); }; - const openDeleteKeyBackup = () => openReusableDialog( - Delete Key Backup, - (requestClose) => ( - { - if (isDone) setKeyBackup(null); - requestClose(); - }} - /> - ), - ); + const openDeleteKeyBackup = () => + openReusableDialog( + + Delete Key Backup + , + (requestClose) => ( + { + if (isDone) setKeyBackup(null); + requestClose(); + }} + /> + ) + ); const renderOptions = () => { if (keyBackup === undefined) return ; - if (keyBackup === null) return ; + if (keyBackup === null) + return ( + + ); return ( <> - + ); @@ -266,9 +278,12 @@ function KeyBackup() { return ( - Online backup your encrypted messages keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key. + + Online backup your encrypted messages keys with your account data in case you lose + access to your sessions. Your keys will be secured with a unique Security Key. + {!isCSEnabled && ( )} - )} + } options={isCSEnabled ? renderOptions() : null} /> ); diff --git a/src/app/organisms/shortcut-spaces/ShortcutSpaces.jsx b/src/app/organisms/shortcut-spaces/ShortcutSpaces.jsx deleted file mode 100644 index 62ec76a31..000000000 --- a/src/app/organisms/shortcut-spaces/ShortcutSpaces.jsx +++ /dev/null @@ -1,169 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import './ShortcutSpaces.scss'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import navigation from '../../../client/state/navigation'; -import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/accountData'; -import { joinRuleToIconSrc } from '../../../util/matrixUtil'; -import { roomIdByAtoZ } from '../../../util/sort'; - -import Text from '../../atoms/text/Text'; -import Button from '../../atoms/button/Button'; -import IconButton from '../../atoms/button/IconButton'; -import Checkbox from '../../atoms/button/Checkbox'; -import Spinner from '../../atoms/spinner/Spinner'; -import RoomSelector from '../../molecules/room-selector/RoomSelector'; -import Dialog from '../../molecules/dialog/Dialog'; - -import PinIC from '../../../../public/res/ic/outlined/pin.svg'; -import PinFilledIC from '../../../../public/res/ic/filled/pin.svg'; -import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; - -import { useSpaceShortcut } from '../../hooks/useSpaceShortcut'; - -function ShortcutSpacesContent() { - const mx = initMatrix.matrixClient; - const { spaces, roomIdToParents } = initMatrix.roomList; - - const [spaceShortcut] = useSpaceShortcut(); - const spaceWithoutShortcut = [...spaces].filter( - (spaceId) => !spaceShortcut.includes(spaceId), - ).sort(roomIdByAtoZ); - - const [process, setProcess] = useState(null); - const [selected, setSelected] = useState([]); - - useEffect(() => { - if (process !== null) { - setProcess(null); - setSelected([]); - } - }, [spaceShortcut]); - - const toggleSelection = (sId) => { - if (process !== null) return; - const newSelected = [...selected]; - const selectedIndex = newSelected.indexOf(sId); - - if (selectedIndex > -1) { - newSelected.splice(selectedIndex, 1); - setSelected(newSelected); - return; - } - newSelected.push(sId); - setSelected(newSelected); - }; - - const handleAdd = () => { - setProcess(`Pinning ${selected.length} spaces...`); - createSpaceShortcut(selected); - }; - - const renderSpace = (spaceId, isShortcut) => { - const room = mx.getRoom(spaceId); - if (!room) return null; - - const parentSet = roomIdToParents.get(spaceId); - const parentNames = parentSet - ? [...parentSet].map((parentId) => mx.getRoom(parentId).name) - : undefined; - const parents = parentNames ? parentNames.join(', ') : null; - - const toggleSelected = () => toggleSelection(spaceId); - const deleteShortcut = () => deleteSpaceShortcut(spaceId); - - return ( - - ) : ( - - )} - /> - ); - }; - - return ( - <> - Pinned spaces - {spaceShortcut.length === 0 && No pinned spaces} - {spaceShortcut.map((spaceId) => renderSpace(spaceId, true))} - Unpinned spaces - {spaceWithoutShortcut.length === 0 && No unpinned spaces} - {spaceWithoutShortcut.map((spaceId) => renderSpace(spaceId, false))} - {selected.length !== 0 && ( -
- {process && } - {process || `${selected.length} spaces selected`} - { !process && ( - - )} -
- )} - - ); -} - -function useVisibilityToggle() { - const [isOpen, setIsOpen] = useState(false); - - useEffect(() => { - const handleOpen = () => setIsOpen(true); - navigation.on(cons.events.navigation.SHORTCUT_SPACES_OPENED, handleOpen); - return () => { - navigation.removeListener(cons.events.navigation.SHORTCUT_SPACES_OPENED, handleOpen); - }; - }, []); - - const requestClose = () => setIsOpen(false); - - return [isOpen, requestClose]; -} - -function ShortcutSpaces() { - const [isOpen, requestClose] = useVisibilityToggle(); - - return ( - - Pin spaces - - )} - contentOptions={} - onRequestClose={requestClose} - > - { - isOpen - ? - :
- } -
- ); -} - -export default ShortcutSpaces; diff --git a/src/app/organisms/shortcut-spaces/ShortcutSpaces.scss b/src/app/organisms/shortcut-spaces/ShortcutSpaces.scss deleted file mode 100644 index 686c8cc05..000000000 --- a/src/app/organisms/shortcut-spaces/ShortcutSpaces.scss +++ /dev/null @@ -1,52 +0,0 @@ -@use '../../partials/dir'; -@use '../../partials/flex'; - -.shortcut-spaces { - height: 100%; - .dialog__content-container { - padding: 0; - padding-bottom: 80px; - @include dir.side(padding, var(--sp-extra-tight), 0); - - & > .text-b1 { - padding: 0 var(--sp-extra-tight); - } - } - - &__header { - margin-top: var(--sp-extra-tight); - padding: var(--sp-extra-tight); - text-transform: uppercase; - } - - .room-selector { - margin: 0 var(--sp-extra-tight); - } - .room-selector__options { - display: flex; - .checkbox { - margin: 0 6px; - } - } - - &__footer { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - padding: var(--sp-normal); - background-color: var(--bg-surface); - border-top: 1px solid var(--bg-surface-border); - display: flex; - align-items: center; - - & > .text { - @extend .cp-fx__item-one; - padding: 0 var(--sp-tight); - } - - & > button { - @include dir.side(margin, var(--sp-normal), 0); - } - } -} diff --git a/src/app/organisms/space-manage/SpaceManage.jsx b/src/app/organisms/space-manage/SpaceManage.jsx deleted file mode 100644 index 60f00ad31..000000000 --- a/src/app/organisms/space-manage/SpaceManage.jsx +++ /dev/null @@ -1,433 +0,0 @@ -/* eslint-disable react/prop-types */ -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import './SpaceManage.scss'; - -import { twemojify } from '../../../util/twemojify'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import navigation from '../../../client/state/navigation'; -import colorMXID from '../../../util/colorMXID'; -import { selectRoom, selectTab } from '../../../client/action/navigation'; -import RoomsHierarchy from '../../../client/state/RoomsHierarchy'; -import { joinRuleToIconSrc } from '../../../util/matrixUtil'; -import { join } from '../../../client/action/room'; -import { Debounce } from '../../../util/common'; - -import Text from '../../atoms/text/Text'; -import RawIcon from '../../atoms/system-icons/RawIcon'; -import Button from '../../atoms/button/Button'; -import IconButton from '../../atoms/button/IconButton'; -import Checkbox from '../../atoms/button/Checkbox'; -import Avatar from '../../atoms/avatar/Avatar'; -import Spinner from '../../atoms/spinner/Spinner'; -import ScrollView from '../../atoms/scroll/ScrollView'; -import PopupWindow from '../../molecules/popup-window/PopupWindow'; - -import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; -import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg'; -import InfoIC from '../../../../public/res/ic/outlined/info.svg'; - -import { useForceUpdate } from '../../hooks/useForceUpdate'; -import { useStore } from '../../hooks/useStore'; - -function SpaceManageBreadcrumb({ path, onSelect }) { - return ( -
- -
- { - path.map((item, index) => ( - - {index > 0 && } - - - )) - } -
-
-
- ); -} -SpaceManageBreadcrumb.propTypes = { - path: PropTypes.arrayOf(PropTypes.exact({ - roomId: PropTypes.string, - name: PropTypes.string, - })).isRequired, - onSelect: PropTypes.func.isRequired, -}; - -function SpaceManageItem({ - parentId, roomInfo, onSpaceClick, requestClose, - isSelected, onSelect, roomHierarchy, -}) { - const [isExpand, setIsExpand] = useState(false); - const [isJoining, setIsJoining] = useState(false); - - const { directs } = initMatrix.roomList; - const mx = initMatrix.matrixClient; - const parentRoom = mx.getRoom(parentId); - const isSpace = roomInfo.room_type === 'm.space'; - const roomId = roomInfo.room_id; - const canManage = parentRoom?.currentState.maySendStateEvent('m.space.child', mx.getUserId()) || false; - const isSuggested = parentRoom?.currentState.getStateEvents('m.space.child', roomId)?.getContent().suggested === true; - - const room = mx.getRoom(roomId); - const isJoined = !!(room?.getMyMembership() === 'join' || null); - const name = room?.name || roomInfo.name || roomInfo.canonical_alias || roomId; - let imageSrc = mx.mxcUrlToHttp(roomInfo.avatar_url, 24, 24, 'crop') || null; - if (!imageSrc && room) { - imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; - if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; - } - const isDM = directs.has(roomId); - - const handleOpen = () => { - if (isSpace) selectTab(roomId); - else selectRoom(roomId); - requestClose(); - }; - const handleJoin = () => { - const viaSet = roomHierarchy.viaMap.get(roomId); - const via = viaSet ? [...viaSet] : undefined; - join(roomId, false, via); - setIsJoining(true); - }; - - const roomAvatarJSX = ( - - ); - const roomNameJSX = ( - - {twemojify(name)} - {` • ${roomInfo.num_joined_members} members`} - - ); - - const expandBtnJsx = ( - setIsExpand(!isExpand)} - /> - ); - - return ( -
-
- {canManage && onSelect(roomId)} variant="positive" />} - - {roomInfo.topic && expandBtnJsx} - { - isJoined - ? - : - } -
- {isExpand && roomInfo.topic && {twemojify(roomInfo.topic, undefined, true)}} -
- ); -} -SpaceManageItem.propTypes = { - parentId: PropTypes.string.isRequired, - roomHierarchy: PropTypes.shape({}).isRequired, - roomInfo: PropTypes.shape({}).isRequired, - onSpaceClick: PropTypes.func.isRequired, - requestClose: PropTypes.func.isRequired, - isSelected: PropTypes.bool.isRequired, - onSelect: PropTypes.func.isRequired, -}; - -function SpaceManageFooter({ parentId, selected }) { - const [process, setProcess] = useState(null); - const mx = initMatrix.matrixClient; - const room = mx.getRoom(parentId); - const { currentState } = room; - - const allSuggested = selected.every((roomId) => { - const sEvent = currentState.getStateEvents('m.space.child', roomId); - return !!sEvent?.getContent()?.suggested; - }); - - const handleRemove = () => { - setProcess(`Removing ${selected.length} items`); - selected.forEach((roomId) => { - mx.sendStateEvent(parentId, 'm.space.child', {}, roomId); - }); - }; - - const handleToggleSuggested = (isMark) => { - if (isMark) setProcess(`Marking as suggested ${selected.length} items`); - else setProcess(`Marking as not suggested ${selected.length} items`); - selected.forEach((roomId) => { - const sEvent = room.currentState.getStateEvents('m.space.child', roomId); - if (!sEvent) return; - const content = { ...sEvent.getContent() }; - if (isMark && content.suggested) return; - if (!isMark && !content.suggested) return; - content.suggested = isMark; - mx.sendStateEvent(parentId, 'm.space.child', content, roomId); - }); - }; - - return ( -
- {process && } - {process || `${selected.length} item selected`} - { !process && ( - <> - - - - )} -
- ); -} -SpaceManageFooter.propTypes = { - parentId: PropTypes.string.isRequired, - selected: PropTypes.arrayOf(PropTypes.string).isRequired, -}; - -function useSpacePath(roomId) { - const mx = initMatrix.matrixClient; - const room = mx.getRoom(roomId); - const [spacePath, setSpacePath] = useState([{ roomId, name: room.name }]); - - const addPathItem = (rId, name) => { - const newPath = [...spacePath]; - const itemIndex = newPath.findIndex((item) => item.roomId === rId); - if (itemIndex < 0) { - newPath.push({ roomId: rId, name }); - setSpacePath(newPath); - return; - } - newPath.splice(itemIndex + 1); - setSpacePath(newPath); - }; - - return [spacePath, addPathItem]; -} - -function useUpdateOnJoin(roomId) { - const [, forceUpdate] = useForceUpdate(); - const { roomList } = initMatrix; - - useEffect(() => { - const handleRoomList = () => forceUpdate(); - - roomList.on(cons.events.roomList.ROOM_JOINED, handleRoomList); - roomList.on(cons.events.roomList.ROOM_LEAVED, handleRoomList); - return () => { - roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleRoomList); - roomList.removeListener(cons.events.roomList.ROOM_LEAVED, handleRoomList); - }; - }, [roomId]); -} - -function useChildUpdate(roomId, roomsHierarchy) { - const [, forceUpdate] = useForceUpdate(); - const [debounce] = useState(new Debounce()); - const mx = initMatrix.matrixClient; - - useEffect(() => { - let isMounted = true; - const handleStateEvent = (event) => { - if (event.getRoomId() !== roomId) return; - if (event.getType() !== 'm.space.child') return; - - debounce._(() => { - if (!isMounted) return; - roomsHierarchy.removeHierarchy(roomId); - forceUpdate(); - }, 500)(); - }; - mx.on('RoomState.events', handleStateEvent); - return () => { - isMounted = false; - mx.removeListener('RoomState.events', handleStateEvent); - }; - }, [roomId, roomsHierarchy]); -} - -function SpaceManageContent({ roomId, requestClose }) { - const mx = initMatrix.matrixClient; - useUpdateOnJoin(roomId); - const [, forceUpdate] = useForceUpdate(); - const [roomsHierarchy] = useState(new RoomsHierarchy(mx, 30)); - const [spacePath, addPathItem] = useSpacePath(roomId); - const [isLoading, setIsLoading] = useState(true); - const [selected, setSelected] = useState([]); - const mountStore = useStore(); - const currentPath = spacePath[spacePath.length - 1]; - useChildUpdate(currentPath.roomId, roomsHierarchy); - - const currentHierarchy = roomsHierarchy.getHierarchy(currentPath.roomId); - - useEffect(() => { - mountStore.setItem(true); - return () => { - mountStore.setItem(false); - }; - }, [roomId]); - - useEffect(() => { - setSelected([]); - }, [spacePath]); - - const handleSelected = (selectedRoomId) => { - const newSelected = [...selected]; - const selectedIndex = newSelected.indexOf(selectedRoomId); - - if (selectedIndex > -1) { - newSelected.splice(selectedIndex, 1); - setSelected(newSelected); - return; - } - newSelected.push(selectedRoomId); - setSelected(newSelected); - }; - - const loadRoomHierarchy = async () => { - if (!roomsHierarchy.canLoadMore(currentPath.roomId)) return; - if (!roomsHierarchy.getHierarchy(currentPath.roomId)) setSelected([]); - setIsLoading(true); - try { - await roomsHierarchy.load(currentPath.roomId); - if (!mountStore.getItem()) return; - setIsLoading(false); - forceUpdate(); - } catch { - if (!mountStore.getItem()) return; - setIsLoading(false); - forceUpdate(); - } - }; - - if (!currentHierarchy) loadRoomHierarchy(); - return ( -
- {spacePath.length > 1 && ( - - )} - Rooms and spaces -
- {!isLoading && currentHierarchy?.rooms?.length === 1 && ( - - Either the space contains private rooms or you need to join space to view it's rooms. - - )} - {currentHierarchy && (currentHierarchy.rooms?.map((roomInfo) => ( - roomInfo.room_id === currentPath.roomId - ? null - : ( - - ) - )))} - {!currentHierarchy && loading...} -
- {currentHierarchy?.canLoadMore && !isLoading && ( - - )} - {isLoading && ( -
- - Loading rooms... -
- )} - {selected.length > 0 && ( - - )} -
- ); -} -SpaceManageContent.propTypes = { - roomId: PropTypes.string.isRequired, - requestClose: PropTypes.func.isRequired, -}; - -function useWindowToggle() { - const [roomId, setRoomId] = useState(null); - - useEffect(() => { - const openSpaceManage = (rId) => { - setRoomId(rId); - }; - navigation.on(cons.events.navigation.SPACE_MANAGE_OPENED, openSpaceManage); - return () => { - navigation.removeListener(cons.events.navigation.SPACE_MANAGE_OPENED, openSpaceManage); - }; - }, []); - - const requestClose = () => setRoomId(null); - - return [roomId, requestClose]; -} -function SpaceManage() { - const mx = initMatrix.matrixClient; - const [roomId, requestClose] = useWindowToggle(); - const room = mx.getRoom(roomId); - - return ( - - {roomId && twemojify(room.name)} - — manage rooms - - )} - contentOptions={} - onRequestClose={requestClose} - > - { - roomId - ? - :
- } - - ); -} - -export default SpaceManage; diff --git a/src/app/organisms/space-manage/SpaceManage.scss b/src/app/organisms/space-manage/SpaceManage.scss deleted file mode 100644 index b72c92d8a..000000000 --- a/src/app/organisms/space-manage/SpaceManage.scss +++ /dev/null @@ -1,168 +0,0 @@ -@use '../../partials/text'; -@use '../../partials/dir'; -@use '../../partials/flex'; - -.space-manage { - & .pw__content-wrapper { - position: relative; - } - & .pw__content-container { - padding-top: 0; - padding-bottom: 73px; - } -} - -.space-manage__content { - margin-bottom: var(--sp-extra-loose); - - & > .text { - margin-top: var(--sp-extra-tight); - padding: var(--sp-extra-tight) var(--sp-normal); - text-transform: uppercase; - } - - &-items { - @include dir.side(padding, var(--sp-extra-tight), 0); - & > .text:first-child { - padding: var(--sp-extra-tight); - } - } - - & > button { - margin: var(--sp-normal); - } - - &-loading { - padding: var(--sp-loose); - display: flex; - justify-content: center; - align-items: center; - & .text { - margin: 0 var(--sp-normal); - } - } -} -.space-manage-breadcrumb { - display: flex; - align-items: center; - height: 100%; - margin: 0 var(--sp-extra-tight); - - &__wrapper { - height: var(--header-height); - position: sticky; - top: 0; - z-index: 99; - background-color: var(--bg-surface); - } - & > * { - flex-shrink: 0; - } - - & > .btn-surface { - min-width: 0; - padding: var(--sp-extra-tight) 10px; - white-space: nowrap; - box-shadow: none; - & p { - @extend .cp-txt__ellipsis; - max-width: 200px; - } - &:last-child { - box-shadow: var(--bs-surface-border) !important; - background-color: var(--bg-surface); - } - } - -} - -.space-manage-item { - margin: var(--sp-ultra-tight) var(--sp-extra-tight); - padding: 0 var(--sp-extra-tight); - border-radius: var(--bo-radius); - - & > div { - min-height: 40px; - display: flex; - align-items: center; - } - - &--space { - @extend .space-manage-item; - & .space-manage-item__btn { - cursor: pointer; - } - } - - &:hover { - background-color: var(--bg-surface-hover); - } - - & .checkbox { - @include dir.side(margin, 0, var(--sp-tight)); - } - - - &__btn { - @extend .cp-fx__item-one; - display: flex; - align-items: center; - - & .avatar__border--active { - box-shadow: none; - } - & > .text-b1 { - @extend .cp-fx__item-one; - @extend .cp-txt__ellipsis; - min-width: 0; - margin: 0 var(--sp-extra-tight); - } - & > .text-b2 { - margin: 0 var(--sp-extra-tight); - padding: 1px var(--sp-ultra-tight); - color: var(--bg-positive); - box-shadow: var(--bs-positive-border); - border-radius: 4px; - } - } - - & .ic-btn { - padding: 7px; - @include dir.side(margin, 0, var(--sp-extra-tight)); - opacity: 0.7; - } - - & .btn-surface, - & .btn-primary { - padding: var(--sp-ultra-tight) var(--sp-extra-tight); - min-width: 60px; - } - - & > .text { - padding: 32px; - padding-top: 0; - padding-bottom: var(--sp-normal); - white-space: pre-wrap; - } -} - -.space-manage__footer { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - padding: var(--sp-normal); - background-color: var(--bg-surface); - border-top: 1px solid var(--bg-surface-border); - display: flex; - align-items: center; - - & > .text { - @extend .cp-fx__item-one; - padding: 0 var(--sp-tight); - } - - & > button { - @include dir.side(margin, var(--sp-normal), 0); - } -} \ No newline at end of file diff --git a/src/app/organisms/space-settings/SpaceSettings.jsx b/src/app/organisms/space-settings/SpaceSettings.jsx index 46fe7b3f7..ff6c18632 100644 --- a/src/app/organisms/space-settings/SpaceSettings.jsx +++ b/src/app/organisms/space-settings/SpaceSettings.jsx @@ -2,12 +2,9 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import './SpaceSettings.scss'; -import { twemojify } from '../../../util/twemojify'; - import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; -import { leave } from '../../../client/action/room'; import Text from '../../atoms/text/Text'; import IconButton from '../../atoms/button/IconButton'; @@ -29,6 +26,7 @@ import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg'; import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg'; import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; const tabText = { GENERAL: 'General', @@ -62,6 +60,7 @@ const tabItems = [ function GeneralSettings({ roomId }) { const roomName = initMatrix.matrixClient.getRoom(roomId)?.name; + const mx = useMatrixClient(); return ( <> @@ -76,7 +75,7 @@ function GeneralSettings({ roomId }) { 'Leave', 'danger' ); - if (isConfirmed) leave(roomId); + if (isConfirmed) mx.leave(roomId); }} iconSrc={LeaveArrowIC} > @@ -138,7 +137,7 @@ function SpaceSettings() { className="space-settings" title={ - {isOpen && twemojify(room.name)} + {isOpen && room.name} — space settings } diff --git a/src/app/organisms/sticker-board/StickerBoard.jsx b/src/app/organisms/sticker-board/StickerBoard.jsx deleted file mode 100644 index 91e259186..000000000 --- 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/organisms/sticker-board/StickerBoard.scss b/src/app/organisms/sticker-board/StickerBoard.scss deleted file mode 100644 index b4e551301..000000000 --- a/src/app/organisms/sticker-board/StickerBoard.scss +++ /dev/null @@ -1,74 +0,0 @@ -@use '../../partials/dir'; - -.sticker-board { - --sticker-board-height: 390px; - --sticker-board-width: 286px; - display: flex; - height: var(--sticker-board-height); - display: flex; - - & > .scrollbar { - width: initial; - height: var(--sticker-board-height); - } - - &__sidebar { - display: flex; - flex-direction: column; - min-height: 100%; - padding: 4px 6px; - @include dir.side(border, none, 1px solid var(--bg-surface-border)); - } - - &__container { - flex-grow: 1; - min-width: 0; - width: var(--sticker-board-width); - display: flex; - } - - &__content { - min-height: 100%; - } - - &__pack { - margin-bottom: var(--sp-normal); - position: relative; - - &-header { - position: sticky; - top: 0; - z-index: 99; - background-color: var(--bg-surface); - - @include dir.side(margin, var(--sp-extra-tight), 0); - padding: var(--sp-extra-tight) var(--sp-ultra-tight); - text-transform: uppercase; - box-shadow: 0 -4px 0 0 var(--bg-surface); - border-bottom: 1px solid var(--bg-surface-border); - } - &-items { - margin: var(--sp-tight); - @include dir.side(margin, var(--sp-normal), var(--sp-extra-tight)); - display: flex; - flex-wrap: wrap; - gap: var(--sp-normal) var(--sp-tight); - - img { - width: 76px; - height: 76px; - object-fit: contain; - cursor: pointer; - } - } - } - - &__empty { - width: 100%; - height: var(--sticker-board-height); - display: flex; - justify-content: center; - align-items: center; - text-align: center; - } -} \ No newline at end of file diff --git a/src/app/organisms/view-source/ViewSource.jsx b/src/app/organisms/view-source/ViewSource.jsx deleted file mode 100644 index 9bd3334f5..000000000 --- a/src/app/organisms/view-source/ViewSource.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import './ViewSource.scss'; - -import cons from '../../../client/state/cons'; -import navigation from '../../../client/state/navigation'; - -import IconButton from '../../atoms/button/IconButton'; -import { MenuHeader } from '../../atoms/context-menu/ContextMenu'; -import ScrollView from '../../atoms/scroll/ScrollView'; -import PopupWindow from '../../molecules/popup-window/PopupWindow'; - -import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; - -function ViewSourceBlock({ title, json }) { - return ( -
- {title} - -
-          
-            {JSON.stringify(json, null, 2)}
-          
-        
-
-
- ); -} -ViewSourceBlock.propTypes = { - title: PropTypes.string.isRequired, - json: PropTypes.shape({}).isRequired, -}; - -function ViewSource() { - const [isOpen, setIsOpen] = useState(false); - const [event, setEvent] = useState(null); - - useEffect(() => { - const loadViewSource = (e) => { - setEvent(e); - setIsOpen(true); - }; - navigation.on(cons.events.navigation.VIEWSOURCE_OPENED, loadViewSource); - return () => { - navigation.removeListener(cons.events.navigation.VIEWSOURCE_OPENED, loadViewSource); - }; - }, []); - - const handleAfterClose = () => { - setEvent(null); - }; - - const renderViewSource = () => ( -
- {event.isEncrypted() && } - -
- ); - - return ( - setIsOpen(false)} - contentOptions={ setIsOpen(false)} tooltip="Close" />} - > - {event ? renderViewSource() :
} - - ); -} - -export default ViewSource; diff --git a/src/app/organisms/view-source/ViewSource.scss b/src/app/organisms/view-source/ViewSource.scss deleted file mode 100644 index 9ceab8b03..000000000 --- a/src/app/organisms/view-source/ViewSource.scss +++ /dev/null @@ -1,19 +0,0 @@ -@use '../../partials/dir'; - -.view-source { - @include dir.side(margin, var(--sp-normal), var(--sp-extra-tight)); - - & pre { - padding: var(--sp-extra-tight); - white-space: pre-wrap; - word-break: break-all; - } - - &__card { - margin: var(--sp-normal) 0; - background-color: var(--bg-surface-hover); - border-radius: var(--bo-radius); - box-shadow: var(--bs-surface-border); - overflow: hidden; - } -} diff --git a/src/app/organisms/welcome/Welcome.jsx b/src/app/organisms/welcome/Welcome.jsx deleted file mode 100644 index 6d135bee2..000000000 --- a/src/app/organisms/welcome/Welcome.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import './Welcome.scss'; - -import Text from '../../atoms/text/Text'; - -import CinnySvg from '../../../../public/res/svg/cinny.svg'; - -function Welcome() { - return ( -
-
- Cinny logo - Welcome to Cinny - Yet another matrix client -
-
- ); -} - -export default Welcome; diff --git a/src/app/organisms/welcome/Welcome.scss b/src/app/organisms/welcome/Welcome.scss deleted file mode 100644 index e55bb8edd..000000000 --- a/src/app/organisms/welcome/Welcome.scss +++ /dev/null @@ -1,23 +0,0 @@ -@use '../../partials/flex'; - -.app-welcome { - width: 100%; - height: 100%; - background-color: var(--bg-surface); - - & > div { - @extend .cp-fx__column--c-c; - max-width: 600px; - } - &__logo { - width: 64px; - height: 64px; - } - &__heading { - margin: var(--sp-extra-loose) 0 var(--sp-tight); - color: var(--tc-surface-high); - } - &__subheading { - color: var(--tc-surface-normal); - } -} \ No newline at end of file diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx index ffa20d4ed..7d0f4fdeb 100644 --- a/src/app/pages/Router.tsx +++ b/src/app/pages/Router.tsx @@ -54,6 +54,7 @@ import { PageRoot } from '../components/page'; import { ScreenSize } from '../hooks/useScreenSize'; import { MobileFriendlyPageNav, MobileFriendlyClientNav } from './MobileFriendly'; import { ClientInitStorageAtom } from './client/ClientInitStorageAtom'; +import { ClientNonUIFeatures } from './client/ClientNonUIFeatures'; export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => { const { hashRouter } = clientConfig; @@ -101,15 +102,17 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) - - - - } - > - - + + + + + } + > + + + diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx new file mode 100644 index 000000000..27d1ae40d --- /dev/null +++ b/src/app/pages/client/ClientNonUIFeatures.tsx @@ -0,0 +1,231 @@ +import { useAtomValue } from 'jotai'; +import React, { ReactNode, useCallback, useEffect, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk'; +import { roomToUnreadAtom, unreadEqual, unreadInfoToUnread } from '../../state/room/roomToUnread'; +import LogoSVG from '../../../../public/res/svg/cinny.svg'; +import LogoUnreadSVG from '../../../../public/res/svg/cinny-unread.svg'; +import LogoHighlightSVG from '../../../../public/res/svg/cinny-highlight.svg'; +import { setFavicon } from '../../utils/dom'; +import { useSetting } from '../../state/hooks/settings'; +import { settingsAtom } from '../../state/settings'; +import { allInvitesAtom } from '../../state/room-list/inviteList'; +import { usePreviousValue } from '../../hooks/usePreviousValue'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { getInboxInvitesPath, getInboxNotificationsPath } from '../pathUtils'; +import { + getMemberDisplayName, + getNotificationType, + getUnreadInfo, + isNotificationEvent, +} from '../../utils/room'; +import { NotificationType, UnreadInfo } from '../../../types/matrix/room'; +import { getMxIdLocalPart } from '../../utils/matrix'; +import { useSelectedRoom } from '../../hooks/router/useSelectedRoom'; +import { useInboxNotificationsSelected } from '../../hooks/router/useInbox'; + +function FaviconUpdater() { + const roomToUnread = useAtomValue(roomToUnreadAtom); + + useEffect(() => { + if (roomToUnread.size === 0) { + setFavicon(LogoSVG); + } else { + const highlight = Array.from(roomToUnread.entries()).find( + ([, unread]) => unread.highlight > 0 + ); + + setFavicon(highlight ? LogoHighlightSVG : LogoUnreadSVG); + } + }, [roomToUnread]); + + return null; +} + +function InviteNotifications() { + const audioRef = useRef(null); + const invites = useAtomValue(allInvitesAtom); + const perviousInviteLen = usePreviousValue(invites.length, 0); + const mx = useMatrixClient(); + + const navigate = useNavigate(); + const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds'); + + const notify = useCallback( + (count: number) => { + const noti = new window.Notification('Invitation', { + icon: LogoSVG, + badge: LogoSVG, + body: `You have ${count} new invitation request.`, + silent: true, + }); + + noti.onclick = () => { + if (!window.closed) navigate(getInboxInvitesPath()); + noti.close(); + }; + }, + [navigate] + ); + + const playSound = useCallback(() => { + const audioElement = audioRef.current; + audioElement?.play(); + }, []); + + useEffect(() => { + if (invites.length > perviousInviteLen && mx.getSyncState() === 'SYNCING') { + if (Notification.permission === 'granted') { + notify(invites.length - perviousInviteLen); + } + + if (notificationSound) { + playSound(); + } + } + }, [mx, invites, perviousInviteLen, notificationSound, notify, playSound]); + + return ( + // eslint-disable-next-line jsx-a11y/media-has-caption + + ); +} + +function MessageNotifications() { + const audioRef = useRef(null); + const notifRef = useRef(); + const unreadCacheRef = useRef>(new Map()); + const mx = useMatrixClient(); + const [showNotifications] = useSetting(settingsAtom, 'showNotifications'); + const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds'); + + const navigate = useNavigate(); + const notificationSelected = useInboxNotificationsSelected(); + const selectedRoomId = useSelectedRoom(); + + const notify = useCallback( + ({ + roomName, + roomAvatar, + username, + }: { + roomName: string; + roomAvatar?: string; + username: string; + roomId: string; + eventId: string; + }) => { + const noti = new window.Notification(roomName, { + icon: roomAvatar, + badge: roomAvatar, + body: `New inbox notification from ${username}`, + silent: true, + }); + + noti.onclick = () => { + if (!window.closed) navigate(getInboxNotificationsPath()); + noti.close(); + notifRef.current = undefined; + }; + + notifRef.current?.close(); + notifRef.current = noti; + }, + [navigate] + ); + + const playSound = useCallback(() => { + const audioElement = audioRef.current; + audioElement?.play(); + }, []); + + useEffect(() => { + const handleTimelineEvent: RoomEventHandlerMap[RoomEvent.Timeline] = ( + mEvent, + room, + toStartOfTimeline, + removed, + data + ) => { + if ( + mx.getSyncState() !== 'SYNCING' || + selectedRoomId === room?.roomId || + notificationSelected || + !room || + !data.liveEvent || + room.isSpaceRoom() || + !isNotificationEvent(mEvent) || + getNotificationType(mx, room.roomId) === NotificationType.Mute + ) + return; + + const sender = mEvent.getSender(); + const eventId = mEvent.getId(); + if (!sender || !eventId || mEvent.getSender() === mx.getUserId()) return; + const unreadInfo = getUnreadInfo(room); + const cachedUnreadInfo = unreadCacheRef.current.get(room.roomId); + unreadCacheRef.current.set(room.roomId, unreadInfo); + + if ( + cachedUnreadInfo && + unreadEqual(unreadInfoToUnread(cachedUnreadInfo), unreadInfoToUnread(unreadInfo)) + ) { + return; + } + + if (showNotifications && Notification.permission === 'granted') { + const avatarMxc = + room.getAvatarFallbackMember()?.getMxcAvatarUrl() ?? room.getMxcAvatarUrl(); + notify({ + roomName: room.name ?? 'Unknown', + roomAvatar: avatarMxc + ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined + : undefined, + username: getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender, + roomId: room.roomId, + eventId, + }); + } + + if (notificationSound) { + playSound(); + } + }; + mx.on(RoomEvent.Timeline, handleTimelineEvent); + return () => { + mx.removeListener(RoomEvent.Timeline, handleTimelineEvent); + }; + }, [ + mx, + notificationSound, + notificationSelected, + showNotifications, + playSound, + notify, + selectedRoomId, + ]); + + return ( + // eslint-disable-next-line jsx-a11y/media-has-caption + + ); +} + +type ClientNonUIFeaturesProps = { + children: ReactNode; +}; + +export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) { + return ( + <> + + + + {children} + + ); +} diff --git a/src/app/pages/client/ClientRoot.tsx b/src/app/pages/client/ClientRoot.tsx index 1bb7855b6..6a1dbcb16 100644 --- a/src/app/pages/client/ClientRoot.tsx +++ b/src/app/pages/client/ClientRoot.tsx @@ -2,7 +2,6 @@ import { Box, Spinner, Text } from 'folds'; import React, { ReactNode, useEffect, useState } from 'react'; import initMatrix from '../../../client/initMatrix'; import { initHotkeys } from '../../../client/event/hotkeys'; -import { initRoomListListener } from '../../../client/event/roomList'; import { getSecret } from '../../../client/state/auth'; import { SplashScreen } from '../../components/splash-screen'; import { CapabilitiesAndMediaConfigLoader } from '../../components/CapabilitiesAndMediaConfigLoader'; @@ -49,7 +48,6 @@ export function ClientRoot({ children }: ClientRootProps) { useEffect(() => { const handleStart = () => { initHotkeys(); - initRoomListListener(initMatrix.roomList); setLoading(false); }; initMatrix.once('init_loading_finished', handleStart); diff --git a/src/app/pages/client/inbox/Notifications.tsx b/src/app/pages/client/inbox/Notifications.tsx index 1ab08f01c..8b9d1847c 100644 --- a/src/app/pages/client/inbox/Notifications.tsx +++ b/src/app/pages/client/inbox/Notifications.tsx @@ -31,13 +31,20 @@ import { InboxNotificationsPathSearchParams } from '../../paths'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { SequenceCard } from '../../../components/sequence-card'; import { RoomAvatar, RoomIcon } from '../../../components/room-avatar'; -import { getMemberAvatarMxc, getMemberDisplayName, getRoomAvatarUrl } from '../../../utils/room'; +import { + getEditedEvent, + getMemberAvatarMxc, + getMemberDisplayName, + getRoomAvatarUrl, +} from '../../../utils/room'; import { ScrollTopContainer } from '../../../components/scroll-top-container'; import { useInterval } from '../../../hooks/useInterval'; import { AvatarBase, ImageContent, MSticker, + MessageNotDecryptedContent, + MessageUnsupportedContent, ModernLayout, RedactedContent, Reply, @@ -62,6 +69,7 @@ import { markAsRead } from '../../../../client/action/notifications'; import { ContainerColor } from '../../../styles/ContainerColor.css'; import { VirtualTile } from '../../../components/virtualizer'; import { UserAvatar } from '../../../components/user-avatar'; +import { EncryptedContent } from '../../../features/room/message'; type RoomNotificationsGroup = { roomId: string; @@ -225,6 +233,78 @@ function RoomNotificationsGroupComp({ /> ); }, + [MessageEvent.RoomMessageEncrypted]: (evt, displayName) => { + const evtTimeline = room.getTimelineForEvent(evt.event_id); + + const mEvent = evtTimeline?.getEvents().find((e) => e.getId() === evt.event_id); + + if (!mEvent || !evtTimeline) { + return ( + + + {evt.type} + {' event'} + + + ); + } + + return ( + + {() => { + if (mEvent.isRedacted()) return ; + if (mEvent.getType() === MessageEvent.Sticker) + return ( + ( + } + renderViewer={(p) => } + /> + )} + /> + ); + if (mEvent.getType() === MessageEvent.RoomMessage) { + const editedEvent = getEditedEvent( + evt.event_id, + mEvent, + evtTimeline.getTimelineSet() + ); + const getContent = (() => + editedEvent?.getContent()['m.new_content'] ?? + mEvent.getContent()) as GetContentCallback; + + return ( + + ); + } + if (mEvent.getType() === MessageEvent.RoomMessageEncrypted) + return ( + + + + ); + return ( + + + + ); + }} + + ); + }, [MessageEvent.Sticker]: (event, displayName, getContent) => { if (event.unsigned?.redacted_because) { return ; @@ -398,7 +478,7 @@ const useNotificationsSearchParams = ( [searchParams] ); -const DEFAULT_REFRESH_MS = 10000; +const DEFAULT_REFRESH_MS = 7000; export function Notifications() { const mx = useMatrixClient(); @@ -441,9 +521,7 @@ export function Notifications() { useInterval( useCallback(() => { - if (document.hasFocus()) { - silentReloadTimeline(); - } + silentReloadTimeline(); }, [silentReloadTimeline]), refreshIntervalTime ); diff --git a/src/app/state/room/roomToUnread.ts b/src/app/state/room/roomToUnread.ts index 4ac391fe9..5a009405e 100644 --- a/src/app/state/room/roomToUnread.ts +++ b/src/app/state/room/roomToUnread.ts @@ -45,7 +45,7 @@ export type RoomToUnreadAction = roomId: string; }; -const unreadInfoToUnread = (unreadInfo: UnreadInfo): Unread => ({ +export const unreadInfoToUnread = (unreadInfo: UnreadInfo): Unread => ({ highlight: unreadInfo.highlight, total: unreadInfo.total, from: null, diff --git a/src/app/state/typingMembers.ts b/src/app/state/typingMembers.ts index 88d4687cd..55bf8f628 100644 --- a/src/app/state/typingMembers.ts +++ b/src/app/state/typingMembers.ts @@ -1,6 +1,5 @@ import produce from 'immer'; import { atom, useSetAtom } from 'jotai'; -import { selectAtom } from 'jotai/utils'; import { MatrixClient, RoomMemberEvent, RoomMemberEventHandlerMap } from 'matrix-js-sdk'; import { useEffect } from 'react'; @@ -148,8 +147,3 @@ export const useBindRoomIdToTypingMembersAtom = ( }; }, [mx, setTypingMembers]); }; - -export const selectRoomTypingMembersAtom = ( - roomId: string, - typingMembersAtom: typeof roomIdToTypingMembersAtom -) => selectAtom(typingMembersAtom, (atoms) => atoms.get(roomId) ?? []); diff --git a/src/app/templates/auth/Auth.jsx b/src/app/templates/auth/Auth.jsx deleted file mode 100644 index 7c2117360..000000000 --- a/src/app/templates/auth/Auth.jsx +++ /dev/null @@ -1,684 +0,0 @@ -/* eslint-disable react/prop-types */ -import React, { useState, useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import './Auth.scss'; -import ReCAPTCHA from 'react-google-recaptcha'; -import { Formik } from 'formik'; - -import * as auth from '../../../client/action/auth'; -import cons from '../../../client/state/cons'; -import { Debounce, getUrlPrams } from '../../../util/common'; -import { getBaseUrl } from '../../../util/matrixUtil'; - -import Text from '../../atoms/text/Text'; -import Button from '../../atoms/button/Button'; -import IconButton from '../../atoms/button/IconButton'; -import Input from '../../atoms/input/Input'; -import Spinner from '../../atoms/spinner/Spinner'; -import ScrollView from '../../atoms/scroll/ScrollView'; -import Header, { TitleWrapper } from '../../atoms/header/Header'; -import Avatar from '../../atoms/avatar/Avatar'; -import ContextMenu, { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu'; - -import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg'; -import EyeIC from '../../../../public/res/ic/outlined/eye.svg'; -import EyeBlindIC from '../../../../public/res/ic/outlined/eye-blind.svg'; -import CinnySvg from '../../../../public/res/svg/cinny.svg'; -import SSOButtons from '../../molecules/sso-buttons/SSOButtons'; - -const LOCALPART_SIGNUP_REGEX = /^[a-z0-9_\-.=/]+$/; -const BAD_LOCALPART_ERROR = 'Username can only contain characters a-z, 0-9, or \'=_-./\''; -const USER_ID_TOO_LONG_ERROR = 'Your user ID, including the hostname, can\'t be more than 255 characters long.'; - -const PASSWORD_STRENGHT_REGEX = /^(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[^\w\d\s:])([^\s]){8,127}$/; -const BAD_PASSWORD_ERROR = 'Password must contain at least 1 lowercase, 1 uppercase, 1 number, 1 non-alphanumeric character, 8-127 characters with no space.'; -const CONFIRM_PASSWORD_ERROR = 'Passwords don\'t match.'; - -const EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; -const BAD_EMAIL_ERROR = 'Invalid email address'; - -function isValidInput(value, regex) { - if (typeof regex === 'string') return regex === value; - return regex.test(value); -} -function normalizeUsername(rawUsername) { - const noLeadingAt = rawUsername.indexOf('@') === 0 ? rawUsername.substr(1) : rawUsername; - return noLeadingAt.trim(); -} - -let searchingHs = null; -function Homeserver({ onChange }) { - const [hs, setHs] = useState(null); - const [debounce] = useState(new Debounce()); - const [process, setProcess] = useState({ isLoading: true, message: 'Loading homeserver list...' }); - const hsRef = useRef(); - - const setupHsConfig = async (servername) => { - setProcess({ isLoading: true, message: 'Looking for homeserver...' }); - let baseUrl = null; - baseUrl = await getBaseUrl(servername); - - if (searchingHs !== servername) return; - setProcess({ isLoading: true, message: `Connecting to ${baseUrl}...` }); - const tempClient = auth.createTemporaryClient(baseUrl); - - Promise.allSettled([tempClient.loginFlows(), tempClient.register()]) - .then((values) => { - const loginFlow = values[0].status === 'fulfilled' ? values[0]?.value : undefined; - const registerFlow = values[1].status === 'rejected' ? values[1]?.reason?.data : undefined; - if (loginFlow === undefined || registerFlow === undefined) throw new Error(); - - if (searchingHs !== servername) return; - onChange({ baseUrl, login: loginFlow, register: registerFlow }); - setProcess({ isLoading: false }); - }).catch(() => { - if (searchingHs !== servername) return; - onChange(null); - setProcess({ isLoading: false, error: 'Unable to connect. Please check your input.' }); - }); - }; - - useEffect(() => { - onChange(null); - if (hs === null || hs?.selected.trim() === '') return; - searchingHs = hs.selected; - setupHsConfig(hs.selected); - }, [hs]); - - useEffect(async () => { - const link = window.location.href; - const configFileUrl = `${link}${link[link.length - 1] === '/' ? '' : '/'}config.json`; - try { - const result = await (await fetch(configFileUrl, { method: 'GET' })).json(); - const selectedHs = result?.defaultHomeserver; - const hsList = result?.homeserverList; - const allowCustom = result?.allowCustomHomeservers ?? true; - if (!hsList?.length > 0 || selectedHs < 0 || selectedHs >= hsList?.length) { - throw new Error(); - } - setHs({ selected: hsList[selectedHs], list: hsList, allowCustom }); - } catch { - setHs({ selected: 'matrix.org', list: ['matrix.org'], allowCustom: true }); - } - }, []); - - const handleHsInput = (e) => { - const { value } = e.target; - setProcess({ isLoading: false }); - debounce._(async () => { - setHs({ ...hs, selected: value.trim() }); - }, 700)(); - }; - - return ( - <> -
- - ( - <> - Homeserver list - { - hs?.list.map((hsName) => ( - { - hideMenu(); - hsRef.current.value = hsName; - setHs({ ...hs, selected: hsName }); - }} - > - {hsName} - - )) - } - - )} - render={(toggleMenu) => } - /> -
- {process.error !== undefined && {process.error}} - {process.isLoading && ( -
- - {process.message} -
- )} - - ); -} -Homeserver.propTypes = { - onChange: PropTypes.func.isRequired, -}; - -function Login({ loginFlow, baseUrl }) { - const [typeIndex, setTypeIndex] = useState(0); - const [passVisible, setPassVisible] = useState(false); - const loginTypes = ['Username', 'Email']; - const isPassword = loginFlow?.filter((flow) => flow.type === 'm.login.password')[0]; - const ssoProviders = loginFlow?.filter((flow) => flow.type === 'm.login.sso')[0]; - - const initialValues = { - username: '', password: '', email: '', other: '', - }; - - const validator = (values) => { - const errors = {}; - if (typeIndex === 1 && values.email.length > 0 && !isValidInput(values.email, EMAIL_REGEX)) { - errors.email = BAD_EMAIL_ERROR; - } - return errors; - }; - const submitter = async (values, actions) => { - let userBaseUrl = baseUrl; - let { username } = values; - const mxIdMatch = username.match(/^@(.+):(.+\..+)$/); - if (typeIndex === 0 && mxIdMatch) { - [, username, userBaseUrl] = mxIdMatch; - userBaseUrl = await getBaseUrl(userBaseUrl); - } - - return auth.login( - userBaseUrl, - typeIndex === 0 ? normalizeUsername(username) : undefined, - typeIndex === 1 ? values.email : undefined, - values.password, - ).then(() => { - actions.setSubmitting(true); - window.location.reload(); - }).catch((error) => { - let msg = error.message; - if (msg === 'Unknown message') msg = 'Please check your credentials'; - actions.setErrors({ - password: msg === 'Invalid password' ? msg : undefined, - other: msg !== 'Invalid password' ? msg : undefined, - }); - actions.setSubmitting(false); - }); - }; - - return ( - <> -
- Login - {isPassword && ( - ( - loginTypes.map((type, index) => ( - { - hideMenu(); - setTypeIndex(index); - }} - > - {type} - - )) - )} - render={(toggleMenu) => ( - - )} - /> - )} -
- {isPassword && ( - - {({ - values, errors, handleChange, handleSubmit, isSubmitting, - }) => ( - <> - {isSubmitting && } -
- {typeIndex === 0 && } - {errors.username && {errors.username}} - {typeIndex === 1 && } - {errors.email && {errors.email}} -
- - setPassVisible(!passVisible)} src={passVisible ? EyeIC : EyeBlindIC} size="extra-small" /> -
- {errors.password && {errors.password}} - {errors.other && {errors.other}} -
- -
-
- - )} -
- )} - {ssoProviders && isPassword && OR} - {ssoProviders && ( - - )} - - ); -} -Login.propTypes = { - loginFlow: PropTypes.arrayOf( - PropTypes.shape({}), - ).isRequired, - baseUrl: PropTypes.string.isRequired, -}; - -let sid; -let clientSecret; -function Register({ registerInfo, loginFlow, baseUrl }) { - const [process, setProcess] = useState({}); - const [passVisible, setPassVisible] = useState(false); - const [cPassVisible, setCPassVisible] = useState(false); - const formRef = useRef(); - - const ssoProviders = loginFlow?.filter((flow) => flow.type === 'm.login.sso')[0]; - const isDisabled = registerInfo.errcode !== undefined; - const { flows, params, session } = registerInfo; - - let isEmail = false; - let isEmailRequired = true; - let isRecaptcha = false; - let isTerms = false; - let isDummy = false; - - flows?.forEach((flow) => { - if (isEmailRequired && flow.stages.indexOf('m.login.email.identity') === -1) isEmailRequired = false; - if (!isEmail) isEmail = flow.stages.indexOf('m.login.email.identity') > -1; - if (!isRecaptcha) isRecaptcha = flow.stages.indexOf('m.login.recaptcha') > -1; - if (!isTerms) isTerms = flow.stages.indexOf('m.login.terms') > -1; - if (!isDummy) isDummy = flow.stages.indexOf('m.login.dummy') > -1; - }); - - const initialValues = { - username: '', password: '', confirmPassword: '', email: '', other: '', - }; - - const validator = (values) => { - const errors = {}; - if (values.username.list > 255) errors.username = USER_ID_TOO_LONG_ERROR; - if (values.username.length > 0 && !isValidInput(values.username, LOCALPART_SIGNUP_REGEX)) { - errors.username = BAD_LOCALPART_ERROR; - } - if (values.password.length > 0 && !isValidInput(values.password, PASSWORD_STRENGHT_REGEX)) { - errors.password = BAD_PASSWORD_ERROR; - } - if (values.confirmPassword.length > 0 - && !isValidInput(values.confirmPassword, values.password)) { - errors.confirmPassword = CONFIRM_PASSWORD_ERROR; - } - if (values.email.length > 0 && !isValidInput(values.email, EMAIL_REGEX)) { - errors.email = BAD_EMAIL_ERROR; - } - return errors; - }; - const submitter = (values, actions) => { - const tempClient = auth.createTemporaryClient(baseUrl); - clientSecret = tempClient.generateClientSecret(); - return tempClient.isUsernameAvailable(values.username) - .then(async (isAvail) => { - if (!isAvail) { - actions.setErrors({ username: 'Username is already taken' }); - actions.setSubmitting(false); - return; - } - if (isEmail && values.email.length > 0) { - const result = await auth.verifyEmail(baseUrl, values.email, clientSecret, 1); - if (result.errcode) { - if (result.errcode === 'M_THREEPID_IN_USE') actions.setErrors({ email: result.error }); - else actions.setErrors({ others: result.error || result.message }); - actions.setSubmitting(false); - return; - } - sid = result.sid; - } - setProcess({ type: 'processing', message: 'Registration in progress....' }); - actions.setSubmitting(false); - }).catch((err) => { - const msg = err.message || err.error; - if (['M_USER_IN_USE', 'M_INVALID_USERNAME', 'M_EXCLUSIVE'].indexOf(err.errcode) > -1) { - actions.setErrors({ username: err.errcode === 'M_USER_IN_USE' ? 'Username is already taken' : msg }); - } else if (msg) actions.setErrors({ other: msg }); - - actions.setSubmitting(false); - }); - }; - - const refreshWindow = () => window.location.reload(); - - const getInputs = () => { - const f = formRef.current; - return [f.username.value, f.password.value, f?.email?.value]; - }; - - useEffect(() => { - if (process.type !== 'processing') return; - const asyncProcess = async () => { - const [username, password, email] = getInputs(); - const d = await auth.completeRegisterStage(baseUrl, username, password, { session }); - - if (isRecaptcha && !d.completed.includes('m.login.recaptcha')) { - const sitekey = params['m.login.recaptcha'].public_key; - setProcess({ type: 'm.login.recaptcha', sitekey }); - return; - } - if (isTerms && !d.completed.includes('m.login.terms')) { - const pp = params['m.login.terms'].policies.privacy_policy; - const url = pp?.en.url || pp[Object.keys(pp)[0]].url; - setProcess({ type: 'm.login.terms', url }); - return; - } - if (isEmail && email.length > 0) { - setProcess({ type: 'm.login.email.identity', email }); - return; - } - if (isDummy) { - const data = await auth.completeRegisterStage(baseUrl, username, password, { - type: 'm.login.dummy', - session, - }); - if (data.done) refreshWindow(); - } - }; - asyncProcess(); - }, [process]); - - const handleRecaptcha = async (value) => { - if (typeof value !== 'string') return; - const [username, password] = getInputs(); - const d = await auth.completeRegisterStage(baseUrl, username, password, { - type: 'm.login.recaptcha', - response: value, - session, - }); - if (d.done) refreshWindow(); - else setProcess({ type: 'processing', message: 'Registration in progress...' }); - }; - const handleTerms = async () => { - const [username, password] = getInputs(); - const d = await auth.completeRegisterStage(baseUrl, username, password, { - type: 'm.login.terms', - session, - }); - if (d.done) refreshWindow(); - else setProcess({ type: 'processing', message: 'Registration in progress...' }); - }; - const handleEmailVerify = async () => { - const [username, password] = getInputs(); - const d = await auth.completeRegisterStage(baseUrl, username, password, { - type: 'm.login.email.identity', - threepidCreds: { sid, client_secret: clientSecret }, - threepid_creds: { sid, client_secret: clientSecret }, - session, - }); - if (d.done) refreshWindow(); - else setProcess({ type: 'processing', message: 'Registration in progress...' }); - }; - - return ( - <> - {process.type === 'processing' && } - {process.type === 'm.login.recaptcha' && } - {process.type === 'm.login.terms' && } - {process.type === 'm.login.email.identity' && } -
- {!isDisabled && Register} - {isDisabled && {registerInfo.error}} -
- {!isDisabled && ( - - {({ - values, errors, handleChange, handleSubmit, isSubmitting, - }) => ( - <> - {process.type === undefined && isSubmitting && } -
- - {errors.username && {errors.username}} -
- - setPassVisible(!passVisible)} src={passVisible ? EyeIC : EyeBlindIC} size="extra-small" /> -
- {errors.password && {errors.password}} -
- - setCPassVisible(!cPassVisible)} src={cPassVisible ? EyeIC : EyeBlindIC} size="extra-small" /> -
- {errors.confirmPassword && {errors.confirmPassword}} - {isEmail && } - {errors.email && {errors.email}} - {errors.other && {errors.other}} -
- -
-
- - )} -
- )} - {isDisabled && ssoProviders && ( - - )} - - ); -} -Register.propTypes = { - registerInfo: PropTypes.shape({}).isRequired, - loginFlow: PropTypes.arrayOf( - PropTypes.shape({}), - ).isRequired, - baseUrl: PropTypes.string.isRequired, -}; - -function AuthCard() { - const [hsConfig, setHsConfig] = useState(null); - const [type, setType] = useState('login'); - - const handleHsChange = (info) => { - console.log(info); - setHsConfig(info); - }; - - return ( - <> - - { hsConfig !== null && ( - type === 'login' - ? - : ( - - ) - )} - { hsConfig !== null && ( - - {`${(type === 'login' ? 'Don\'t have' : 'Already have')} an account?`} - - - )} - - ); -} - -function Auth() { - const [loginToken, setLoginToken] = useState(getUrlPrams('loginToken')); - - useEffect(async () => { - if (!loginToken) return; - if (localStorage.getItem(cons.secretKey.BASE_URL) === undefined) { - setLoginToken(null); - return; - } - const baseUrl = localStorage.getItem(cons.secretKey.BASE_URL); - try { - await auth.loginWithToken(baseUrl, loginToken); - - const { href } = window.location; - window.location.replace(href.slice(0, href.indexOf('?'))); - } catch { - setLoginToken(null); - } - }, []); - - return ( - -
-
- {loginToken && } - {!loginToken && ( -
-
- - - Cinny - -
-
- -
-
- )} -
- - -
-
- ); -} - -function LoadingScreen({ message }) { - return ( - - -
- {message} -
-
- ); -} -LoadingScreen.propTypes = { - message: PropTypes.string.isRequired, -}; - -function Recaptcha({ message, sitekey, onChange }) { - return ( - -
- {message} -
- -
- ); -} -Recaptcha.propTypes = { - message: PropTypes.string.isRequired, - sitekey: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, -}; - -function Terms({ url, onSubmit }) { - return ( - -
{ e.preventDefault(); onSubmit(); }}> -
- Agree with terms -
- In order to complete registration, you need to agree to the terms and conditions. -
- - - {'I accept '} - Terms and Conditions - -
- -
- - - ); -} -Terms.propTypes = { - url: PropTypes.string.isRequired, - onSubmit: PropTypes.func.isRequired, -}; - -function EmailVerify({ email, onContinue }) { - return ( - -
- Verify email -
- - {'Please check your email '} - {`(${email})`} - {' and validate before continuing further.'} - -
- -
-
- ); -} -EmailVerify.propTypes = { - email: PropTypes.string.isRequired, -}; - -function ProcessWrapper({ children }) { - return ( -
- {children} -
- ); -} -ProcessWrapper.propTypes = { - children: PropTypes.node.isRequired, -}; - -export default Auth; diff --git a/src/app/templates/auth/Auth.scss b/src/app/templates/auth/Auth.scss deleted file mode 100644 index 956a27004..000000000 --- a/src/app/templates/auth/Auth.scss +++ /dev/null @@ -1,173 +0,0 @@ -@use '../../partials/flex'; -@use '../../partials/dir'; - -.auth__base { - --pattern-size: 48px; - min-height: 100%; - background-color: var(--bg-surface-low); - - background-image: radial-gradient(rgba(0, 0, 0, 6%) 2px, rgba(0, 0, 0, 0%) 2px); - background-size: var(--pattern-size) var(--pattern-size); - - display: flex; - flex-direction: column; -} -.auth__wrapper { - flex: 1; - padding: var(--sp-loose); - padding-bottom: 0; - display: flex; - justify-content: center; - align-items: flex-start; -} -.auth-footer { - padding: var(--sp-normal) 0; - display: flex; - justify-content: center; - align-items: center; - - & > *:nth-child(2n) { - margin: 0 var(--sp-loose); - } - & a { - color: var(--tc-surface-normal); - &:hover { text-decoration: underline; } - } -} -.auth-card { - width: 462px; - background-color: var(--bg-surface); - border-radius: var(--bo-radius); - box-shadow: var(--bs-popup); - overflow: hidden; - - &__content { - padding: var(--sp-extra-loose) calc(var(--sp-normal) + var(--sp-extra-loose)); - } - &__switch { - margin-top: var(--sp-loose) !important; - } -} - -.homeserver-form, -.auth-form__heading { - & .context-menu__item .text { - margin: 0 !important; - } -} - -.homeserver-form { - display: flex; - margin-bottom: var(--sp-extra-tight); - align-items: flex-end; - & > .input-container { - flex: 1; - & .input { - background-color: var(--bg-surface); - @include dir.prop(border-right-width, 0, 1px); - @include dir.prop(border-left-width, 1px, 0 ); - @include dir.prop(border-radius, - var(--bo-radius) 0 0 var(--bo-radius), - 0 var(--bo-radius) var(--bo-radius) 0, - ); - } - } - & .ic-btn { - height: 46px; - border: 1px solid var(--bg-surface-border); - @include dir.prop(border-radius, - 0 var(--bo-radius) var(--bo-radius) 0, - var(--bo-radius) 0 0 var(--bo-radius), - ); - } - - &__status { - margin-top: var(--sp-normal); - & .donut-spinner { - min-width: 28px; - } - & .text { - margin: 0 var(--sp-tight); - } - } - &__error { - margin-bottom: var(--sp-normal) !important; - color: var(--tc-danger-normal) !important; - } -} - -.auth-form { - & > .input-container, - &__pass-eye-wrapper { - margin: var(--sp-tight) 0 var(--sp-ultra-tight); - } - - &__heading { - display: flex; - justify-content: space-between; - margin-top: calc(var(--sp-extra-loose) + var(--sp-tight)); - } - - &__pass-eye-wrapper { - position: relative; - & .ic-btn { - position: absolute; - @include dir.prop(right, 6px, unset); - @include dir.prop(left, unset, 6px ); - bottom: 6px; - border-radius: 4px; - } - & input { - @include dir.side(padding, var(--sp-normal), 46px); - } - } - - &__btns { - padding-top: var(--sp-loose); - margin-bottom: var(--sp-extra-loose); - display: flex; - justify-content: flex-end; - } - - &__error { - color: var(--tc-danger-normal) !important; - } -} -.sso__divider { - margin-bottom: var(--sp-tight); - display: flex; - align-items: center; - - &::before, - &::after { - flex: 1; - content: ''; - margin: var(--sp-tight); - border-bottom: 1px solid var(--bg-surface-border); - } -} - -@media (max-width: 462px) { - .auth__wrapper { - padding: var(--sp-tight); - } - .auth-card { - &__content { - padding: var(--sp-loose) var(--sp-normal); - } - } -} - -.process-wrapper { - @extend .cp-fx__column--c-c; - - min-height: 100%; - width: 100%; - background-color: var(--bg-surface-low); - opacity: .96; - - position: fixed; - top: 0; - left: 0; - z-index: 999; -} \ No newline at end of file diff --git a/src/app/templates/client/Client.jsx b/src/app/templates/client/Client.jsx deleted file mode 100644 index f6ef2b9e1..000000000 --- a/src/app/templates/client/Client.jsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import './Client.scss'; - -import ReusableContextMenu from '../../atoms/context-menu/ReusableContextMenu'; -import Windows from '../../organisms/pw/Windows'; -import Dialogs from '../../organisms/pw/Dialogs'; - -import navigation from '../../../client/state/navigation'; -import cons from '../../../client/state/cons'; - -import { ClientContent } from './ClientContent'; -import { useSetting } from '../../state/hooks/settings'; -import { settingsAtom } from '../../state/settings'; - -function SystemEmojiFeature() { - const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); - - if (twitterEmoji) { - document.documentElement.style.setProperty('--font-emoji', 'Twemoji'); - } else { - document.documentElement.style.setProperty('--font-emoji', 'Twemoji_DISABLED'); - } - - return null; -} - -function Client() { - const classNameHidden = 'client__item-hidden'; - - const navWrapperRef = useRef(null); - const roomWrapperRef = useRef(null); - - function onRoomSelected() { - navWrapperRef.current?.classList.add(classNameHidden); - roomWrapperRef.current?.classList.remove(classNameHidden); - } - function onNavigationSelected() { - navWrapperRef.current?.classList.remove(classNameHidden); - roomWrapperRef.current?.classList.add(classNameHidden); - } - - useEffect(() => { - navigation.on(cons.events.navigation.ROOM_SELECTED, onRoomSelected); - navigation.on(cons.events.navigation.NAVIGATION_OPENED, onNavigationSelected); - - return () => { - navigation.removeListener(cons.events.navigation.ROOM_SELECTED, onRoomSelected); - navigation.removeListener(cons.events.navigation.NAVIGATION_OPENED, onNavigationSelected); - }; - }, []); - - return ( -
- {/*
- -
*/} -
- -
- - - - -
- ); -} - -export default Client; diff --git a/src/app/templates/client/Client.scss b/src/app/templates/client/Client.scss deleted file mode 100644 index bad5fc93b..000000000 --- a/src/app/templates/client/Client.scss +++ /dev/null @@ -1,57 +0,0 @@ -@use '../../partials/screen'; - -.client-container { - display: flex; - height: 100%; - flex-grow: 1; -} - -.navigation__wrapper { - width: var(--navigation-width); - - @include screen.smallerThan(mobileBreakpoint) { - width: 100%; - } -} - -.room__wrapper { - flex: 1; - min-width: 0; -} - -@include screen.smallerThan(mobileBreakpoint) { - .client__item-hidden { - display: none; - } -} - -.loading-display { - position: absolute; - top: 0; - left: 0; - width: 100vw; - height: 100%; - - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} -.loading__message { - margin-top: var(--sp-normal); - max-width: 350px; - text-align: center; -} -.loading__appname { - position: absolute; - bottom: var(--sp-normal); -} -.loading__menu { - position: absolute; - top: var(--sp-extra-tight); - right: var(--sp-extra-tight); - cursor: pointer; - .context-menu__item .text { - margin: 0 !important; - } -} diff --git a/src/app/templates/client/ClientContent.jsx b/src/app/templates/client/ClientContent.jsx deleted file mode 100644 index cebe012c2..000000000 --- a/src/app/templates/client/ClientContent.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -import initMatrix from '../../../client/initMatrix'; -import cons from '../../../client/state/cons'; -import navigation from '../../../client/state/navigation'; -import { openNavigation } from '../../../client/action/navigation'; - -import Welcome from '../../organisms/welcome/Welcome'; -import { RoomBaseView } from '../../features/room/Room'; - -export function ClientContent() { - const [roomInfo, setRoomInfo] = useState({ - room: null, - eventId: null, - }); - - const mx = initMatrix.matrixClient; - - useEffect(() => { - const handleRoomSelected = (rId, pRoomId, eId) => { - roomInfo.roomTimeline?.removeInternalListeners(); - const r = mx.getRoom(rId); - if (r) { - setRoomInfo({ - room: r, - eventId: eId ?? null, - }); - } else { - setRoomInfo({ - room: null, - eventId: null, - }); - } - }; - - navigation.on(cons.events.navigation.ROOM_SELECTED, handleRoomSelected); - return () => { - navigation.removeListener(cons.events.navigation.ROOM_SELECTED, handleRoomSelected); - }; - }, [roomInfo, mx]); - - const { room, eventId } = roomInfo; - if (!room) { - setTimeout(() => openNavigation()); - return ; - } - - return ; -} diff --git a/src/app/utils/disposable.ts b/src/app/utils/disposable.ts index 7840fe494..744241546 100644 --- a/src/app/utils/disposable.ts +++ b/src/app/utils/disposable.ts @@ -1,8 +1,16 @@ -export type DisposeCallback = (...args: Q) => R; -export type DisposableContext

= ( - ...args: P -) => DisposeCallback; +export type DisposeCallback = ( + ...args: DisposeArgs +) => DisposeReturn; +export type DisposableContext< + DisposableArgs extends unknown[] = [], + DisposeArgs extends unknown[] = [], + DisposeReturn = void +> = (...args: DisposableArgs) => DisposeCallback; -export const disposable =

( - context: DisposableContext +export const disposable = < + DisposableArgs extends unknown[], + DisposeArgs extends unknown[] = [], + DisposeReturn = void +>( + context: DisposableContext ) => context; diff --git a/src/app/utils/dom.ts b/src/app/utils/dom.ts index f39fe623a..1aea6754c 100644 --- a/src/app/utils/dom.ts +++ b/src/app/utils/dom.ts @@ -190,3 +190,9 @@ export const copyToClipboard = (text: string) => { copyInput.remove(); } }; + +export const setFavicon = (url: string): void => { + const favicon = document.querySelector('#favicon'); + if (!favicon) return; + favicon.setAttribute('href', url); +}; diff --git a/src/client/action/accountData.js b/src/client/action/accountData.js deleted file mode 100644 index 1fb49fbfe..000000000 --- a/src/client/action/accountData.js +++ /dev/null @@ -1,41 +0,0 @@ -import appDispatcher from '../dispatcher'; -import cons from '../state/cons'; - -/** - * @param {string | string[]} roomId - room id or array of them to add into shortcuts - */ -export function createSpaceShortcut(roomId) { - appDispatcher.dispatch({ - type: cons.actions.accountData.CREATE_SPACE_SHORTCUT, - roomId, - }); -} - -export function deleteSpaceShortcut(roomId) { - appDispatcher.dispatch({ - type: cons.actions.accountData.DELETE_SPACE_SHORTCUT, - roomId, - }); -} - -export function moveSpaceShortcut(roomId, toIndex) { - appDispatcher.dispatch({ - type: cons.actions.accountData.MOVE_SPACE_SHORTCUTS, - roomId, - toIndex, - }); -} - -export function categorizeSpace(roomId) { - appDispatcher.dispatch({ - type: cons.actions.accountData.CATEGORIZE_SPACE, - roomId, - }); -} - -export function unCategorizeSpace(roomId) { - appDispatcher.dispatch({ - type: cons.actions.accountData.UNCATEGORIZE_SPACE, - roomId, - }); -} diff --git a/src/client/action/auth.js b/src/client/action/auth.js deleted file mode 100644 index f04306b8f..000000000 --- a/src/client/action/auth.js +++ /dev/null @@ -1,104 +0,0 @@ -import * as sdk from 'matrix-js-sdk'; -import cons from '../state/cons'; - -function updateLocalStore(accessToken, deviceId, userId, baseUrl) { - localStorage.setItem(cons.secretKey.ACCESS_TOKEN, accessToken); - localStorage.setItem(cons.secretKey.DEVICE_ID, deviceId); - localStorage.setItem(cons.secretKey.USER_ID, userId); - localStorage.setItem(cons.secretKey.BASE_URL, baseUrl); -} - -function createTemporaryClient(baseUrl) { - return sdk.createClient({ baseUrl }); -} - -async function startSsoLogin(baseUrl, type, idpId) { - const client = createTemporaryClient(baseUrl); - localStorage.setItem(cons.secretKey.BASE_URL, client.baseUrl); - window.location.href = client.getSsoLoginUrl(window.location.href, type, idpId); -} - -async function login(baseUrl, username, email, password) { - const identifier = {}; - if (username) { - identifier.type = 'm.id.user'; - identifier.user = username; - } else if (email) { - identifier.type = 'm.id.thirdparty'; - identifier.medium = 'email'; - identifier.address = email; - } else throw new Error('Bad Input'); - - const client = createTemporaryClient(baseUrl); - const res = await client.login('m.login.password', { - identifier, - password, - initial_device_display_name: cons.DEVICE_DISPLAY_NAME, - }); - - const myBaseUrl = res?.well_known?.['m.homeserver']?.base_url || client.baseUrl; - updateLocalStore(res.access_token, res.device_id, res.user_id, myBaseUrl); -} - -async function loginWithToken(baseUrl, token) { - const client = createTemporaryClient(baseUrl); - - const res = await client.login('m.login.token', { - token, - initial_device_display_name: cons.DEVICE_DISPLAY_NAME, - }); - - const myBaseUrl = res?.well_known?.['m.homeserver']?.base_url || client.baseUrl; - updateLocalStore(res.access_token, res.device_id, res.user_id, myBaseUrl); -} - -// eslint-disable-next-line camelcase -async function verifyEmail(baseUrl, email, client_secret, send_attempt, next_link) { - const res = await fetch(`${baseUrl}/_matrix/client/r0/register/email/requestToken`, { - method: 'POST', - body: JSON.stringify({ - email, client_secret, send_attempt, next_link, - }), - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - credentials: 'same-origin', - }); - const data = await res.json(); - return data; -} - -async function completeRegisterStage( - baseUrl, username, password, auth, -) { - const tempClient = createTemporaryClient(baseUrl); - - try { - const result = await tempClient.registerRequest({ - username, - password, - auth, - initial_device_display_name: cons.DEVICE_DISPLAY_NAME, - }); - const data = { completed: result.completed || [] }; - if (result.access_token) { - data.done = true; - updateLocalStore(result.access_token, result.device_id, result.user_id, baseUrl); - } - return data; - } catch (e) { - const result = e.data; - const data = { completed: result.completed || [] }; - if (result.access_token) { - data.done = true; - updateLocalStore(result.access_token, result.device_id, result.user_id, baseUrl); - } - return data; - } -} - -export { - updateLocalStore, createTemporaryClient, login, verifyEmail, - loginWithToken, startSsoLogin, - completeRegisterStage, -}; diff --git a/src/client/action/auth.ts b/src/client/action/auth.ts new file mode 100644 index 000000000..dbe9baac5 --- /dev/null +++ b/src/client/action/auth.ts @@ -0,0 +1,13 @@ +import cons from '../state/cons'; + +export function updateLocalStore( + accessToken: string, + deviceId: string, + userId: string, + baseUrl: string +) { + localStorage.setItem(cons.secretKey.ACCESS_TOKEN, accessToken); + localStorage.setItem(cons.secretKey.DEVICE_ID, deviceId); + localStorage.setItem(cons.secretKey.USER_ID, userId); + localStorage.setItem(cons.secretKey.BASE_URL, baseUrl); +} diff --git a/src/client/action/navigation.js b/src/client/action/navigation.js index e48e839b3..1967a463b 100644 --- a/src/client/action/navigation.js +++ b/src/client/action/navigation.js @@ -1,35 +1,6 @@ import appDispatcher from '../dispatcher'; import cons from '../state/cons'; -export function selectTab(tabId) { - appDispatcher.dispatch({ - type: cons.actions.navigation.SELECT_TAB, - tabId, - }); -} - -export function selectSpace(roomId) { - appDispatcher.dispatch({ - type: cons.actions.navigation.SELECT_SPACE, - roomId, - }); -} - -export function selectRoom(roomId, eventId) { - appDispatcher.dispatch({ - type: cons.actions.navigation.SELECT_ROOM, - roomId, - eventId, - }); -} - -// Open navigation on compact screen sizes -export function openNavigation() { - appDispatcher.dispatch({ - type: cons.actions.navigation.OPEN_NAVIGATION, - }); -} - export function openSpaceSettings(roomId, tabText) { appDispatcher.dispatch({ type: cons.actions.navigation.OPEN_SPACE_SETTINGS, @@ -38,13 +9,6 @@ export function openSpaceSettings(roomId, tabText) { }); } -export function openSpaceManage(roomId) { - appDispatcher.dispatch({ - type: cons.actions.navigation.OPEN_SPACE_MANAGE, - roomId, - }); -} - export function openSpaceAddExisting(roomId, spaces = false) { appDispatcher.dispatch({ type: cons.actions.navigation.OPEN_SPACE_ADDEXISTING, @@ -61,24 +25,6 @@ export function toggleRoomSettings(roomId, tabText) { }); } -export function openShortcutSpaces() { - appDispatcher.dispatch({ - type: cons.actions.navigation.OPEN_SHORTCUT_SPACES, - }); -} - -export function openInviteList() { - appDispatcher.dispatch({ - type: cons.actions.navigation.OPEN_INVITE_LIST, - }); -} - -export function openPublicRooms(searchTerm) { - appDispatcher.dispatch({ - type: cons.actions.navigation.OPEN_PUBLIC_ROOMS, - searchTerm, - }); -} export function openCreateRoom(isSpace = false, parentId = null) { appDispatcher.dispatch({ @@ -118,39 +64,6 @@ export function openSettings(tabText) { }); } -export function openEmojiBoard(cords, requestEmojiCallback) { - appDispatcher.dispatch({ - type: cons.actions.navigation.OPEN_EMOJIBOARD, - cords, - requestEmojiCallback, - }); -} - -export function openReadReceipts(roomId, userIds) { - appDispatcher.dispatch({ - type: cons.actions.navigation.OPEN_READRECEIPTS, - roomId, - userIds, - }); -} - -export function openViewSource(event) { - appDispatcher.dispatch({ - type: cons.actions.navigation.OPEN_VIEWSOURCE, - event, - }); -} - -export function replyTo(userId, eventId, body, formattedBody) { - appDispatcher.dispatch({ - type: cons.actions.navigation.CLICK_REPLY_TO, - userId, - eventId, - body, - formattedBody, - }); -} - export function openSearch(term) { appDispatcher.dispatch({ type: cons.actions.navigation.OPEN_SEARCH, diff --git a/src/client/action/notifications.js b/src/client/action/notifications.js index a869632a8..579c7c3cb 100644 --- a/src/client/action/notifications.js +++ b/src/client/action/notifications.js @@ -5,7 +5,6 @@ export async function markAsRead(roomId) { const mx = initMatrix.matrixClient; const room = mx.getRoom(roomId); if (!room) return; - initMatrix.notifications.deleteNoti(roomId); const timeline = room.getLiveTimeline().getEvents(); const readEventId = room.getEventReadUpTo(mx.getUserId()); diff --git a/src/client/action/room.js b/src/client/action/room.js index 996c26802..c2d114388 100644 --- a/src/client/action/room.js +++ b/src/client/action/room.js @@ -107,37 +107,12 @@ async function join(roomIdOrAlias, isDM = false, via = undefined) { const targetUserId = guessDMRoomTargetId(mx.getRoom(resultRoom.roomId), mx.getUserId()); await addRoomToMDirect(resultRoom.roomId, targetUserId); } - appDispatcher.dispatch({ - type: cons.actions.room.JOIN, - roomId: resultRoom.roomId, - isDM, - }); return resultRoom.roomId; } catch (e) { throw new Error(e); } } -/** - * - * @param {string} roomId - * @param {boolean} isDM - */ -async function leave(roomId) { - const mx = initMatrix.matrixClient; - const isDM = initMatrix.roomList.directs.has(roomId); - try { - await mx.leave(roomId); - appDispatcher.dispatch({ - type: cons.actions.room.LEAVE, - roomId, - isDM, - }); - } catch { - console.error('Unable to leave room.'); - } -} - async function create(options, isDM = false) { const mx = initMatrix.matrixClient; try { @@ -145,11 +120,6 @@ async function create(options, isDM = false) { if (isDM && typeof options.invite?.[0] === 'string') { await addRoomToMDirect(result.room_id, options.invite[0]); } - appDispatcher.dispatch({ - type: cons.actions.room.CREATE, - roomId: result.room_id, - isDM, - }); return result; } catch (e) { const errcodes = ['M_UNKNOWN', 'M_BAD_JSON', 'M_ROOM_IN_USE', 'M_INVALID_ROOM_STATE', 'M_UNSUPPORTED_ROOM_VERSION']; @@ -348,7 +318,7 @@ async function setMyRoomAvatar(roomId, mxc) { export { convertToDm, convertToRoom, - join, leave, + join, createDM, createRoom, invite, kick, ban, unban, ignore, unignore, diff --git a/src/client/action/roomTimeline.js b/src/client/action/roomTimeline.js deleted file mode 100644 index 41c62d4f9..000000000 --- a/src/client/action/roomTimeline.js +++ /dev/null @@ -1,34 +0,0 @@ -import initMatrix from '../initMatrix'; - -async function redactEvent(roomId, eventId, reason) { - const mx = initMatrix.matrixClient; - - try { - await mx.redactEvent(roomId, eventId, undefined, typeof reason === 'undefined' ? undefined : { reason }); - return true; - } catch (e) { - throw new Error(e); - } -} - -async function sendReaction(roomId, toEventId, reaction, shortcode) { - const mx = initMatrix.matrixClient; - const content = { - 'm.relates_to': { - event_id: toEventId, - key: reaction, - rel_type: 'm.annotation', - }, - }; - if (typeof shortcode === 'string') content.shortcode = shortcode; - try { - await mx.sendEvent(roomId, 'm.reaction', content); - } catch (e) { - throw new Error(e); - } -} - -export { - redactEvent, - sendReaction, -}; diff --git a/src/client/event/hotkeys.js b/src/client/event/hotkeys.js index 076d37941..856fcadc4 100644 --- a/src/client/event/hotkeys.js +++ b/src/client/event/hotkeys.js @@ -1,31 +1,5 @@ import { openSearch } from '../action/navigation'; import navigation from '../state/navigation'; -import { markAsRead } from '../action/notifications'; - -function shouldFocusMessageField(code) { - // do not focus on F keys - if (/^F\d+$/.test(code)) return false; - - // do not focus on numlock/scroll lock - if ( - code.metaKey - || code.startsWith('OS') - || code.startsWith('Meta') - || code.startsWith('Shift') - || code.startsWith('Alt') - || code.startsWith('Control') - || code.startsWith('Arrow') - || code === 'Tab' - || code === 'Space' - || code === 'Enter' - || code === 'NumLock' - || code === 'ScrollLock' - ) { - return false; - } - - return true; -} function listenKeyboard(event) { // Ctrl/Cmd + @@ -36,39 +10,6 @@ function listenKeyboard(event) { if (navigation.isRawModalVisible) return; openSearch(); } - - // focus message field on paste - if (event.key === 'v') { - if (navigation.isRawModalVisible) return; - const msgTextarea = document.getElementById('message-textarea'); - const { activeElement } = document; - if (activeElement !== msgTextarea - && ['input', 'textarea'].includes(activeElement.tagName.toLowerCase()) - ) return; - msgTextarea?.focus(); - } - } - - if (!event.ctrlKey && !event.altKey && !event.metaKey) { - if (navigation.isRawModalVisible) return; - - if (event.key === 'Escape') { - if (navigation.selectedRoomId) { - markAsRead(navigation.selectedRoomId); - return; - } - } - - if (['input', 'textarea'].includes(document.activeElement.tagName.toLowerCase())) { - return; - } - - // focus the text field on most keypresses - if (shouldFocusMessageField(event.code)) { - // press any key to focus and type in message field - const msgTextarea = document.getElementById('message-textarea'); - msgTextarea?.focus(); - } } } diff --git a/src/client/event/roomList.js b/src/client/event/roomList.js deleted file mode 100644 index 6592d67f5..000000000 --- a/src/client/event/roomList.js +++ /dev/null @@ -1,38 +0,0 @@ -import cons from '../state/cons'; -import navigation from '../state/navigation'; -import { selectTab, selectSpace, selectRoom } from '../action/navigation'; - -function initRoomListListener(roomList) { - const listenRoomLeave = (roomId) => { - const parents = roomList.roomIdToParents.get(roomId); - - if (parents) { - [...parents].forEach((pId) => { - const data = navigation.spaceToRoom.get(pId); - if (data?.roomId === roomId) { - navigation.spaceToRoom.delete(pId); - } - }); - } - - if (navigation.selectedRoomId === roomId) { - selectRoom(null); - } - - if (navigation.selectedSpacePath.includes(roomId)) { - const idIndex = navigation.selectedSpacePath.indexOf(roomId); - if (idIndex === 0) selectTab(cons.tabs.HOME); - else selectSpace(navigation.selectedSpacePath[idIndex - 1]); - } - - navigation.removeRecentRoom(roomId); - }; - - roomList.on(cons.events.roomList.ROOM_LEAVED, listenRoomLeave); - return () => { - roomList.removeListener(cons.events.roomList.ROOM_LEAVED, listenRoomLeave); - }; -} - -// eslint-disable-next-line import/prefer-default-export -export { initRoomListListener }; diff --git a/src/client/initMatrix.js b/src/client/initMatrix.js index 835982f81..0352ff363 100644 --- a/src/client/initMatrix.js +++ b/src/client/initMatrix.js @@ -4,12 +4,7 @@ import Olm from '@matrix-org/olm'; import { logger } from 'matrix-js-sdk/lib/logger'; import { getSecret } from './state/auth'; -import RoomList from './state/RoomList'; -import AccountData from './state/AccountData'; -import RoomsInput from './state/RoomsInput'; -import Notifications from './state/Notifications'; import { cryptoCallbacks } from './state/secretStorageKeys'; -import navigation from './state/navigation'; global.Olm = Olm; @@ -18,12 +13,6 @@ if (import.meta.env.PROD) { } class InitMatrix extends EventEmitter { - constructor() { - super(); - - navigation.initMatrix = this; - } - async init() { if (this.matrixClient || this.initializing) { console.warn('Client is already initialized!') @@ -84,17 +73,9 @@ class InitMatrix extends EventEmitter { PREPARED: (prevState) => { console.log('PREPARED state'); console.log('Previous state: ', prevState); - // TODO: remove global.initMatrix at end global.initMatrix = this; if (prevState === null) { - this.roomList = new RoomList(this.matrixClient); - this.accountData = new AccountData(this.roomList); - this.roomsInput = new RoomsInput(this.matrixClient, this.roomList); - this.notifications = new Notifications(this.roomList); this.emit('init_loading_finished'); - this.notifications._initNoti(); - } else { - this.notifications?._initNoti(); } }, RECONNECTING: () => { diff --git a/src/client/state/AccountData.js b/src/client/state/AccountData.js deleted file mode 100644 index 6fc811a39..000000000 --- a/src/client/state/AccountData.js +++ /dev/null @@ -1,144 +0,0 @@ -import EventEmitter from 'events'; -import appDispatcher from '../dispatcher'; -import cons from './cons'; - -class AccountData extends EventEmitter { - constructor(roomList) { - super(); - - this.matrixClient = roomList.matrixClient; - this.roomList = roomList; - this.spaces = roomList.spaces; - - this.spaceShortcut = new Set(); - this._populateSpaceShortcut(); - - this.categorizedSpaces = new Set(); - this._populateCategorizedSpaces(); - - this._listenEvents(); - - appDispatcher.register(this.accountActions.bind(this)); - } - - _getAccountData() { - return this.matrixClient.getAccountData(cons.IN_CINNY_SPACES)?.getContent() || {}; - } - - _populateSpaceShortcut() { - this.spaceShortcut.clear(); - const spacesContent = this._getAccountData(); - - if (spacesContent?.shortcut === undefined) return; - - spacesContent.shortcut.forEach((shortcut) => { - if (this.spaces.has(shortcut)) this.spaceShortcut.add(shortcut); - }); - if (spacesContent.shortcut.length !== this.spaceShortcut.size) { - // update shortcut list from account data if shortcut space doesn't exist. - // TODO: we can wait for sync to complete or else we may end up removing valid space id - this._updateSpaceShortcutData([...this.spaceShortcut]); - } - } - - _updateSpaceShortcutData(shortcutList) { - const spaceContent = this._getAccountData(); - spaceContent.shortcut = shortcutList; - this.matrixClient.setAccountData(cons.IN_CINNY_SPACES, spaceContent); - } - - _populateCategorizedSpaces() { - this.categorizedSpaces.clear(); - const spaceContent = this._getAccountData(); - - if (spaceContent?.categorized === undefined) return; - - spaceContent.categorized.forEach((spaceId) => { - if (this.spaces.has(spaceId)) this.categorizedSpaces.add(spaceId); - }); - if (spaceContent.categorized.length !== this.categorizedSpaces.size) { - // TODO: we can wait for sync to complete or else we may end up removing valid space id - this._updateCategorizedSpacesData([...this.categorizedSpaces]); - } - } - - _updateCategorizedSpacesData(categorizedSpaceList) { - const spaceContent = this._getAccountData(); - spaceContent.categorized = categorizedSpaceList; - this.matrixClient.setAccountData(cons.IN_CINNY_SPACES, spaceContent); - } - - accountActions(action) { - const actions = { - [cons.actions.accountData.CREATE_SPACE_SHORTCUT]: () => { - const addRoomId = (id) => { - if (this.spaceShortcut.has(id)) return; - this.spaceShortcut.add(id); - }; - if (Array.isArray(action.roomId)) { - action.roomId.forEach(addRoomId); - } else { - addRoomId(action.roomId); - } - this._updateSpaceShortcutData([...this.spaceShortcut]); - this.emit(cons.events.accountData.SPACE_SHORTCUT_UPDATED, action.roomId); - }, - [cons.actions.accountData.DELETE_SPACE_SHORTCUT]: () => { - if (!this.spaceShortcut.has(action.roomId)) return; - this.spaceShortcut.delete(action.roomId); - this._updateSpaceShortcutData([...this.spaceShortcut]); - this.emit(cons.events.accountData.SPACE_SHORTCUT_UPDATED, action.roomId); - }, - [cons.actions.accountData.MOVE_SPACE_SHORTCUTS]: () => { - const { roomId, toIndex } = action; - if (!this.spaceShortcut.has(roomId)) return; - this.spaceShortcut.delete(roomId); - const ssList = [...this.spaceShortcut]; - if (toIndex >= ssList.length) ssList.push(roomId); - else ssList.splice(toIndex, 0, roomId); - this.spaceShortcut = new Set(ssList); - this._updateSpaceShortcutData(ssList); - this.emit(cons.events.accountData.SPACE_SHORTCUT_UPDATED, roomId); - }, - [cons.actions.accountData.CATEGORIZE_SPACE]: () => { - if (this.categorizedSpaces.has(action.roomId)) return; - this.categorizedSpaces.add(action.roomId); - this._updateCategorizedSpacesData([...this.categorizedSpaces]); - this.emit(cons.events.accountData.CATEGORIZE_SPACE_UPDATED, action.roomId); - }, - [cons.actions.accountData.UNCATEGORIZE_SPACE]: () => { - if (!this.categorizedSpaces.has(action.roomId)) return; - this.categorizedSpaces.delete(action.roomId); - this._updateCategorizedSpacesData([...this.categorizedSpaces]); - this.emit(cons.events.accountData.CATEGORIZE_SPACE_UPDATED, action.roomId); - }, - }; - actions[action.type]?.(); - } - - _listenEvents() { - this.matrixClient.on('accountData', (event) => { - if (event.getType() !== cons.IN_CINNY_SPACES) return; - this._populateSpaceShortcut(); - this.emit(cons.events.accountData.SPACE_SHORTCUT_UPDATED); - this._populateCategorizedSpaces(); - this.emit(cons.events.accountData.CATEGORIZE_SPACE_UPDATED); - }); - - this.roomList.on(cons.events.roomList.ROOM_LEAVED, (roomId) => { - if (this.spaceShortcut.has(roomId)) { - // if deleted space has shortcut remove it. - this.spaceShortcut.delete(roomId); - this._updateSpaceShortcutData([...this.spaceShortcut]); - this.emit(cons.events.accountData.SPACE_SHORTCUT_UPDATED, roomId); - } - if (this.categorizedSpaces.has(roomId)) { - this.categorizedSpaces.delete(roomId); - this._updateCategorizedSpacesData([...this.categorizedSpaces]); - this.emit(cons.events.accountData.CATEGORIZE_SPACE_UPDATED, roomId); - } - }); - } -} - -export default AccountData; diff --git a/src/client/state/Notifications.js b/src/client/state/Notifications.js deleted file mode 100644 index 09fa240e7..000000000 --- a/src/client/state/Notifications.js +++ /dev/null @@ -1,412 +0,0 @@ -import EventEmitter from 'events'; -import renderAvatar from '../../app/atoms/avatar/render'; -import { cssColorMXID } from '../../util/colorMXID'; -import { selectRoom } from '../action/navigation'; -import cons from './cons'; -import navigation from './navigation'; -import settings from './settings'; -import { setFavicon } from '../../util/common'; - -import LogoSVG from '../../../public/res/svg/cinny.svg'; -import LogoUnreadSVG from '../../../public/res/svg/cinny-unread.svg'; -import LogoHighlightSVG from '../../../public/res/svg/cinny-highlight.svg'; -import { html, plain } from '../../util/markdown'; - -function isNotifEvent(mEvent) { - const eType = mEvent.getType(); - if (!cons.supportEventTypes.includes(eType)) return false; - if (eType === 'm.room.member') return false; - - if (mEvent.isRedacted()) return false; - if (mEvent.getRelation()?.rel_type === 'm.replace') return false; - - return true; -} - -function isMutedRule(rule) { - return rule.actions[0] === 'dont_notify' && rule.conditions[0].kind === 'event_match'; -} - -function findMutedRule(overrideRules, roomId) { - return overrideRules.find((rule) => ( - rule.rule_id === roomId - && isMutedRule(rule) - )); -} - -class Notifications extends EventEmitter { - constructor(roomList) { - super(); - - this.initialized = false; - this.favicon = LogoSVG; - this.matrixClient = roomList.matrixClient; - this.roomList = roomList; - - this.roomIdToNoti = new Map(); - this.roomIdToPopupNotis = new Map(); - this.eventIdToPopupNoti = new Map(); - - // this._initNoti(); - this._listenEvents(); - - // Ask for permission by default after loading - window.Notification?.requestPermission(); - } - - async _initNoti() { - this.initialized = false; - this.roomIdToNoti = new Map(); - - const addNoti = (roomId) => { - const room = this.matrixClient.getRoom(roomId); - if (this.getNotiType(room.roomId) === cons.notifs.MUTE) return; - if (this.doesRoomHaveUnread(room) === false) return; - - const total = room.getUnreadNotificationCount('total'); - const highlight = room.getUnreadNotificationCount('highlight'); - this._setNoti(room.roomId, total ?? 0, highlight ?? 0); - }; - [...this.roomList.rooms].forEach(addNoti); - [...this.roomList.directs].forEach(addNoti); - - this.initialized = true; - this._updateFavicon(); - } - - doesRoomHaveUnread(room) { - const userId = this.matrixClient.getUserId(); - const readUpToId = room.getEventReadUpTo(userId); - const liveEvents = room.getLiveTimeline().getEvents(); - - if (liveEvents[liveEvents.length - 1]?.getSender() === userId) { - return false; - } - - for (let i = liveEvents.length - 1; i >= 0; i -= 1) { - const event = liveEvents[i]; - if (event.getId() === readUpToId) return false; - if (isNotifEvent(event)) return true; - } - return true; - } - - getNotiType(roomId) { - const mx = this.matrixClient; - let pushRule; - try { - pushRule = mx.getRoomPushRule('global', roomId); - } catch { - pushRule = undefined; - } - - if (pushRule === undefined) { - const overrideRules = mx.getAccountData('m.push_rules')?.getContent()?.global?.override; - if (overrideRules === undefined) return cons.notifs.DEFAULT; - - const isMuted = findMutedRule(overrideRules, roomId); - - return isMuted ? cons.notifs.MUTE : cons.notifs.DEFAULT; - } - if (pushRule.actions[0] === 'notify') return cons.notifs.ALL_MESSAGES; - return cons.notifs.MENTIONS_AND_KEYWORDS; - } - - getNoti(roomId) { - return this.roomIdToNoti.get(roomId) || { total: 0, highlight: 0, from: null }; - } - - getTotalNoti(roomId) { - const { total } = this.getNoti(roomId); - return total; - } - - getHighlightNoti(roomId) { - const { highlight } = this.getNoti(roomId); - return highlight; - } - - getFromNoti(roomId) { - const { from } = this.getNoti(roomId); - return from; - } - - hasNoti(roomId) { - return this.roomIdToNoti.has(roomId); - } - - deleteNoti(roomId) { - if (this.hasNoti(roomId)) { - const noti = this.getNoti(roomId); - this._deleteNoti(roomId, noti.total, noti.highlight); - } - } - - async _updateFavicon() { - if (!this.initialized) return; - let unread = false; - let highlight = false; - [...this.roomIdToNoti.values()].find((noti) => { - if (!unread) { - unread = noti.total > 0 || noti.highlight > 0; - } - highlight = noti.highlight > 0; - if (unread && highlight) return true; - return false; - }); - let newFavicon = LogoSVG; - if (unread && !highlight) { - newFavicon = LogoUnreadSVG; - } - if (unread && highlight) { - newFavicon = LogoHighlightSVG; - } - if (newFavicon === this.favicon) return; - this.favicon = newFavicon; - setFavicon(this.favicon); - } - - _setNoti(roomId, total, highlight) { - const addNoti = (id, t, h, fromId) => { - const prevTotal = this.roomIdToNoti.get(id)?.total ?? null; - const noti = this.getNoti(id); - - noti.total += t; - noti.highlight += h; - - if (fromId) { - if (noti.from === null) noti.from = new Set(); - noti.from.add(fromId); - } - this.roomIdToNoti.set(id, noti); - this.emit(cons.events.notifications.NOTI_CHANGED, id, noti.total, prevTotal); - }; - - const noti = this.getNoti(roomId); - const addT = (highlight > total ? highlight : total) - noti.total; - const addH = highlight - noti.highlight; - if (addT < 0 || addH < 0) return; - - addNoti(roomId, addT, addH); - const allParentSpaces = this.roomList.getAllParentSpaces(roomId); - allParentSpaces.forEach((spaceId) => { - addNoti(spaceId, addT, addH, roomId); - }); - this._updateFavicon(); - } - - _deleteNoti(roomId, total, highlight) { - const removeNoti = (id, t, h, fromId) => { - if (this.roomIdToNoti.has(id) === false) return; - - const noti = this.getNoti(id); - const prevTotal = noti.total; - noti.total -= t; - noti.highlight -= h; - if (noti.total < 0) { - noti.total = 0; - noti.highlight = 0; - } - if (fromId && noti.from !== null) { - if (!this.hasNoti(fromId)) noti.from.delete(fromId); - } - if (noti.from === null || noti.from.size === 0) { - this.roomIdToNoti.delete(id); - this.emit(cons.events.notifications.FULL_READ, id); - this.emit(cons.events.notifications.NOTI_CHANGED, id, null, prevTotal); - } else { - this.roomIdToNoti.set(id, noti); - this.emit(cons.events.notifications.NOTI_CHANGED, id, noti.total, prevTotal); - } - }; - - removeNoti(roomId, total, highlight); - const allParentSpaces = this.roomList.getAllParentSpaces(roomId); - allParentSpaces.forEach((spaceId) => { - removeNoti(spaceId, total, highlight, roomId); - }); - this._updateFavicon(); - } - - async _displayPopupNoti(mEvent, room) { - if (!settings.showNotifications && !settings.isNotificationSounds) return; - - const actions = this.matrixClient.getPushActionsForEvent(mEvent); - if (!actions?.notify) return; - - if (navigation.selectedRoomId === room.roomId && document.hasFocus()) return; - - if (mEvent.isEncrypted()) { - await mEvent.attemptDecryption(this.matrixClient.crypto); - } - - if (settings.showNotifications) { - let title; - if (!mEvent.sender || room.name === mEvent.sender.name) { - title = room.name; - } else if (mEvent.sender) { - title = `${mEvent.sender.name} (${room.name})`; - } - - const iconSize = 36; - const icon = await renderAvatar({ - text: mEvent.sender.name, - bgColor: cssColorMXID(mEvent.getSender()), - imageSrc: mEvent.sender?.getAvatarUrl(this.matrixClient.baseUrl, iconSize, iconSize, 'crop'), - size: iconSize, - borderRadius: 8, - scale: 8, - }); - - const content = mEvent.getContent(); - - const state = { kind: 'notification', onlyPlain: true }; - let body; - if (content.format === 'org.matrix.custom.html') { - body = html(content.formatted_body, state); - } else { - body = plain(content.body, state); - } - - const noti = new window.Notification(title, { - body: body.plain, - icon, - tag: mEvent.getId(), - silent: settings.isNotificationSounds, - }); - if (settings.isNotificationSounds) { - noti.onshow = () => this._playNotiSound(); - } - noti.onclick = () => selectRoom(room.roomId, mEvent.getId()); - - this.eventIdToPopupNoti.set(mEvent.getId(), noti); - if (this.roomIdToPopupNotis.has(room.roomId)) { - this.roomIdToPopupNotis.get(room.roomId).push(noti); - } else { - this.roomIdToPopupNotis.set(room.roomId, [noti]); - } - } else { - this._playNotiSound(); - } - } - - _deletePopupNoti(eventId) { - this.eventIdToPopupNoti.get(eventId)?.close(); - this.eventIdToPopupNoti.delete(eventId); - } - - _deletePopupRoomNotis(roomId) { - this.roomIdToPopupNotis.get(roomId)?.forEach((n) => { - this.eventIdToPopupNoti.delete(n.tag); - n.close(); - }); - this.roomIdToPopupNotis.delete(roomId); - } - - _playNotiSound() { - if (!this._notiAudio) { - this._notiAudio = document.getElementById('notificationSound'); - } - this._notiAudio.play(); - } - - _playInviteSound() { - if (!this._inviteAudio) { - this._inviteAudio = document.getElementById('inviteSound'); - } - this._inviteAudio.play(); - } - - _listenEvents() { - this.matrixClient.on('Room.timeline', (mEvent, room) => { - if (mEvent.isRedaction()) this._deletePopupNoti(mEvent.event.redacts); - - if (room.isSpaceRoom()) return; - if (!isNotifEvent(mEvent)) return; - - const liveEvents = room.getLiveTimeline().getEvents(); - - const lastTimelineEvent = liveEvents[liveEvents.length - 1]; - if (lastTimelineEvent.getId() !== mEvent.getId()) return; - if (mEvent.getSender() === this.matrixClient.getUserId()) return; - - const total = room.getUnreadNotificationCount('total'); - const highlight = room.getUnreadNotificationCount('highlight'); - - if (this.getNotiType(room.roomId) === cons.notifs.MUTE) { - this.deleteNoti(room.roomId, total ?? 0, highlight ?? 0); - return; - } - - this._setNoti(room.roomId, total ?? 0, highlight ?? 0); - - if (this.matrixClient.getSyncState() === 'SYNCING') { - this._displayPopupNoti(mEvent, room); - } - }); - - this.matrixClient.on('accountData', (mEvent, oldMEvent) => { - if (mEvent.getType() === 'm.push_rules') { - const override = mEvent?.getContent()?.global?.override; - const oldOverride = oldMEvent?.getContent()?.global?.override; - if (!override || !oldOverride) return; - - const isMuteToggled = (rule, otherOverride) => { - const roomId = rule.rule_id; - const room = this.matrixClient.getRoom(roomId); - if (room === null) return false; - if (room.isSpaceRoom()) return false; - - 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)); - - mutedRules.forEach((rule) => { - this.emit(cons.events.notifications.MUTE_TOGGLED, rule.rule_id, true); - this.deleteNoti(rule.rule_id); - }); - unMutedRules.forEach((rule) => { - this.emit(cons.events.notifications.MUTE_TOGGLED, rule.rule_id, false); - const room = this.matrixClient.getRoom(rule.rule_id); - if (!this.doesRoomHaveUnread(room)) return; - const total = room.getUnreadNotificationCount('total'); - const highlight = room.getUnreadNotificationCount('highlight'); - this._setNoti(room.roomId, total ?? 0, highlight ?? 0); - }); - } - }); - - this.matrixClient.on('Room.receipt', (mEvent, room) => { - if (mEvent.getType() !== 'm.receipt' || room.isSpaceRoom()) return; - const content = mEvent.getContent(); - const userId = this.matrixClient.getUserId(); - - Object.keys(content).forEach((eventId) => { - Object.entries(content[eventId]).forEach(([receiptType, receipt]) => { - if (!cons.supportReceiptTypes.includes(receiptType)) return; - if (Object.keys(receipt || {}).includes(userId)) { - this.deleteNoti(room.roomId); - this._deletePopupRoomNotis(room.roomId); - } - }); - }); - }); - - this.matrixClient.on('Room.myMembership', (room, membership) => { - if (membership === 'leave' && this.hasNoti(room.roomId)) { - this.deleteNoti(room.roomId); - } - if (membership === 'invite') { - this._playInviteSound(); - } - }); - } -} - -export default Notifications; diff --git a/src/client/state/RoomList.js b/src/client/state/RoomList.js deleted file mode 100644 index fc137ae26..000000000 --- a/src/client/state/RoomList.js +++ /dev/null @@ -1,383 +0,0 @@ -import EventEmitter from 'events'; -import appDispatcher from '../dispatcher'; -import cons from './cons'; - -function isMEventSpaceChild(mEvent) { - return mEvent.getType() === 'm.space.child' && Object.keys(mEvent.getContent()).length > 0; -} - -/** - * @param {() => boolean} callback if return true wait will over else callback will be called again. - * @param {number} timeout timeout to callback - * @param {number} maxTry maximum callback try > 0. -1 means no limit - */ -async function waitFor(callback, timeout = 400, maxTry = -1) { - if (maxTry === 0) return false; - const isOver = async () => new Promise((resolve) => { - setTimeout(() => resolve(callback()), timeout); - }); - - if (await isOver()) return true; - return waitFor(callback, timeout, maxTry - 1); -} - -class RoomList extends EventEmitter { - constructor(matrixClient) { - super(); - this.matrixClient = matrixClient; - this.mDirects = this.getMDirects(); - - // Contains roomId to parent spaces roomId mapping of all spaces children. - // No matter if you have joined those children rooms or not. - this.roomIdToParents = new Map(); - - this.inviteDirects = new Set(); - this.inviteSpaces = new Set(); - this.inviteRooms = new Set(); - - this.directs = new Set(); - this.spaces = new Set(); - this.rooms = new Set(); - - this.processingRooms = new Map(); - - this._populateRooms(); - this._listenEvents(); - - appDispatcher.register(this.roomActions.bind(this)); - } - - isOrphan(roomId) { - return !this.roomIdToParents.has(roomId); - } - - getOrphanSpaces() { - return [...this.spaces].filter((roomId) => !this.roomIdToParents.has(roomId)); - } - - getOrphanRooms() { - return [...this.rooms].filter((roomId) => !this.roomIdToParents.has(roomId)); - } - - getOrphans() { - const rooms = [...this.spaces].concat([...this.rooms]); - return rooms.filter((roomId) => !this.roomIdToParents.has(roomId)); - } - - getSpaceChildren(roomId) { - const space = this.matrixClient.getRoom(roomId); - if (space === null) return null; - const mSpaceChild = space?.currentState.getStateEvents('m.space.child'); - - const children = []; - mSpaceChild.forEach((mEvent) => { - const childId = mEvent.event.state_key; - if (isMEventSpaceChild(mEvent)) children.push(childId); - }); - return children; - } - - getCategorizedSpaces(spaceIds) { - const categorized = new Map(); - - const categorizeSpace = (spaceId) => { - if (categorized.has(spaceId)) return; - const mappedChild = new Set(); - categorized.set(spaceId, mappedChild); - - const child = this.getSpaceChildren(spaceId); - - child.forEach((childId) => { - const room = this.matrixClient.getRoom(childId); - if (room === null || room.getMyMembership() !== 'join') return; - if (room.isSpaceRoom()) categorizeSpace(childId); - else mappedChild.add(childId); - }); - }; - spaceIds.forEach(categorizeSpace); - - return categorized; - } - - addToRoomIdToParents(roomId, parentRoomId) { - if (!this.roomIdToParents.has(roomId)) { - this.roomIdToParents.set(roomId, new Set()); - } - const parents = this.roomIdToParents.get(roomId); - parents.add(parentRoomId); - } - - removeFromRoomIdToParents(roomId, parentRoomId) { - if (!this.roomIdToParents.has(roomId)) return; - const parents = this.roomIdToParents.get(roomId); - parents.delete(parentRoomId); - if (parents.size === 0) this.roomIdToParents.delete(roomId); - } - - getAllParentSpaces(roomId) { - const allParents = new Set(); - - const addAllParentIds = (rId) => { - if (allParents.has(rId)) return; - allParents.add(rId); - - const parents = this.roomIdToParents.get(rId); - if (parents === undefined) return; - - parents.forEach((id) => addAllParentIds(id)); - }; - addAllParentIds(roomId); - allParents.delete(roomId); - return allParents; - } - - addToSpaces(roomId) { - this.spaces.add(roomId); - - const allParentSpaces = this.getAllParentSpaces(roomId); - const spaceChildren = this.getSpaceChildren(roomId); - spaceChildren?.forEach((childId) => { - if (allParentSpaces.has(childId)) return; - this.addToRoomIdToParents(childId, roomId); - }); - } - - deleteFromSpaces(roomId) { - this.spaces.delete(roomId); - - const spaceChildren = this.getSpaceChildren(roomId); - spaceChildren?.forEach((childId) => { - this.removeFromRoomIdToParents(childId, roomId); - }); - } - - roomActions(action) { - const addRoom = (roomId, isDM) => { - const myRoom = this.matrixClient.getRoom(roomId); - if (myRoom === null) return false; - - if (isDM) this.directs.add(roomId); - else if (myRoom.isSpaceRoom()) this.addToSpaces(roomId); - else this.rooms.add(roomId); - return true; - }; - const actions = { - [cons.actions.room.JOIN]: () => { - if (addRoom(action.roomId, action.isDM)) { - setTimeout(() => { - this.emit(cons.events.roomList.ROOM_JOINED, action.roomId); - this.emit(cons.events.roomList.ROOMLIST_UPDATED); - }, 100); - } else { - this.processingRooms.set(action.roomId, { - roomId: action.roomId, - isDM: action.isDM, - task: 'JOIN', - }); - } - }, - [cons.actions.room.CREATE]: () => { - if (addRoom(action.roomId, action.isDM)) { - setTimeout(() => { - this.emit(cons.events.roomList.ROOM_CREATED, action.roomId); - this.emit(cons.events.roomList.ROOM_JOINED, action.roomId); - this.emit(cons.events.roomList.ROOMLIST_UPDATED); - }, 100); - } else { - this.processingRooms.set(action.roomId, { - roomId: action.roomId, - isDM: action.isDM, - task: 'CREATE', - }); - } - }, - }; - actions[action.type]?.(); - } - - getMDirects() { - const mDirectsId = new Set(); - const mDirect = this.matrixClient - .getAccountData('m.direct') - ?.getContent(); - - if (typeof mDirect === 'undefined') return mDirectsId; - - Object.keys(mDirect).forEach((direct) => { - mDirect[direct].forEach((directId) => mDirectsId.add(directId)); - }); - - return mDirectsId; - } - - _populateRooms() { - this.directs.clear(); - this.roomIdToParents.clear(); - this.spaces.clear(); - this.rooms.clear(); - this.inviteDirects.clear(); - this.inviteSpaces.clear(); - this.inviteRooms.clear(); - this.matrixClient.getRooms().forEach((room) => { - const { roomId } = room; - - if (room.getMyMembership() === 'invite') { - if (this._isDMInvite(room)) this.inviteDirects.add(roomId); - else if (room.isSpaceRoom()) this.inviteSpaces.add(roomId); - else this.inviteRooms.add(roomId); - return; - } - - if (room.getMyMembership() !== 'join') return; - - if (this.mDirects.has(roomId)) this.directs.add(roomId); - else if (room.isSpaceRoom()) this.addToSpaces(roomId); - else this.rooms.add(roomId); - }); - } - - _isDMInvite(room) { - if (this.mDirects.has(room.roomId)) return true; - const me = room.getMember(this.matrixClient.getUserId()); - const myEventContent = me.events.member.getContent(); - return myEventContent.membership === 'invite' && myEventContent.is_direct; - } - - _listenEvents() { - // Update roomList when m.direct changes - this.matrixClient.on('accountData', (event) => { - if (event.getType() !== 'm.direct') return; - - const latestMDirects = this.getMDirects(); - - latestMDirects.forEach((directId) => { - if (this.mDirects.has(directId)) return; - this.mDirects.add(directId); - - const myRoom = this.matrixClient.getRoom(directId); - if (myRoom === null) return; - if (myRoom.getMyMembership() === 'join') { - this.directs.add(directId); - this.rooms.delete(directId); - this.emit(cons.events.roomList.ROOMLIST_UPDATED); - } - }); - - [...this.directs].forEach((directId) => { - if (latestMDirects.has(directId)) return; - this.mDirects.delete(directId); - - const myRoom = this.matrixClient.getRoom(directId); - if (myRoom === null) return; - if (myRoom.getMyMembership() === 'join') { - this.directs.delete(directId); - this.rooms.add(directId); - this.emit(cons.events.roomList.ROOMLIST_UPDATED); - } - }); - }); - - this.matrixClient.on('Room.name', (room) => { - this.emit(cons.events.roomList.ROOMLIST_UPDATED); - this.emit(cons.events.roomList.ROOM_PROFILE_UPDATED, room.roomId); - }); - - this.matrixClient.on('RoomState.events', (mEvent, state) => { - if (mEvent.getType() === 'm.space.child') { - const roomId = mEvent.event.room_id; - const childId = mEvent.event.state_key; - if (isMEventSpaceChild(mEvent)) { - const allParentSpaces = this.getAllParentSpaces(roomId); - // only add if it doesn't make a cycle - if (!allParentSpaces.has(childId)) { - this.addToRoomIdToParents(childId, roomId); - } - } else { - this.removeFromRoomIdToParents(childId, roomId); - } - this.emit(cons.events.roomList.ROOMLIST_UPDATED); - return; - } - if (mEvent.getType() === 'm.room.join_rules') { - this.emit(cons.events.roomList.ROOMLIST_UPDATED); - return; - } - if (['m.room.avatar', 'm.room.topic'].includes(mEvent.getType())) { - if (mEvent.getType() === 'm.room.avatar') { - this.emit(cons.events.roomList.ROOMLIST_UPDATED); - } - this.emit(cons.events.roomList.ROOM_PROFILE_UPDATED, state.roomId); - } - }); - - this.matrixClient.on('Room.myMembership', async (room, membership, prevMembership) => { - // room => prevMembership = null | invite | join | leave | kick | ban | unban - // room => membership = invite | join | leave | kick | ban | unban - const { roomId } = room; - const isRoomReady = () => this.matrixClient.getRoom(roomId) !== null; - if (['join', 'invite'].includes(membership) && isRoomReady() === false) { - if (await waitFor(isRoomReady, 200, 100) === false) return; - } - - if (membership === 'unban') return; - - if (membership === 'invite') { - if (this._isDMInvite(room)) this.inviteDirects.add(roomId); - else if (room.isSpaceRoom()) this.inviteSpaces.add(roomId); - else this.inviteRooms.add(roomId); - - this.emit(cons.events.roomList.INVITELIST_UPDATED, roomId); - return; - } - - if (prevMembership === 'invite') { - if (this.inviteDirects.has(roomId)) this.inviteDirects.delete(roomId); - else if (this.inviteSpaces.has(roomId)) this.inviteSpaces.delete(roomId); - else this.inviteRooms.delete(roomId); - - this.emit(cons.events.roomList.INVITELIST_UPDATED, roomId); - } - - if (['leave', 'kick', 'ban'].includes(membership)) { - if (this.directs.has(roomId)) this.directs.delete(roomId); - else if (this.spaces.has(roomId)) this.deleteFromSpaces(roomId); - else this.rooms.delete(roomId); - this.emit(cons.events.roomList.ROOM_LEAVED, roomId); - this.emit(cons.events.roomList.ROOMLIST_UPDATED); - return; - } - - // when user create room/DM OR accept room/dm invite from this client. - // we will update this.rooms/this.directs with user action - if (membership === 'join' && this.processingRooms.has(roomId)) { - const procRoomInfo = this.processingRooms.get(roomId); - - if (procRoomInfo.isDM) this.directs.add(roomId); - else if (room.isSpaceRoom()) this.addToSpaces(roomId); - else this.rooms.add(roomId); - - if (procRoomInfo.task === 'CREATE') this.emit(cons.events.roomList.ROOM_CREATED, roomId); - this.emit(cons.events.roomList.ROOM_JOINED, roomId); - this.emit(cons.events.roomList.ROOMLIST_UPDATED); - - this.processingRooms.delete(roomId); - return; - } - - if (this.mDirects.has(roomId) && membership === 'join') { - this.directs.add(roomId); - this.emit(cons.events.roomList.ROOM_JOINED, roomId); - this.emit(cons.events.roomList.ROOMLIST_UPDATED); - return; - } - - if (membership === 'join') { - if (room.isSpaceRoom()) this.addToSpaces(roomId); - else this.rooms.add(roomId); - this.emit(cons.events.roomList.ROOM_JOINED, roomId); - this.emit(cons.events.roomList.ROOMLIST_UPDATED); - } - }); - } -} -export default RoomList; diff --git a/src/client/state/RoomTimeline.js b/src/client/state/RoomTimeline.js deleted file mode 100644 index 57d91c141..000000000 --- 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; diff --git a/src/client/state/RoomsHierarchy.js b/src/client/state/RoomsHierarchy.js deleted file mode 100644 index f3ffb1fcb..000000000 --- a/src/client/state/RoomsHierarchy.js +++ /dev/null @@ -1,49 +0,0 @@ -import { RoomHierarchy } from 'matrix-js-sdk/lib/room-hierarchy'; - -class RoomsHierarchy { - constructor(matrixClient, limit = 20, maxDepth = 1, suggestedOnly = false) { - this.matrixClient = matrixClient; - this._maxDepth = maxDepth; - this._suggestedOnly = suggestedOnly; - this._limit = limit; - - this.roomIdToHierarchy = new Map(); - } - - getHierarchy(roomId) { - return this.roomIdToHierarchy.get(roomId); - } - - removeHierarchy(roomId) { - return this.roomIdToHierarchy.delete(roomId); - } - - canLoadMore(roomId) { - const roomHierarchy = this.getHierarchy(roomId); - if (!roomHierarchy) return true; - return roomHierarchy.canLoadMore; - } - - async load(roomId, limit = this._limit) { - let roomHierarchy = this.getHierarchy(roomId); - - if (!roomHierarchy) { - roomHierarchy = new RoomHierarchy( - { roomId, client: this.matrixClient }, - limit, - this._maxDepth, - this._suggestedOnly, - ); - this.roomIdToHierarchy.set(roomId, roomHierarchy); - } - - try { - await roomHierarchy.load(limit); - return roomHierarchy.rooms; - } catch { - return roomHierarchy.rooms; - } - } -} - -export default RoomsHierarchy; diff --git a/src/client/state/RoomsInput.js b/src/client/state/RoomsInput.js deleted file mode 100644 index d1e0aedb7..000000000 --- a/src/client/state/RoomsInput.js +++ /dev/null @@ -1,423 +0,0 @@ -import EventEmitter from 'events'; -import encrypt from 'browser-encrypt-attachment'; -import { encode } from 'blurhash'; -import { getShortcodeToEmoji } from '../../app/organisms/emoji-board/custom-emoji'; -import { getBlobSafeMimeType } from '../../util/mimetypes'; -import { sanitizeText } from '../../util/sanitize'; -import cons from './cons'; -import settings from './settings'; -import { markdown, plain } from '../../util/markdown'; - -const blurhashField = 'xyz.amorgan.blurhash'; - -function encodeBlurhash(img) { - const canvas = document.createElement('canvas'); - canvas.width = 100; - canvas.height = 100; - const context = canvas.getContext('2d'); - context.drawImage(img, 0, 0, canvas.width, canvas.height); - const data = context.getImageData(0, 0, canvas.width, canvas.height); - return encode(data.data, data.width, data.height, 4, 4); -} - -function loadImage(url) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => resolve(img); - img.onerror = (err) => reject(err); - img.src = url; - }); -} - -function loadVideo(videoFile) { - return new Promise((resolve, reject) => { - const video = document.createElement('video'); - video.preload = 'metadata'; - video.playsInline = true; - video.muted = true; - - const reader = new FileReader(); - - reader.onload = (ev) => { - // Wait until we have enough data to thumbnail the first frame. - video.onloadeddata = async () => { - resolve(video); - video.pause(); - }; - video.onerror = (e) => { - reject(e); - }; - - video.src = ev.target.result; - video.load(); - video.play(); - }; - reader.onerror = (e) => { - reject(e); - }; - if (videoFile.type === 'video/quicktime') { - const quicktimeVideoFile = new File([videoFile], videoFile.name, { type: 'video/mp4' }); - reader.readAsDataURL(quicktimeVideoFile); - } else { - reader.readAsDataURL(videoFile); - } - }); -} -function getVideoThumbnail(video, width, height, mimeType) { - return new Promise((resolve) => { - const MAX_WIDTH = 800; - const MAX_HEIGHT = 600; - let targetWidth = width; - let targetHeight = height; - if (targetHeight > MAX_HEIGHT) { - targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); - targetHeight = MAX_HEIGHT; - } - if (targetWidth > MAX_WIDTH) { - targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); - targetWidth = MAX_WIDTH; - } - - const canvas = document.createElement('canvas'); - canvas.width = targetWidth; - canvas.height = targetHeight; - const context = canvas.getContext('2d'); - context.drawImage(video, 0, 0, targetWidth, targetHeight); - - canvas.toBlob((thumbnail) => { - resolve({ - thumbnail, - info: { - w: targetWidth, - h: targetHeight, - mimetype: thumbnail.type, - size: thumbnail.size, - }, - }); - }, mimeType); - }); -} - -class RoomsInput extends EventEmitter { - constructor(mx, roomList) { - super(); - - this.matrixClient = mx; - this.roomList = roomList; - this.roomIdToInput = new Map(); - } - - cleanEmptyEntry(roomId) { - const input = this.getInput(roomId); - const isEmpty = typeof input.attachment === 'undefined' - && typeof input.replyTo === 'undefined' - && (typeof input.message === 'undefined' || input.message === ''); - if (isEmpty) { - this.roomIdToInput.delete(roomId); - } - } - - getInput(roomId) { - return this.roomIdToInput.get(roomId) || {}; - } - - setMessage(roomId, message) { - const input = this.getInput(roomId); - input.message = message; - this.roomIdToInput.set(roomId, input); - if (message === '') this.cleanEmptyEntry(roomId); - } - - getMessage(roomId) { - const input = this.getInput(roomId); - if (typeof input.message === 'undefined') return ''; - return input.message; - } - - setReplyTo(roomId, replyTo) { - const input = this.getInput(roomId); - input.replyTo = replyTo; - this.roomIdToInput.set(roomId, input); - } - - getReplyTo(roomId) { - const input = this.getInput(roomId); - if (typeof input.replyTo === 'undefined') return null; - return input.replyTo; - } - - cancelReplyTo(roomId) { - const input = this.getInput(roomId); - if (typeof input.replyTo === 'undefined') return; - delete input.replyTo; - this.roomIdToInput.set(roomId, input); - } - - setAttachment(roomId, file) { - const input = this.getInput(roomId); - input.attachment = { - file, - }; - this.roomIdToInput.set(roomId, input); - } - - getAttachment(roomId) { - const input = this.getInput(roomId); - if (typeof input.attachment === 'undefined') return null; - return input.attachment.file; - } - - cancelAttachment(roomId) { - const input = this.getInput(roomId); - if (typeof input.attachment === 'undefined') return; - - const { uploadingPromise } = input.attachment; - - if (uploadingPromise) { - this.matrixClient.cancelUpload(uploadingPromise); - delete input.attachment.uploadingPromise; - } - delete input.attachment; - delete input.isSending; - this.roomIdToInput.set(roomId, input); - this.emit(cons.events.roomsInput.ATTACHMENT_CANCELED, roomId); - } - - isSending(roomId) { - return this.roomIdToInput.get(roomId)?.isSending || false; - } - - getContent(roomId, options, message, reply, edit) { - const msgType = options?.msgType || 'm.text'; - const autoMarkdown = options?.autoMarkdown ?? true; - - const room = this.matrixClient.getRoom(roomId); - - const userNames = room.currentState.userIdsToDisplayNames; - const parentIds = this.roomList.getAllParentSpaces(room.roomId); - const parentRooms = [...parentIds].map((id) => this.matrixClient.getRoom(id)); - const emojis = getShortcodeToEmoji(this.matrixClient, [room, ...parentRooms]); - - const output = settings.isMarkdown && autoMarkdown ? markdown : plain; - const body = output(message, { userNames, emojis }); - - const content = { - body: body.plain, - msgtype: msgType, - }; - - if (!body.onlyPlain || reply) { - content.format = 'org.matrix.custom.html'; - content.formatted_body = body.html; - } - - if (edit) { - content['m.new_content'] = { ...content }; - content['m.relates_to'] = { - event_id: edit.getId(), - rel_type: 'm.replace', - }; - - const isReply = edit.getWireContent()['m.relates_to']?.['m.in_reply_to']; - if (isReply) { - content.format = 'org.matrix.custom.html'; - content.formatted_body = body.html; - } - - content.body = ` * ${content.body}`; - if (content.formatted_body) content.formatted_body = ` * ${content.formatted_body}`; - - if (isReply) { - const eBody = edit.getContent().body; - const replyHead = eBody.substring(0, eBody.indexOf('\n\n')); - if (replyHead) content.body = `${replyHead}\n\n${content.body}`; - - const eFBody = edit.getContent().formatted_body; - const fReplyHead = eFBody.substring(0, eFBody.indexOf('')); - if (fReplyHead) content.formatted_body = `${fReplyHead}${content.formatted_body}`; - } - } - - if (reply) { - content['m.relates_to'] = { - 'm.in_reply_to': { - event_id: reply.eventId, - }, - }; - - content.body = `> <${reply.userId}> ${reply.body.replace(/\n/g, '\n> ')}\n\n${content.body}`; - - const replyToLink = `In reply to`; - const userLink = `${sanitizeText(reply.userId)}`; - const fallback = `

${replyToLink}${userLink}
${reply.formattedBody || sanitizeText(reply.body)}
`; - content.formatted_body = fallback + content.formatted_body; - } - - return content; - } - - async sendInput(roomId, options) { - const input = this.getInput(roomId); - input.isSending = true; - this.roomIdToInput.set(roomId, input); - if (input.attachment) { - await this.sendFile(roomId, input.attachment.file); - if (!this.isSending(roomId)) return; - } - - if (this.getMessage(roomId).trim() !== '') { - const content = this.getContent(roomId, options, input.message, input.replyTo); - this.matrixClient.sendMessage(roomId, content); - } - - if (this.isSending(roomId)) this.roomIdToInput.delete(roomId); - this.emit(cons.events.roomsInput.MESSAGE_SENT, roomId); - } - - async sendSticker(roomId, data) { - const { mxc: url, body, httpUrl } = data; - const info = {}; - - const img = new Image(); - img.src = httpUrl; - - try { - const res = await fetch(httpUrl); - const blob = await res.blob(); - info.w = img.width; - info.h = img.height; - info.mimetype = blob.type; - info.size = blob.size; - info.thumbnail_info = { ...info }; - info.thumbnail_url = url; - } catch { - // send sticker without info - } - - this.matrixClient.sendEvent(roomId, 'm.sticker', { - body, - url, - info, - }); - this.emit(cons.events.roomsInput.MESSAGE_SENT, roomId); - } - - async sendFile(roomId, file) { - const fileType = getBlobSafeMimeType(file.type).slice(0, file.type.indexOf('/')); - const info = { - mimetype: file.type, - size: file.size, - }; - const content = { info }; - let uploadData = null; - - if (fileType === 'image') { - const img = await loadImage(URL.createObjectURL(file)); - - info.w = img.width; - info.h = img.height; - info[blurhashField] = encodeBlurhash(img); - - content.msgtype = 'm.image'; - content.body = file.name || 'Image'; - } else if (fileType === 'video') { - content.msgtype = 'm.video'; - content.body = file.name || 'Video'; - - try { - const video = await loadVideo(file); - - info.w = video.videoWidth; - info.h = video.videoHeight; - info[blurhashField] = encodeBlurhash(video); - - const thumbnailData = await getVideoThumbnail(video, video.videoWidth, video.videoHeight, 'image/jpeg'); - const thumbnailUploadData = await this.uploadFile(roomId, thumbnailData.thumbnail); - info.thumbnail_info = thumbnailData.info; - if (this.matrixClient.isRoomEncrypted(roomId)) { - info.thumbnail_file = thumbnailUploadData.file; - } else { - info.thumbnail_url = thumbnailUploadData.url; - } - } catch (e) { - this.emit(cons.events.roomsInput.FILE_UPLOAD_CANCELED, roomId); - return; - } - } else if (fileType === 'audio') { - content.msgtype = 'm.audio'; - content.body = file.name || 'Audio'; - } else { - content.msgtype = 'm.file'; - content.body = file.name || 'File'; - } - - try { - uploadData = await this.uploadFile(roomId, file, (data) => { - // data have two properties: data.loaded, data.total - this.emit(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, roomId, data); - }); - this.emit(cons.events.roomsInput.FILE_UPLOADED, roomId); - } catch (e) { - this.emit(cons.events.roomsInput.FILE_UPLOAD_CANCELED, roomId); - return; - } - if (this.matrixClient.isRoomEncrypted(roomId)) { - content.file = uploadData.file; - await this.matrixClient.sendMessage(roomId, content); - } else { - content.url = uploadData.url; - await this.matrixClient.sendMessage(roomId, content); - } - } - - async uploadFile(roomId, file, progressHandler) { - const isEncryptedRoom = this.matrixClient.isRoomEncrypted(roomId); - - let encryptInfo = null; - let encryptBlob = null; - - if (isEncryptedRoom) { - const dataBuffer = await file.arrayBuffer(); - if (typeof this.getInput(roomId).attachment === 'undefined') throw new Error('Attachment canceled'); - const encryptedResult = await encrypt.encryptAttachment(dataBuffer); - if (typeof this.getInput(roomId).attachment === 'undefined') throw new Error('Attachment canceled'); - encryptInfo = encryptedResult.info; - encryptBlob = new Blob([encryptedResult.data]); - } - - const uploadingPromise = this.matrixClient.uploadContent(isEncryptedRoom ? encryptBlob : file, { - // don't send filename if room is encrypted. - includeFilename: !isEncryptedRoom, - progressHandler, - }); - - const input = this.getInput(roomId); - input.attachment.uploadingPromise = uploadingPromise; - this.roomIdToInput.set(roomId, input); - - const { content_uri: url } = await uploadingPromise; - - delete input.attachment.uploadingPromise; - this.roomIdToInput.set(roomId, input); - - if (isEncryptedRoom) { - encryptInfo.url = url; - if (file.type) encryptInfo.mimetype = file.type; - return { file: encryptInfo }; - } - return { url }; - } - - async sendEditedMessage(roomId, mEvent, editedBody) { - const content = this.getContent( - roomId, - { msgType: mEvent.getWireContent().msgtype }, - editedBody, - null, - mEvent, - ); - this.matrixClient.sendMessage(roomId, content); - } -} - -export default RoomsInput; diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 62c0cacc9..523e871a1 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -8,10 +8,6 @@ const cons = { }, DEVICE_DISPLAY_NAME: 'Cinny Web', IN_CINNY_SPACES: 'in.cinny.spaces', - tabs: { - HOME: 'home', - DIRECTS: 'dm', - }, supportEventTypes: [ 'm.room.create', 'm.room.message', @@ -37,43 +33,19 @@ const cons = { }, actions: { navigation: { - SELECT_TAB: 'SELECT_TAB', - SELECT_SPACE: 'SELECT_SPACE', - SELECT_ROOM: 'SELECT_ROOM', OPEN_SPACE_SETTINGS: 'OPEN_SPACE_SETTINGS', - OPEN_SPACE_MANAGE: 'OPEN_SPACE_MANAGE', OPEN_SPACE_ADDEXISTING: 'OPEN_SPACE_ADDEXISTING', TOGGLE_ROOM_SETTINGS: 'TOGGLE_ROOM_SETTINGS', - OPEN_SHORTCUT_SPACES: 'OPEN_SHORTCUT_SPACES', - OPEN_INVITE_LIST: 'OPEN_INVITE_LIST', - OPEN_PUBLIC_ROOMS: 'OPEN_PUBLIC_ROOMS', OPEN_CREATE_ROOM: 'OPEN_CREATE_ROOM', OPEN_JOIN_ALIAS: 'OPEN_JOIN_ALIAS', OPEN_INVITE_USER: 'OPEN_INVITE_USER', OPEN_PROFILE_VIEWER: 'OPEN_PROFILE_VIEWER', OPEN_SETTINGS: 'OPEN_SETTINGS', - OPEN_EMOJIBOARD: 'OPEN_EMOJIBOARD', - OPEN_READRECEIPTS: 'OPEN_READRECEIPTS', - OPEN_VIEWSOURCE: 'OPEN_VIEWSOURCE', - CLICK_REPLY_TO: 'CLICK_REPLY_TO', OPEN_SEARCH: 'OPEN_SEARCH', OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU', - OPEN_NAVIGATION: 'OPEN_NAVIGATION', OPEN_REUSABLE_DIALOG: 'OPEN_REUSABLE_DIALOG', OPEN_EMOJI_VERIFICATION: 'OPEN_EMOJI_VERIFICATION', }, - room: { - JOIN: 'JOIN', - LEAVE: 'LEAVE', - CREATE: 'CREATE', - }, - accountData: { - CREATE_SPACE_SHORTCUT: 'CREATE_SPACE_SHORTCUT', - DELETE_SPACE_SHORTCUT: 'DELETE_SPACE_SHORTCUT', - MOVE_SPACE_SHORTCUTS: 'MOVE_SPACE_SHORTCUTS', - CATEGORIZE_SPACE: 'CATEGORIZE_SPACE', - UNCATEGORIZE_SPACE: 'UNCATEGORIZE_SPACE', - }, settings: { TOGGLE_SYSTEM_THEME: 'TOGGLE_SYSTEM_THEME', TOGGLE_MARKDOWN: 'TOGGLE_MARKDOWN', @@ -86,66 +58,23 @@ const cons = { }, events: { navigation: { - TAB_SELECTED: 'TAB_SELECTED', - SPACE_SELECTED: 'SPACE_SELECTED', - ROOM_SELECTED: 'ROOM_SELECTED', SPACE_SETTINGS_OPENED: 'SPACE_SETTINGS_OPENED', - SPACE_MANAGE_OPENED: 'SPACE_MANAGE_OPENED', SPACE_ADDEXISTING_OPENED: 'SPACE_ADDEXISTING_OPENED', ROOM_SETTINGS_TOGGLED: 'ROOM_SETTINGS_TOGGLED', - SHORTCUT_SPACES_OPENED: 'SHORTCUT_SPACES_OPENED', - INVITE_LIST_OPENED: 'INVITE_LIST_OPENED', - PUBLIC_ROOMS_OPENED: 'PUBLIC_ROOMS_OPENED', CREATE_ROOM_OPENED: 'CREATE_ROOM_OPENED', JOIN_ALIAS_OPENED: 'JOIN_ALIAS_OPENED', INVITE_USER_OPENED: 'INVITE_USER_OPENED', SETTINGS_OPENED: 'SETTINGS_OPENED', - PROFILE_VIEWER_OPENED: 'PROFILE_VIEWER_OPENED', - EMOJIBOARD_OPENED: 'EMOJIBOARD_OPENED', - READRECEIPTS_OPENED: 'READRECEIPTS_OPENED', - VIEWSOURCE_OPENED: 'VIEWSOURCE_OPENED', - REPLY_TO_CLICKED: 'REPLY_TO_CLICKED', SEARCH_OPENED: 'SEARCH_OPENED', REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED', - NAVIGATION_OPENED: 'NAVIGATION_OPENED', REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED', EMOJI_VERIFICATION_OPENED: 'EMOJI_VERIFICATION_OPENED', }, - roomList: { - ROOMLIST_UPDATED: 'ROOMLIST_UPDATED', - INVITELIST_UPDATED: 'INVITELIST_UPDATED', - ROOM_JOINED: 'ROOM_JOINED', - ROOM_LEAVED: 'ROOM_LEAVED', - ROOM_CREATED: 'ROOM_CREATED', - ROOM_PROFILE_UPDATED: 'ROOM_PROFILE_UPDATED', - }, - accountData: { - SPACE_SHORTCUT_UPDATED: 'SPACE_SHORTCUT_UPDATED', - CATEGORIZE_SPACE_UPDATED: 'CATEGORIZE_SPACE_UPDATED', - }, notifications: { NOTI_CHANGED: 'NOTI_CHANGED', FULL_READ: 'FULL_READ', MUTE_TOGGLED: 'MUTE_TOGGLED', }, - roomTimeline: { - READY: 'READY', - EVENT: 'EVENT', - PAGINATED: 'PAGINATED', - TYPING_MEMBERS_UPDATED: 'TYPING_MEMBERS_UPDATED', - LIVE_RECEIPT: 'LIVE_RECEIPT', - EVENT_REDACTED: 'EVENT_REDACTED', - AT_BOTTOM: 'AT_BOTTOM', - SCROLL_TO_LIVE: 'SCROLL_TO_LIVE', - }, - roomsInput: { - MESSAGE_SENT: 'MESSAGE_SENT', - ATTACHMENT_SET: 'ATTACHMENT_SET', - FILE_UPLOADED: 'FILE_UPLOADED', - UPLOAD_PROGRESS_CHANGES: 'UPLOAD_PROGRESS_CHANGES', - FILE_UPLOAD_CANCELED: 'FILE_UPLOAD_CANCELED', - ATTACHMENT_CANCELED: 'ATTACHMENT_CANCELED', - }, settings: { SYSTEM_THEME_TOGGLED: 'SYSTEM_THEME_TOGGLED', MARKDOWN_TOGGLED: 'MARKDOWN_TOGGLED', diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js index ddac4dda8..5f28f232f 100644 --- a/src/client/state/navigation.js +++ b/src/client/state/navigation.js @@ -5,268 +5,9 @@ import cons from './cons'; class Navigation extends EventEmitter { constructor() { super(); - // this will attached by initMatrix - this.initMatrix = {}; - - this.selectedTab = cons.tabs.HOME; - this.selectedSpaceId = null; - this.selectedSpacePath = [cons.tabs.HOME]; - - this.selectedRoomId = null; - this.recentRooms = []; - - this.spaceToRoom = new Map(); - this.rawModelStack = []; } - _addToSpacePath(roomId, asRoot) { - if (typeof roomId !== 'string') { - this.selectedSpacePath = [cons.tabs.HOME]; - return; - } - if (asRoot) { - this.selectedSpacePath = [roomId]; - return; - } - if (this.selectedSpacePath.includes(roomId)) { - const spIndex = this.selectedSpacePath.indexOf(roomId); - this.selectedSpacePath = this.selectedSpacePath.slice(0, spIndex + 1); - return; - } - this.selectedSpacePath.push(roomId); - } - - _mapRoomToSpace(roomId) { - const { roomList, accountData } = this.initMatrix; - if ( - this.selectedTab === cons.tabs.HOME - && roomList.rooms.has(roomId) - && !roomList.roomIdToParents.has(roomId) - ) { - this.spaceToRoom.set(cons.tabs.HOME, { - roomId, - timestamp: Date.now(), - }); - return; - } - if (this.selectedTab === cons.tabs.DIRECTS && roomList.directs.has(roomId)) { - this.spaceToRoom.set(cons.tabs.DIRECTS, { - roomId, - timestamp: Date.now(), - }); - return; - } - - const parents = roomList.roomIdToParents.get(roomId); - if (!parents) return; - if (parents.has(this.selectedSpaceId)) { - this.spaceToRoom.set(this.selectedSpaceId, { - roomId, - timestamp: Date.now(), - }); - } else if (accountData.categorizedSpaces.has(this.selectedSpaceId)) { - const categories = roomList.getCategorizedSpaces([this.selectedSpaceId]); - const parent = [...parents].find((pId) => categories.has(pId)); - if (parent) { - this.spaceToRoom.set(parent, { - roomId, - timestamp: Date.now(), - }); - } - } - } - - _selectRoom(roomId, eventId) { - const prevSelectedRoomId = this.selectedRoomId; - this.selectedRoomId = roomId; - if (prevSelectedRoomId !== roomId) this._mapRoomToSpace(roomId); - this.removeRecentRoom(prevSelectedRoomId); - this.addRecentRoom(prevSelectedRoomId); - this.removeRecentRoom(this.selectedRoomId); - this.emit( - cons.events.navigation.ROOM_SELECTED, - this.selectedRoomId, - prevSelectedRoomId, - eventId, - ); - } - - _selectTabWithRoom(roomId) { - const { roomList, accountData } = this.initMatrix; - const { categorizedSpaces } = accountData; - - if (roomList.isOrphan(roomId)) { - if (roomList.directs.has(roomId)) { - this._selectSpace(null, true, false); - this._selectTab(cons.tabs.DIRECTS, false); - return; - } - this._selectSpace(null, true, false); - this._selectTab(cons.tabs.HOME, false); - return; - } - - const parents = roomList.roomIdToParents.get(roomId); - - if (parents.has(this.selectedSpaceId)) { - return; - } - - if (categorizedSpaces.has(this.selectedSpaceId)) { - const categories = roomList.getCategorizedSpaces([this.selectedSpaceId]); - if ([...parents].find((pId) => categories.has(pId))) { - // No need to select tab - // As one of parent is child of selected categorized space. - return; - } - } - - const spaceInPath = [...this.selectedSpacePath].reverse().find((sId) => parents.has(sId)); - if (spaceInPath) { - this._selectSpace(spaceInPath, false, false); - return; - } - - if (roomList.directs.has(roomId)) { - this._selectSpace(null, true, false); - this._selectTab(cons.tabs.DIRECTS, false); - return; - } - - if (parents.size > 0) { - const sortedParents = [...parents].sort((p1, p2) => { - const t1 = this.spaceToRoom.get(p1)?.timestamp ?? 0; - const t2 = this.spaceToRoom.get(p2)?.timestamp ?? 0; - return t2 - t1; - }); - this._selectSpace(sortedParents[0], true, false); - this._selectTab(sortedParents[0], false); - } - } - - _getLatestActiveRoomId(roomIds) { - const mx = this.initMatrix.matrixClient; - - let ts = 0; - let roomId = null; - roomIds.forEach((childId) => { - const room = mx.getRoom(childId); - if (!room) return; - const newTs = room.getLastActiveTimestamp(); - if (newTs > ts) { - ts = newTs; - roomId = childId; - } - }); - return roomId; - } - - _getLatestSelectedRoomId(spaceIds) { - let ts = 0; - let roomId = null; - - spaceIds.forEach((sId) => { - const data = this.spaceToRoom.get(sId); - if (!data) return; - const newTs = data.timestamp; - if (newTs > ts) { - ts = newTs; - roomId = data.roomId; - } - }); - return roomId; - } - - _selectTab(tabId, selectRoom = true) { - this.selectedTab = tabId; - if (selectRoom) this._selectRoomWithTab(this.selectedTab); - this.emit(cons.events.navigation.TAB_SELECTED, this.selectedTab); - } - - _selectSpace(roomId, asRoot, selectRoom = true) { - this._addToSpacePath(roomId, asRoot); - this.selectedSpaceId = roomId; - if (!asRoot && selectRoom) this._selectRoomWithSpace(this.selectedSpaceId); - this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId); - } - - _selectRoomWithSpace(spaceId) { - if (!spaceId) return; - const { roomList, accountData, matrixClient } = this.initMatrix; - const { categorizedSpaces } = accountData; - - const data = this.spaceToRoom.get(spaceId); - if (data && !categorizedSpaces.has(spaceId)) { - this._selectRoom(data.roomId); - return; - } - - const children = []; - - if (categorizedSpaces.has(spaceId)) { - const categories = roomList.getCategorizedSpaces([spaceId]); - - const latestSelectedRoom = this._getLatestSelectedRoomId([...categories.keys()]); - - if (latestSelectedRoom) { - this._selectRoom(latestSelectedRoom); - return; - } - - categories?.forEach((categoryId) => { - categoryId?.forEach((childId) => { - children.push(childId); - }); - }); - } else { - roomList.getSpaceChildren(spaceId).forEach((id) => { - if (matrixClient.getRoom(id)?.isSpaceRoom() === false) { - children.push(id); - } - }); - } - - if (!children) { - this._selectRoom(null); - return; - } - - this._selectRoom(this._getLatestActiveRoomId(children)); - } - - _selectRoomWithTab(tabId) { - const { roomList } = this.initMatrix; - if (tabId === cons.tabs.HOME || tabId === cons.tabs.DIRECTS) { - const data = this.spaceToRoom.get(tabId); - if (data) { - this._selectRoom(data.roomId); - return; - } - const children = tabId === cons.tabs.HOME ? roomList.getOrphanRooms() : [...roomList.directs]; - this._selectRoom(this._getLatestActiveRoomId(children)); - return; - } - this._selectRoomWithSpace(tabId); - } - - removeRecentRoom(roomId) { - if (typeof roomId !== 'string') return; - const roomIdIndex = this.recentRooms.indexOf(roomId); - if (roomIdIndex >= 0) { - this.recentRooms.splice(roomIdIndex, 1); - } - } - - addRecentRoom(roomId) { - if (typeof roomId !== 'string') return; - - this.recentRooms.push(roomId); - if (this.recentRooms.length > 10) { - this.recentRooms.splice(0, 1); - } - } - get isRawModalVisible() { return this.rawModelStack.length > 0; } @@ -278,27 +19,9 @@ class Navigation extends EventEmitter { navigate(action) { const actions = { - [cons.actions.navigation.SELECT_TAB]: () => { - const roomId = ( - action.tabId !== cons.tabs.HOME && action.tabId !== cons.tabs.DIRECTS - ) ? action.tabId : null; - - this._selectSpace(roomId, true); - this._selectTab(action.tabId); - }, - [cons.actions.navigation.SELECT_SPACE]: () => { - this._selectSpace(action.roomId, false); - }, - [cons.actions.navigation.SELECT_ROOM]: () => { - if (action.roomId) this._selectTabWithRoom(action.roomId); - this._selectRoom(action.roomId, action.eventId); - }, [cons.actions.navigation.OPEN_SPACE_SETTINGS]: () => { this.emit(cons.events.navigation.SPACE_SETTINGS_OPENED, action.roomId, action.tabText); }, - [cons.actions.navigation.OPEN_SPACE_MANAGE]: () => { - this.emit(cons.events.navigation.SPACE_MANAGE_OPENED, action.roomId); - }, [cons.actions.navigation.OPEN_SPACE_ADDEXISTING]: () => { this.emit(cons.events.navigation.SPACE_ADDEXISTING_OPENED, action.roomId, action.spaces); }, @@ -309,15 +32,6 @@ class Navigation extends EventEmitter { action.tabText ); }, - [cons.actions.navigation.OPEN_SHORTCUT_SPACES]: () => { - this.emit(cons.events.navigation.SHORTCUT_SPACES_OPENED); - }, - [cons.actions.navigation.OPEN_INVITE_LIST]: () => { - this.emit(cons.events.navigation.INVITE_LIST_OPENED); - }, - [cons.actions.navigation.OPEN_PUBLIC_ROOMS]: () => { - this.emit(cons.events.navigation.PUBLIC_ROOMS_OPENED, action.searchTerm); - }, [cons.actions.navigation.OPEN_CREATE_ROOM]: () => { this.emit( cons.events.navigation.CREATE_ROOM_OPENED, @@ -340,38 +54,6 @@ class Navigation extends EventEmitter { [cons.actions.navigation.OPEN_SETTINGS]: () => { this.emit(cons.events.navigation.SETTINGS_OPENED, action.tabText); }, - [cons.actions.navigation.OPEN_NAVIGATION]: () => { - this.emit(cons.events.navigation.NAVIGATION_OPENED); - }, - [cons.actions.navigation.OPEN_EMOJIBOARD]: () => { - this.emit( - cons.events.navigation.EMOJIBOARD_OPENED, - action.cords, - action.requestEmojiCallback, - ); - }, - [cons.actions.navigation.OPEN_READRECEIPTS]: () => { - this.emit( - cons.events.navigation.READRECEIPTS_OPENED, - action.roomId, - action.userIds, - ); - }, - [cons.actions.navigation.OPEN_VIEWSOURCE]: () => { - this.emit( - cons.events.navigation.VIEWSOURCE_OPENED, - action.event, - ); - }, - [cons.actions.navigation.CLICK_REPLY_TO]: () => { - this.emit( - cons.events.navigation.REPLY_TO_CLICKED, - action.userId, - action.eventId, - action.body, - action.formattedBody, - ); - }, [cons.actions.navigation.OPEN_SEARCH]: () => { this.emit( cons.events.navigation.SEARCH_OPENED, diff --git a/src/util/Postie.js b/src/util/Postie.js deleted file mode 100644 index 73c8f9e81..000000000 --- a/src/util/Postie.js +++ /dev/null @@ -1,97 +0,0 @@ -class Postie { - constructor() { - this._topics = new Map(); - } - - _getSubscribers(topic) { - const subscribers = this._topics.get(topic); - if (subscribers === undefined) { - throw new Error(`Topic:"${topic}" doesn't exist.`); - } - return subscribers; - } - - _getInboxes(topic, address) { - const subscribers = this._getSubscribers(topic); - const inboxes = subscribers.get(address); - if (inboxes === undefined) { - throw new Error(`Inbox on topic:"${topic}" at address:"${address}" doesn't exist.`); - } - return inboxes; - } - - hasTopic(topic) { - return this._topics.get(topic) !== undefined; - } - - hasSubscriber(topic, address) { - const subscribers = this._getSubscribers(topic); - return subscribers.get(address) !== undefined; - } - - hasTopicAndSubscriber(topic, address) { - return (this.hasTopic(topic)) - ? this.hasSubscriber(topic, address) - : false; - } - - /** - * @param {string} topic - Subscription topic - * @param {string} address - Address of subscriber - * @param {function} inbox - The inbox function to receive post data - */ - subscribe(topic, address, inbox) { - if (typeof inbox !== 'function') { - throw new TypeError('Inbox must be a function.'); - } - - if (this._topics.has(topic) === false) { - this._topics.set(topic, new Map()); - } - const subscribers = this._topics.get(topic); - - const inboxes = subscribers.get(address) ?? new Set(); - inboxes.add(inbox); - subscribers.set(address, inboxes); - - return () => this.unsubscribe(topic, address, inbox); - } - - unsubscribe(topic, address, inbox) { - const subscribers = this._getSubscribers(topic); - if (!subscribers) throw new Error(`Unable to unsubscribe. Topic: "${topic}" doesn't exist.`); - - const inboxes = subscribers.get(address); - if (!inboxes) throw new Error(`Unable to unsubscribe. Subscriber on topic:"${topic}" at address:"${address}" doesn't exist`); - - if (!inboxes.delete(inbox)) throw new Error('Unable to unsubscribe. Inbox doesn\'t exist'); - - if (inboxes.size === 0) subscribers.delete(address); - if (subscribers.size === 0) this._topics.delete(topic); - } - - /** - * @param {string} topic - Subscription topic - * @param {string|string[]} address - Address of subscriber - * @param {*} data - Data to deliver to subscriber - */ - post(topic, address, data) { - const sendPost = (inboxes, addr) => { - if (inboxes === undefined) { - throw new Error(`Unable to post on topic:"${topic}" at address:"${addr}". Subscriber doesn't exist.`); - } - inboxes.forEach((inbox) => inbox(data)); - }; - - if (typeof address === 'string') { - sendPost(this._getInboxes(topic, address), address); - return; - } - const subscribers = this._getSubscribers(topic); - address.forEach((addr) => { - sendPost(subscribers.get(addr), addr); - }); - } -} - -export default Postie; diff --git a/src/util/colorMXID.js b/src/util/colorMXID.js index 4d303aae4..95600d29e 100644 --- a/src/util/colorMXID.js +++ b/src/util/colorMXID.js @@ -1,6 +1,6 @@ // https://github.com/cloudrac3r/cadencegq/blob/master/pug/mxid.pug -export function hashCode(str) { +function hashCode(str) { let hash = 0; let i; let chr; diff --git a/src/util/markdown.js b/src/util/markdown.js deleted file mode 100644 index c6c1a4900..000000000 --- a/src/util/markdown.js +++ /dev/null @@ -1,515 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* eslint-disable no-use-before-define */ -import SimpleMarkdown from '@khanacademy/simple-markdown'; -import { idRegex, parseIdUri } from './common'; - -const { - defaultRules, parserFor, outputFor, anyScopeRegex, blockRegex, inlineRegex, - sanitizeText, sanitizeUrl, -} = SimpleMarkdown; - -function htmlTag(tagName, content, attributes, isClosed) { - let s = ''; - Object.entries(attributes || {}).forEach(([k, v]) => { - if (v !== undefined) { - s += ` ${sanitizeText(k)}`; - if (v !== null) s += `="${sanitizeText(v)}"`; - } - }); - - s = `<${tagName}${s}>`; - - if (isClosed === false) { - return s; - } - return `${s}${content}`; -} - -function mathHtml(wrap, node) { - return htmlTag(wrap, htmlTag('code', sanitizeText(node.content)), { 'data-mx-maths': node.content }); -} - -const emojiRegex = /^:([\w-]+):/; - -const plainRules = { - Array: { - ...defaultRules.Array, - plain: defaultRules.Array.html, - }, - userMention: { - order: defaultRules.em.order - 0.9, - match: inlineRegex(idRegex('@', undefined, '^')), - parse: (capture, _, state) => ({ - type: 'mention', - content: state.userNames[capture[1]] ? `@${state.userNames[capture[1]]}` : capture[1], - id: capture[1], - }), - }, - roomMention: { - order: defaultRules.em.order - 0.8, - match: inlineRegex(idRegex('#', undefined, '^')), - parse: (capture) => ({ type: 'mention', content: capture[1], id: capture[1] }), - }, - mention: { - plain: (node, _, state) => (state.kind === 'edit' ? node.id : node.content), - html: (node) => htmlTag('a', sanitizeText(node.content), { - href: `https://matrix.to/#/${encodeURIComponent(node.id)}`, - }), - }, - emoji: { - order: defaultRules.em.order - 0.1, - match: (source, state) => { - if (!state.inline) return null; - const capture = emojiRegex.exec(source); - if (!capture) return null; - const emoji = state.emojis.get(capture[1]); - if (emoji) return capture; - return null; - }, - parse: (capture, _, state) => ({ content: capture[1], emoji: state.emojis.get(capture[1]) }), - plain: ({ emoji }) => (emoji.mxc - ? `:${emoji.shortcode}:` - : emoji.unicode), - html: ({ emoji }) => (emoji.mxc - ? htmlTag('img', null, { - 'data-mx-emoticon': null, - src: emoji.mxc, - alt: `:${emoji.shortcode}:`, - title: `:${emoji.shortcode}:`, - height: 32, - }, false) - : emoji.unicode), - }, - newline: { - ...defaultRules.newline, - plain: () => '\n', - }, - paragraph: { - ...defaultRules.paragraph, - plain: (node, output, state) => `${output(node.content, state)}\n\n`, - html: (node, output, state) => htmlTag('p', output(node.content, state)), - }, - escape: { - ...defaultRules.escape, - plain: (node, output, state) => `\\${output(node.content, state)}`, - }, - br: { - ...defaultRules.br, - match: anyScopeRegex(/^ *\n/), - plain: () => '\n', - }, - text: { - ...defaultRules.text, - match: anyScopeRegex(/^[\s\S]+?(?=[^0-9A-Za-z\s\u00c0-\uffff]| *\n|\w+:\S|$)/), - plain: (node, _, state) => (state.kind === 'edit' - ? node.content.replace(/(\*|_|!\[|\[|\|\||\$\$?)/g, '\\$1') - : node.content), - }, -}; - -const markdownRules = { - ...defaultRules, - ...plainRules, - heading: { - ...defaultRules.heading, - match: blockRegex(/^ *(#{1,6})([^\n:]*?(?: [^\n]*?)?)#* *(?:\n *)*\n/), - plain: (node, output, state) => { - const out = output(node.content, state); - if (state.kind === 'edit' || state.kind === 'notification' || node.level > 2) { - return `${'#'.repeat(node.level)} ${out}\n\n`; - } - return `${out}\n${(node.level === 1 ? '=' : '-').repeat(out.length)}\n\n`; - }, - }, - hr: { - ...defaultRules.hr, - plain: () => '---\n\n', - }, - codeBlock: { - ...defaultRules.codeBlock, - plain: (node) => `\`\`\`${node.lang || ''}\n${node.content}\n\`\`\`\n`, - html: (node) => htmlTag('pre', htmlTag('code', sanitizeText(node.content), { - class: node.lang ? `language-${node.lang}` : undefined, - })), - }, - fence: { - ...defaultRules.fence, - match: blockRegex(/^ *(`{3,}|~{3,}) *(?:(\S+) *)?\n([\s\S]+?)\n?\1 *(?:\n *)*\n/), - }, - blockQuote: { - ...defaultRules.blockQuote, - plain: (node, output, state) => `> ${output(node.content, state).trim().replace(/\n/g, '\n> ')}\n\n`, - }, - list: { - ...defaultRules.list, - plain: (node, output, state) => { - const oldList = state._list; - state._list = true; - - let items = node.items.map((item, i) => { - const prefix = node.ordered ? `${node.start + i}. ` : '* '; - return prefix + output(item, state).replace(/\n/g, `\n${' '.repeat(prefix.length)}`); - }).join('\n'); - - state._list = oldList; - - if (!state._list) { - items += '\n\n'; - } - return items; - }, - }, - def: undefined, - table: { - ...defaultRules.table, - plain: (node, output, state) => { - const header = node.header.map((content) => output(content, state)); - - const colWidth = node.align.map((align) => { - switch (align) { - case 'left': - case 'right': - return 2; - case 'center': - return 3; - default: - return 1; - } - }); - header.forEach((s, i) => { - if (s.length > colWidth[i])colWidth[i] = s.length; - }); - - const cells = node.cells.map((row) => row.map((content, i) => { - const s = output(content, state); - if (colWidth[i] === undefined || s.length > colWidth[i]) { - colWidth[i] = s.length; - } - return s; - })); - - function pad(s, i) { - switch (node.align[i]) { - case 'right': - return s.padStart(colWidth[i]); - case 'center': - return s - .padStart(s.length + Math.floor((colWidth[i] - s.length) / 2)) - .padEnd(colWidth[i]); - default: - return s.padEnd(colWidth[i]); - } - } - - const line = colWidth.map((len, i) => { - switch (node.align[i]) { - case 'left': - return `:${'-'.repeat(len - 1)}`; - case 'center': - return `:${'-'.repeat(len - 2)}:`; - case 'right': - return `${'-'.repeat(len - 1)}:`; - default: - return '-'.repeat(len); - } - }); - - const table = [ - header.map(pad), - line, - ...cells.map((row) => row.map(pad))]; - - return table.map((row) => `| ${row.join(' | ')} |\n`).join(''); - }, - }, - displayMath: { - order: defaultRules.table.order + 0.1, - match: blockRegex(/^ *\$\$ *\n?([\s\S]+?)\n?\$\$ *(?:\n *)*\n/), - parse: (capture) => ({ content: capture[1] }), - plain: (node) => (node.content.includes('\n') - ? `$$\n${node.content}\n$$\n` - : `$$${node.content}$$\n`), - html: (node) => mathHtml('div', node), - }, - shrug: { - order: defaultRules.escape.order - 0.1, - match: inlineRegex(/^¯\\_\(ツ\)_\/¯/), - parse: (capture) => ({ type: 'text', content: capture[0] }), - }, - tableSeparator: { - ...defaultRules.tableSeparator, - plain: () => ' | ', - }, - link: { - ...defaultRules.link, - plain: (node, output, state) => { - const out = output(node.content, state); - const target = sanitizeUrl(node.target) || ''; - if (out !== target || node.title) { - return `[${out}](${target}${node.title ? ` "${node.title}"` : ''})`; - } - return out; - }, - html: (node, output, state) => { - const out = output(node.content, state); - const target = sanitizeUrl(node.target) || ''; - if (out !== target || node.title) { - return htmlTag('a', out, { - href: target, - title: node.title, - }); - } - return target; - }, - }, - image: { - ...defaultRules.image, - plain: (node) => `![${node.alt}](${sanitizeUrl(node.target) || ''}${node.title ? ` "${node.title}"` : ''})`, - html: (node) => htmlTag('img', '', { - src: sanitizeUrl(node.target) || '', - alt: node.alt, - title: node.title, - }, false), - }, - reflink: undefined, - refimage: undefined, - em: { - ...defaultRules.em, - plain: (node, output, state) => `_${output(node.content, state)}_`, - }, - strong: { - ...defaultRules.strong, - plain: (node, output, state) => `**${output(node.content, state)}**`, - }, - u: { - ...defaultRules.u, - plain: (node, output, state) => `__${output(node.content, state)}__`, - }, - del: { - ...defaultRules.del, - plain: (node, output, state) => `~~${output(node.content, state)}~~`, - }, - inlineCode: { - ...defaultRules.inlineCode, - match: inlineRegex(/^(`+)([^\n]*?[^`\n])\1(?!`)/), - plain: (node) => `\`${node.content}\``, - }, - spoiler: { - order: defaultRules.inlineCode.order + 0.1, - match: inlineRegex(/^\|\|([\s\S]+?)\|\|(?:\(([\s\S]+?)\))?/), - parse: (capture, parse, state) => ({ - content: parse(capture[1], state), - reason: capture[2], - }), - plain: (node, output, state) => { - const warning = `spoiler${node.reason ? `: ${node.reason}` : ''}`; - switch (state.kind) { - case 'edit': - return `||${output(node.content, state)}||${node.reason ? `(${node.reason})` : ''}`; - case 'notification': - return `<${warning}>`; - default: - return `[${warning}](${output(node.content, state)})`; - } - }, - html: (node, output, state) => htmlTag( - 'span', - output(node.content, state), - { 'data-mx-spoiler': node.reason || null }, - ), - }, - inlineMath: { - order: defaultRules.del.order + 0.2, - match: inlineRegex(/^\$(\S[\s\S]+?\S|\S)\$(?!\d)/), - parse: (capture) => ({ content: capture[1] }), - plain: (node) => `$${node.content}$`, - html: (node) => mathHtml('span', node), - }, -}; - -function mapElement(el) { - switch (el.tagName) { - case 'MX-REPLY': - return []; - - case 'P': - return [{ type: 'paragraph', content: mapChildren(el) }]; - case 'BR': - return [{ type: 'br' }]; - - case 'H1': - case 'H2': - case 'H3': - case 'H4': - case 'H5': - case 'H6': - return [{ type: 'heading', level: Number(el.tagName[1]), content: mapChildren(el) }]; - case 'HR': - return [{ type: 'hr' }]; - case 'PRE': { - let lang; - if (el.firstChild) { - Array.from(el.firstChild.classList).some((c) => { - const langPrefix = 'language-'; - if (c.startsWith(langPrefix)) { - lang = c.slice(langPrefix.length); - return true; - } - return false; - }); - } - return [{ type: 'codeBlock', lang, content: el.innerText }]; - } - case 'BLOCKQUOTE': - return [{ type: 'blockQuote', content: mapChildren(el) }]; - case 'UL': - return [{ type: 'list', items: Array.from(el.childNodes).map(mapNode) }]; - case 'OL': - return [{ - type: 'list', - ordered: true, - start: Number(el.getAttribute('start')), - items: Array.from(el.childNodes).map(mapNode), - }]; - case 'TABLE': { - const headerEl = Array.from(el.querySelector('thead > tr').childNodes); - const align = headerEl.map((childE) => childE.style['text-align']); - return [{ - type: 'table', - header: headerEl.map(mapChildren), - align, - cells: Array.from(el.querySelectorAll('tbody > tr')).map((rowEl) => Array.from(rowEl.childNodes).map((childEl, i) => { - if (align[i] === undefined) align[i] = childEl.style['text-align']; - return mapChildren(childEl); - })), - }]; - } - case 'A': { - const href = el.getAttribute('href'); - - const id = parseIdUri(href); - if (id) return [{ type: 'mention', content: el.innerText, id }]; - - return [{ - type: 'link', - target: el.getAttribute('href'), - title: el.getAttribute('title'), - content: mapChildren(el), - }]; - } - case 'IMG': { - const src = el.getAttribute('src'); - let title = el.getAttribute('title'); - if (el.hasAttribute('data-mx-emoticon')) { - if (title.length > 2 && title.startsWith(':') && title.endsWith(':')) { - title = title.slice(1, -1); - } - return [{ - type: 'emoji', - content: title, - emoji: { - mxc: src, - shortcode: title, - }, - }]; - } - - return [{ - type: 'image', - alt: el.getAttribute('alt'), - target: src, - title, - }]; - } - case 'EM': - case 'I': - return [{ type: 'em', content: mapChildren(el) }]; - case 'STRONG': - case 'B': - return [{ type: 'strong', content: mapChildren(el) }]; - case 'U': - return [{ type: 'u', content: mapChildren(el) }]; - case 'DEL': - case 'STRIKE': - return [{ type: 'del', content: mapChildren(el) }]; - case 'CODE': - return [{ type: 'inlineCode', content: el.innerText }]; - - case 'DIV': - if (el.hasAttribute('data-mx-maths')) { - return [{ type: 'displayMath', content: el.getAttribute('data-mx-maths') }]; - } - return mapChildren(el); - case 'SPAN': - if (el.hasAttribute('data-mx-spoiler')) { - return [{ type: 'spoiler', reason: el.getAttribute('data-mx-spoiler'), content: mapChildren(el) }]; - } - if (el.hasAttribute('data-mx-maths')) { - return [{ type: 'inlineMath', content: el.getAttribute('data-mx-maths') }]; - } - return mapChildren(el); - default: - return mapChildren(el); - } -} - -function mapNode(n) { - switch (n.nodeType) { - case Node.TEXT_NODE: - return [{ type: 'text', content: n.textContent }]; - case Node.ELEMENT_NODE: - return mapElement(n); - default: - return []; - } -} - -function mapChildren(n) { - return Array.from(n.childNodes).reduce((ast, childN) => { - ast.push(...mapNode(childN)); - return ast; - }, []); -} - -function render(content, state, plainOut, htmlOut) { - let c = content; - if (content.length === 1 && content[0].type === 'paragraph') { - c = c[0].content; - } - - const plainStr = plainOut(c, state).trim(); - if (state.onlyPlain) return { plain: plainStr }; - - const htmlStr = htmlOut(c, state); - - const plainHtml = htmlStr.replace(/
/g, '\n').replace(/<\/p>

/g, '\n\n').replace(/<\/?p>/g, ''); - const onlyPlain = sanitizeText(plainStr) === plainHtml; - - return { - onlyPlain, - plain: plainStr, - html: htmlStr, - }; -} - -const plainParser = parserFor(plainRules); -const plainPlainOut = outputFor(plainRules, 'plain'); -const plainHtmlOut = outputFor(plainRules, 'html'); - -const mdParser = parserFor(markdownRules); -const mdPlainOut = outputFor(markdownRules, 'plain'); -const mdHtmlOut = outputFor(markdownRules, 'html'); - -export function plain(source, state) { - return render(plainParser(source, state), state, plainPlainOut, plainHtmlOut); -} - -export function markdown(source, state) { - return render(mdParser(source, state), state, mdPlainOut, mdHtmlOut); -} - -export function html(source, state) { - const el = document.createElement('template'); - el.innerHTML = source; - return render(mapChildren(el.content), state, mdPlainOut, mdHtmlOut); -} diff --git a/src/util/matrixUtil.js b/src/util/matrixUtil.js index a776fb2be..74e56ec76 100644 --- a/src/util/matrixUtil.js +++ b/src/util/matrixUtil.js @@ -89,20 +89,6 @@ export function trimHTMLReply(html) { return html.slice(i + suffix.length); } -export function hasDMWith(userId) { - const mx = initMatrix.matrixClient; - const directIds = [...initMatrix.roomList.directs]; - - return directIds.find((roomId) => { - const dRoom = mx.getRoom(roomId); - const roomMembers = dRoom.getMembers(); - if (roomMembers.length <= 2 && dRoom.getMember(userId)) { - return true; - } - return false; - }); -} - export function joinRuleToIconSrc(joinRule, isSpace) { return ({ restricted: () => (isSpace ? SpaceIC : HashIC), diff --git a/src/util/mimetypes.js b/src/util/mimetypes.js deleted file mode 100644 index bf7efbce2..000000000 --- a/src/util/mimetypes.js +++ /dev/null @@ -1,39 +0,0 @@ -// https://github.com/matrix-org/matrix-react-sdk/blob/cd15e08fc285da42134817cce50de8011809cd53/src/utils/blobs.ts -export const ALLOWED_BLOB_MIMETYPES = [ - 'image/jpeg', - 'image/gif', - 'image/png', - 'image/apng', - 'image/webp', - 'image/avif', - - 'video/mp4', - 'video/webm', - 'video/ogg', - 'video/quicktime', - - 'audio/mp4', - 'audio/webm', - 'audio/aac', - 'audio/mpeg', - 'audio/ogg', - 'audio/wave', - 'audio/wav', - 'audio/x-wav', - 'audio/x-pn-wav', - 'audio/flac', - 'audio/x-flac', -]; - -export function getBlobSafeMimeType(mimetype) { - if (typeof mimetype !== 'string') return 'application/octet-stream'; - const [type] = mimetype.split(';'); - if (!ALLOWED_BLOB_MIMETYPES.includes(type)) { - return 'application/octet-stream'; - } - // Required for Chromium browsers - if (type === 'video/quicktime') { - return 'video/mp4'; - } - return type; -} diff --git a/src/util/sanitize.js b/src/util/sanitize.js deleted file mode 100644 index 3723a11b3..000000000 --- a/src/util/sanitize.js +++ /dev/null @@ -1,140 +0,0 @@ -import sanitizeHtml from 'sanitize-html'; - -const MAX_TAG_NESTING = 100; -let mx = null; - -const permittedHtmlTags = [ - 'font', 'del', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub', - 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 's', 'code', - 'hr', 'br', 'div', 'table', 'thead', 'tbody', 'tr', 'th', - 'td', 'caption', 'pre', 'span', 'img', 'details', 'summary', -]; - -const urlSchemes = ['https', 'http', 'ftp', 'mailto', 'magnet']; - -const permittedTagToAttributes = { - font: ['style', 'data-mx-bg-color', 'data-mx-color', 'color'], - span: ['style', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'data-mx-maths', 'data-mx-pill', 'data-mx-ping'], - div: ['data-mx-maths'], - a: ['name', 'target', 'href', 'rel'], - img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'], - ol: ['start'], - code: ['class'], -}; - -function transformFontTag(tagName, attribs) { - return { - tagName, - attribs: { - ...attribs, - style: `background-color: ${attribs['data-mx-bg-color']}; color: ${attribs['data-mx-color']}`, - }, - }; -} - -function transformSpanTag(tagName, attribs) { - return { - tagName, - attribs: { - ...attribs, - style: `background-color: ${attribs['data-mx-bg-color']}; color: ${attribs['data-mx-color']}`, - }, - }; -} - -function transformATag(tagName, attribs) { - const userLink = decodeURIComponent(attribs.href).match(/^https?:\/\/matrix.to\/#\/(@.+:.+)/); - if (userLink !== null) { - // convert user link to pill - const userId = userLink[1]; - const pill = { - tagName: 'span', - attribs: { - 'data-mx-pill': userId, - }, - }; - if (userId === mx?.getUserId()) { - pill.attribs['data-mx-ping'] = undefined; - } - return pill; - } - - const rex = /[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/ug; - const newHref = attribs.href.replace(rex, (match) => `[e-${match.codePointAt(0).toString(16)}]`); - - return { - tagName, - attribs: { - ...attribs, - href: newHref, - rel: 'noopener', - target: '_blank', - }, - }; -} - -function transformImgTag(tagName, attribs) { - const { src } = attribs; - if (src.startsWith('mxc://') === false) { - return { - tagName: 'a', - attribs: { - href: src, - rel: 'noopener', - target: '_blank', - }, - text: attribs.alt || src, - }; - } - return { - tagName, - attribs: { - ...attribs, - src: mx?.mxcUrlToHttp(src), - }, - }; -} - -export function sanitizeCustomHtml(matrixClient, body) { - mx = matrixClient; - return sanitizeHtml(body, { - allowedTags: permittedHtmlTags, - allowedAttributes: permittedTagToAttributes, - disallowedTagsMode: 'discard', - allowedSchemes: urlSchemes, - allowedSchemesByTag: { - a: urlSchemes, - }, - allowedSchemesAppliedToAttributes: ['href'], - allowProtocolRelative: false, - allowedClasses: { - code: ['language-*'], - }, - allowedStyles: { - '*': { - color: [/^#(?:[0-9a-fA-F]{3}){1,2}$/], - 'background-color': [/^#(?:[0-9a-fA-F]{3}){1,2}$/], - }, - }, - transformTags: { - font: transformFontTag, - span: transformSpanTag, - a: transformATag, - img: transformImgTag, - }, - nonTextTags: ['style', 'script', 'textarea', 'option', 'noscript', 'mx-reply'], - nestingLimit: MAX_TAG_NESTING, - }); -} - -export function sanitizeText(body) { - const tagsToReplace = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - }; - return body.replace(/[&<>'"]/g, (tag) => tagsToReplace[tag] || tag); -} diff --git a/src/util/twemojify.jsx b/src/util/twemojify.jsx deleted file mode 100644 index ad203a916..000000000 --- a/src/util/twemojify.jsx +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -import linkifyHtml from 'linkify-html'; -import parse from 'html-react-parser'; -import { sanitizeText } from './sanitize'; - -export const TWEMOJI_BASE_URL = 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/'; - -/** - * @param {string} text - text to twemojify - * @param {object|undefined} opts - DEPRECATED - options for tweomoji.parse - * @param {boolean} [linkify=false] - convert links to html tags (default: false) - * @param {boolean} [sanitize=true] - sanitize html text (default: true) - * @param {boolean} [maths=false] - DEPRECATED - render maths (default: false) - * @returns React component - */ -export function twemojify(text, opts, linkify = false, sanitize = true) { - if (typeof text !== 'string') return text; - let content = text; - - if (sanitize) { - content = sanitizeText(content); - } - - if (linkify) { - content = linkifyHtml(content, { - target: '_blank', - rel: 'noreferrer noopener', - }); - } - return parse(content); -}