(chore) remove outdated code (#1765)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Mon, 8 Jul 2024 11:27:10 +0000 (16:57 +0530)
committerGitHub <noreply@github.com>
Mon, 8 Jul 2024 11:27:10 +0000 (21:27 +1000)
* optimize room typing members hook

* remove unused code - WIP

* remove old code from initMatrix

* remove twemojify function

* remove old sanitize util

* delete old markdown util

* delete Math atom component

* uninstall unused dependencies

* remove old notification system

* decrypt message in inbox notification center and fix refresh in background

* improve notification

---------

Co-authored-by: Krishan <33421343+kfiven@users.noreply.github.com>
147 files changed:
index.html
package-lock.json
package.json
src/app/atoms/avatar/Avatar.jsx
src/app/atoms/math/Math.jsx [deleted file]
src/app/atoms/math/Math.scss [deleted file]
src/app/features/room/RoomInput.tsx
src/app/features/room/RoomTimeline.tsx
src/app/hooks/useCategorizedSpaces.js [deleted file]
src/app/hooks/useCommands.ts
src/app/hooks/usePreviousValue.ts [new file with mode: 0644]
src/app/hooks/useRoomTypingMembers.ts
src/app/hooks/useSelectedSpace.js [deleted file]
src/app/hooks/useSelectedTab.js [deleted file]
src/app/hooks/useSpaceShortcut.js [deleted file]
src/app/molecules/dialog/Dialog.jsx
src/app/molecules/following-members/FollowingMembers.jsx [deleted file]
src/app/molecules/following-members/FollowingMembers.scss [deleted file]
src/app/molecules/image-lightbox/ImageLightbox.jsx [deleted file]
src/app/molecules/image-lightbox/ImageLightbox.scss [deleted file]
src/app/molecules/media/Media.jsx [deleted file]
src/app/molecules/media/Media.scss [deleted file]
src/app/molecules/message/Message.jsx [deleted file]
src/app/molecules/message/Message.scss [deleted file]
src/app/molecules/message/TimelineChange.jsx [deleted file]
src/app/molecules/message/TimelineChange.scss [deleted file]
src/app/molecules/people-selector/PeopleSelector.jsx
src/app/molecules/popup-window/PopupWindow.jsx
src/app/molecules/room-notification/RoomNotification.jsx
src/app/molecules/room-options/RoomOptions.jsx [deleted file]
src/app/molecules/room-profile/RoomProfile.jsx
src/app/molecules/room-search/RoomSearch.jsx [deleted file]
src/app/molecules/room-search/RoomSearch.scss [deleted file]
src/app/molecules/room-selector/RoomSelector.jsx
src/app/molecules/room-tile/RoomTile.jsx
src/app/molecules/sidebar-avatar/SidebarAvatar.jsx [deleted file]
src/app/molecules/sidebar-avatar/SidebarAvatar.scss [deleted file]
src/app/molecules/space-add-existing/SpaceAddExisting.jsx
src/app/molecules/space-options/SpaceOptions.jsx [deleted file]
src/app/molecules/sso-buttons/SSOButtons.jsx [deleted file]
src/app/molecules/sso-buttons/SSOButtons.scss [deleted file]
src/app/organisms/create-room/CreateRoom.jsx
src/app/organisms/emoji-board/EmojiBoard.jsx [deleted file]
src/app/organisms/emoji-board/EmojiBoard.scss [deleted file]
src/app/organisms/emoji-board/EmojiBoardOpener.jsx [deleted file]
src/app/organisms/emoji-board/custom-emoji.js
src/app/organisms/emoji-board/emoji.js [deleted file]
src/app/organisms/emoji-board/recent.js [deleted file]
src/app/organisms/emoji-verification/EmojiVerification.jsx
src/app/organisms/invite-list/InviteList.jsx [deleted file]
src/app/organisms/invite-list/InviteList.scss [deleted file]
src/app/organisms/invite-user/InviteUser.jsx
src/app/organisms/join-alias/JoinAlias.jsx
src/app/organisms/navigation/Directs.jsx [deleted file]
src/app/organisms/navigation/Drawer.jsx [deleted file]
src/app/organisms/navigation/Drawer.scss [deleted file]
src/app/organisms/navigation/DrawerBreadcrumb.jsx [deleted file]
src/app/organisms/navigation/DrawerBreadcrumb.scss [deleted file]
src/app/organisms/navigation/DrawerHeader.jsx [deleted file]
src/app/organisms/navigation/DrawerHeader.scss [deleted file]
src/app/organisms/navigation/Home.jsx [deleted file]
src/app/organisms/navigation/Navigation.jsx [deleted file]
src/app/organisms/navigation/Navigation.scss [deleted file]
src/app/organisms/navigation/RoomsCategory.jsx [deleted file]
src/app/organisms/navigation/RoomsCategory.scss [deleted file]
src/app/organisms/navigation/Selector.jsx [deleted file]
src/app/organisms/navigation/SideBar.jsx [deleted file]
src/app/organisms/navigation/SideBar.scss [deleted file]
src/app/organisms/profile-editor/ProfileEditor.jsx
src/app/organisms/profile-viewer/ProfileViewer.jsx
src/app/organisms/public-rooms/PublicRooms.jsx [deleted file]
src/app/organisms/public-rooms/PublicRooms.scss [deleted file]
src/app/organisms/pw/Dialogs.jsx
src/app/organisms/pw/Windows.jsx
src/app/organisms/read-receipts/ReadReceipts.jsx [deleted file]
src/app/organisms/room/EventLimit.js [deleted file]
src/app/organisms/room/PeopleDrawer.jsx [deleted file]
src/app/organisms/room/PeopleDrawer.scss [deleted file]
src/app/organisms/room/Room.scss [deleted file]
src/app/organisms/room/RoomSettings.jsx
src/app/organisms/room/RoomView.scss [deleted file]
src/app/organisms/room/RoomViewCmdBar.jsx [deleted file]
src/app/organisms/room/RoomViewCmdBar.scss [deleted file]
src/app/organisms/room/RoomViewContent.jsx [deleted file]
src/app/organisms/room/RoomViewContent.scss [deleted file]
src/app/organisms/room/RoomViewFloating.jsx [deleted file]
src/app/organisms/room/RoomViewFloating.scss [deleted file]
src/app/organisms/room/RoomViewHeader.jsx [deleted file]
src/app/organisms/room/RoomViewHeader.scss [deleted file]
src/app/organisms/room/RoomViewInput.jsx [deleted file]
src/app/organisms/room/RoomViewInput.scss [deleted file]
src/app/organisms/room/TimelineScroll.js [deleted file]
src/app/organisms/room/commands.jsx [deleted file]
src/app/organisms/room/commands.scss [deleted file]
src/app/organisms/room/common.jsx [deleted file]
src/app/organisms/search/Search.jsx
src/app/organisms/settings/CrossSigning.jsx
src/app/organisms/settings/KeyBackup.jsx
src/app/organisms/shortcut-spaces/ShortcutSpaces.jsx [deleted file]
src/app/organisms/shortcut-spaces/ShortcutSpaces.scss [deleted file]
src/app/organisms/space-manage/SpaceManage.jsx [deleted file]
src/app/organisms/space-manage/SpaceManage.scss [deleted file]
src/app/organisms/space-settings/SpaceSettings.jsx
src/app/organisms/sticker-board/StickerBoard.jsx [deleted file]
src/app/organisms/sticker-board/StickerBoard.scss [deleted file]
src/app/organisms/view-source/ViewSource.jsx [deleted file]
src/app/organisms/view-source/ViewSource.scss [deleted file]
src/app/organisms/welcome/Welcome.jsx [deleted file]
src/app/organisms/welcome/Welcome.scss [deleted file]
src/app/pages/Router.tsx
src/app/pages/client/ClientNonUIFeatures.tsx [new file with mode: 0644]
src/app/pages/client/ClientRoot.tsx
src/app/pages/client/inbox/Notifications.tsx
src/app/state/room/roomToUnread.ts
src/app/state/typingMembers.ts
src/app/templates/auth/Auth.jsx [deleted file]
src/app/templates/auth/Auth.scss [deleted file]
src/app/templates/client/Client.jsx [deleted file]
src/app/templates/client/Client.scss [deleted file]
src/app/templates/client/ClientContent.jsx [deleted file]
src/app/utils/disposable.ts
src/app/utils/dom.ts
src/client/action/accountData.js [deleted file]
src/client/action/auth.js [deleted file]
src/client/action/auth.ts [new file with mode: 0644]
src/client/action/navigation.js
src/client/action/notifications.js
src/client/action/room.js
src/client/action/roomTimeline.js [deleted file]
src/client/event/hotkeys.js
src/client/event/roomList.js [deleted file]
src/client/initMatrix.js
src/client/state/AccountData.js [deleted file]
src/client/state/Notifications.js [deleted file]
src/client/state/RoomList.js [deleted file]
src/client/state/RoomTimeline.js [deleted file]
src/client/state/RoomsHierarchy.js [deleted file]
src/client/state/RoomsInput.js [deleted file]
src/client/state/cons.js
src/client/state/navigation.js
src/util/Postie.js [deleted file]
src/util/colorMXID.js
src/util/markdown.js [deleted file]
src/util/matrixUtil.js
src/util/mimetypes.js [deleted file]
src/util/sanitize.js [deleted file]
src/util/twemojify.jsx [deleted file]

index af2952d38cb319909d8ea1ce2448e091a361caae..9196cf3dcd0150134d55a9aa2bff7a2743db2e56 100644 (file)
       window.global ||= window;
     </script>
     <div id="root"></div>
-    <audio id="notificationSound">
-      <source src="./public/sound/notification.ogg" type="audio/ogg" />
-    </audio>
-    <audio id="inviteSound">
-      <source src="./public/sound/invite.ogg" type="audio/ogg" />
-    </audio>
     <script type="module" src="./src/index.tsx"></script>
   </body>
 </html>
index 17e4dd509b2f8525d3aa6269445c6b056d749f78..ab70f0fbb375d2dfd834698196f4923b51de5ddb 100644 (file)
@@ -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": {
       "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",
         "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",
     "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": "*",
     "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",
         "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",
         "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",
     "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",
         "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",
     "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",
         "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",
       "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",
         "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",
         "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",
         "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",
         "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",
       "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",
index e626e83738cb1b398d6f40752b0f6280d66d4b31..a4bf6f34b4b6ea047b912f168838a92b33a0c383 100644 (file)
@@ -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": {
index fc46c250453e0648f9b97ddd34de371a13c8d8cd..27bf7c906bb3b1b4b89b92b7a0c47e4dbd0d5d90 100644 (file)
@@ -2,17 +2,13 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import './Avatar.scss';
 
-import { twemojify } from '../../../util/twemojify';
-
 import Text from '../text/Text';
 import RawIcon from '../system-icons/RawIcon';
 
 import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg';
 import { avatarInitials } from '../../../util/common';
 
-const Avatar = React.forwardRef(({
-  text, bgColor, iconSrc, iconColor, imageSrc, size,
-}, ref) => {
+const Avatar = React.forwardRef(({ text, bgColor, iconSrc, iconColor, imageSrc, size }, ref) => {
   let textSize = 's1';
   if (size === 'large') textSize = 'h1';
   if (size === 'small') textSize = 'b1';
@@ -20,34 +16,34 @@ const Avatar = React.forwardRef(({
 
   return (
     <div ref={ref} className={`avatar-container avatar-container__${size} noselect`}>
-      {
-        imageSrc !== null
-          ? (
-            <img
-              draggable="false"
-              src={imageSrc}
-              onLoad={(e) => { e.target.style.backgroundColor = 'transparent'; }}
-              onError={(e) => { e.target.src = ImageBrokenSVG; }}
-              alt=""
-            />
-          )
-          : (
-            <span
-              style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
-              className={`avatar__border${iconSrc !== null ? '--active' : ''}`}
-            >
-              {
-                iconSrc !== null
-                  ? <RawIcon size={size} src={iconSrc} color={iconColor} />
-                  : text !== null && (
-                    <Text variant={textSize} primary>
-                      {twemojify(avatarInitials(text))}
-                    </Text>
-                  )
-              }
-            </span>
-          )
-      }
+      {imageSrc !== null ? (
+        <img
+          draggable="false"
+          src={imageSrc}
+          onLoad={(e) => {
+            e.target.style.backgroundColor = 'transparent';
+          }}
+          onError={(e) => {
+            e.target.src = ImageBrokenSVG;
+          }}
+          alt=""
+        />
+      ) : (
+        <span
+          style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
+          className={`avatar__border${iconSrc !== null ? '--active' : ''}`}
+        >
+          {iconSrc !== null ? (
+            <RawIcon size={size} src={iconSrc} color={iconColor} />
+          ) : (
+            text !== null && (
+              <Text variant={textSize} primary>
+                {avatarInitials(text)}
+              </Text>
+            )
+          )}
+        </span>
+      )}
     </div>
   );
 });
diff --git a/src/app/atoms/math/Math.jsx b/src/app/atoms/math/Math.jsx
deleted file mode 100644 (file)
index ab52a47..0000000
+++ /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 <span ref={ref} />;
-});
-Math.defaultProps = {
-  throwOnError: null,
-  errorColor: null,
-  displayMode: null,
-};
-Math.propTypes = {
-  content: PropTypes.string.isRequired,
-  throwOnError: PropTypes.bool,
-  errorColor: PropTypes.string,
-  displayMode: PropTypes.bool,
-};
-
-export default Math;
diff --git a/src/app/atoms/math/Math.scss b/src/app/atoms/math/Math.scss
deleted file mode 100644 (file)
index 306b147..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-.katex-display {
-  margin: 0 !important;
-}
index 6f9a2495b1b15304a339cf58a454440f7c83bbc2..2728a54c8e84273f4202de51bf17fb901d273cfa 100644 (file)
@@ -8,7 +8,7 @@ import React, {
   useRef,
   useState,
 } from 'react';
-import { useAtom } from 'jotai';
+import { useAtom, useAtomValue } from 'jotai';
 import { isKeyHotkey } from 'is-hotkey';
 import { EventType, IContent, MsgType, Room } from 'matrix-js-sdk';
 import { ReactEditor } from 'slate-react';
@@ -56,7 +56,6 @@ import {
 } from '../../components/editor';
 import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
 import { UseStateProvider } from '../../components/UseStateProvider';
-import initMatrix from '../../../client/initMatrix';
 import { TUploadContent, encryptFile, getImageInfo, getMxIdLocalPart } from '../../utils/matrix';
 import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater';
 import { useFilePicker } from '../../hooks/useFilePicker';
@@ -95,6 +94,7 @@ import {
 } from './msgContent';
 import colorMXID from '../../../util/colorMXID';
 import {
+  getAllParents,
   getMemberDisplayName,
   parseReplyBody,
   parseReplyFormattedBody,
@@ -107,6 +107,7 @@ import { Command, SHRUG, useCommands } from '../../hooks/useCommands';
 import { mobileOrTablet } from '../../utils/user-agent';
 import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
 import { ReplyLayout } from '../../components/message';
+import { roomToParentsAtom } from '../../state/room/roomToParents';
 
 interface RoomInputProps {
   editor: Editor;
@@ -121,6 +122,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
     const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
     const commands = useCommands(mx, room);
     const emojiBtnRef = useRef<HTMLButtonElement>(null);
+    const roomToParents = useAtomValue(roomToParentsAtom);
 
     const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
     const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
@@ -133,13 +135,13 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
     const uploadBoardHandlers = useRef<UploadBoardImperativeHandlers>();
 
     const imagePackRooms: Room[] = useMemo(() => {
-      const allParentSpaces = [roomId, ...(initMatrix.roomList?.getAllParentSpaces(roomId) ?? [])];
+      const allParentSpaces = [roomId].concat(Array.from(getAllParents(roomToParents, roomId)));
       return allParentSpaces.reduce<Room[]>((list, rId) => {
         const r = mx.getRoom(rId);
         if (r) list.push(r);
         return list;
       }, []);
-    }, [mx, roomId]);
+    }, [mx, roomId, roomToParents]);
 
     const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar');
     const [autocompleteQuery, setAutocompleteQuery] =
index 29b874fca2e2427a5f55018647db10820d6ad44d..14cdb56e84727ed96e3b760bf5a1debd39efa5a9 100644 (file)
@@ -28,7 +28,7 @@ import classNames from 'classnames';
 import { ReactEditor } from 'slate-react';
 import { Editor } from 'slate';
 import to from 'await-to-js';
-import { useSetAtom } from 'jotai';
+import { useAtomValue, useSetAtom } from 'jotai';
 import {
   Badge,
   Box,
@@ -74,6 +74,7 @@ import { getReactCustomHtmlParser } from '../../plugins/react-custom-html-parser
 import {
   canEditEvent,
   decryptAllTimelineEvent,
+  getAllParents,
   getEditedEvent,
   getEventReactions,
   getLatestEditableEvt,
@@ -103,14 +104,15 @@ import { createMentionElement, isEmptyEditor, moveCursor } from '../../component
 import { roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts';
 import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
 import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
-import initMatrix from '../../../client/initMatrix';
 import { useKeyDown } from '../../hooks/useKeyDown';
-import cons from '../../../client/state/cons';
 import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange';
 import { RenderMessageContent } from '../../components/RenderMessageContent';
 import { Image } from '../../components/media';
 import { ImageViewer } from '../../components/image-viewer';
 import { useRoomNavigate } from '../../hooks/useRoomNavigate';
+import { roomToParentsAtom } from '../../state/room/roomToParents';
+import { useRoomUnread } from '../../state/hooks/unread';
+import { roomToUnreadAtom } from '../../state/room/roomToUnread';
 
 const TimelineFloat = as<'div', css.TimelineFloatVariants>(
   ({ position, className, ...props }, ref) => (
@@ -444,18 +446,19 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
   const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel);
   const [editId, setEditId] = useState<string>();
   const { navigateRoom, navigateSpace } = useRoomNavigate();
+  const roomToParents = useAtomValue(roomToParentsAtom);
+  const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
 
   const imagePackRooms: Room[] = useMemo(() => {
-    const allParentSpaces = [
-      room.roomId,
-      ...(initMatrix.roomList?.getAllParentSpaces(room.roomId) ?? []),
-    ];
+    const allParentSpaces = [room.roomId].concat(
+      Array.from(getAllParents(roomToParents, room.roomId))
+    );
     return allParentSpaces.reduce<Room[]>((list, rId) => {
       const r = mx.getRoom(rId);
       if (r) list.push(r);
       return list;
     }, []);
-  }, [mx, room]);
+  }, [mx, room, roomToParents]);
 
   const [unreadInfo, setUnreadInfo] = useState(() => getRoomUnreadInfo(room, true));
   const readUptoEventIdRef = useRef<string>();
@@ -794,15 +797,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
 
   // Remove unreadInfo on mark as read
   useEffect(() => {
-    const handleFullRead = (rId: string) => {
-      if (rId !== room.roomId) return;
+    if (!unread) {
       setUnreadInfo(undefined);
-    };
-    initMatrix.notifications?.on(cons.events.notifications.FULL_READ, handleFullRead);
-    return () => {
-      initMatrix.notifications?.removeListener(cons.events.notifications.FULL_READ, handleFullRead);
-    };
-  }, [room]);
+    }
+  }, [unread]);
 
   // scroll out of view msg editor in view.
   useEffect(() => {
diff --git a/src/app/hooks/useCategorizedSpaces.js b/src/app/hooks/useCategorizedSpaces.js
deleted file mode 100644 (file)
index 6d3c02a..0000000
+++ /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];
-}
index aadbf5348ba5998897fd521ac51be6a98a80a093..3c82951492c4fa1c68225681d305dfd95d913399 100644 (file)
@@ -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 (file)
index 0000000..01b4850
--- /dev/null
@@ -0,0 +1,11 @@
+import { useEffect, useRef } from 'react';
+
+export const usePreviousValue = <T>(currentValue: T, initialValue: T) => {
+  const valueRef = useRef(initialValue);
+
+  useEffect(() => {
+    valueRef.current = currentValue;
+  }, [currentValue]);
+
+  return valueRef.current;
+};
index 5f24fb5dfc543a23e2fc395c2b373e22b1a873dd..f526cbf163a2ae430c9b804667d84b4e80bdb959 100644 (file)
@@ -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 (file)
index 535c5dc..0000000
+++ /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 (file)
index 33b76a8..0000000
+++ /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 (file)
index a1710c6..0000000
+++ /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];
-}
index 2f29971d54295725570be73218270b835c4d0ea7..478a085585c799cdd222729bfcdf8d85713bf9b2 100644 (file)
@@ -2,16 +2,21 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import './Dialog.scss';
 
-import { twemojify } from '../../../util/twemojify';
-
 import Text from '../../atoms/text/Text';
 import Header, { TitleWrapper } from '../../atoms/header/Header';
 import ScrollView from '../../atoms/scroll/ScrollView';
 import RawModal from '../../atoms/modal/RawModal';
 
 function Dialog({
-  className, isOpen, title, onAfterOpen, onAfterClose,
-  contentOptions, onRequestClose, closeFromOutside, children,
+  className,
+  isOpen,
+  title,
+  onAfterOpen,
+  onAfterClose,
+  contentOptions,
+  onRequestClose,
+  closeFromOutside,
+  children,
   invisibleScroll,
 }) {
   return (
@@ -28,19 +33,19 @@ function Dialog({
         <div className="dialog__content">
           <Header>
             <TitleWrapper>
-              {
-                typeof title === 'string'
-                  ? <Text variant="h2" weight="medium" primary>{twemojify(title)}</Text>
-                  : title
-              }
+              {typeof title === 'string' ? (
+                <Text variant="h2" weight="medium" primary>
+                  {title}
+                </Text>
+              ) : (
+                title
+              )}
             </TitleWrapper>
             {contentOptions}
           </Header>
           <div className="dialog__content__wrapper">
             <ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
-              <div className="dialog__content-container">
-                {children}
-              </div>
+              <div className="dialog__content-container">{children}</div>
             </ScrollView>
           </div>
         </div>
diff --git a/src/app/molecules/following-members/FollowingMembers.jsx b/src/app/molecules/following-members/FollowingMembers.jsx
deleted file mode 100644 (file)
index 949dac7..0000000
+++ /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 && (
-      <button
-        className="following-members"
-        onClick={() => openReadReceipts(roomId, followingMembers)}
-        type="button"
-      >
-        <RawIcon size="extra-small" src={TickMarkIC} />
-        <Text variant="b2">
-          {getUsersActionJsx(roomId, filteredM, 'following the conversation.')}
-        </Text>
-      </button>
-    )
-  );
-}
-
-FollowingMembers.propTypes = {
-  roomTimeline: PropTypes.shape({}).isRequired,
-};
-
-export default FollowingMembers;
diff --git a/src/app/molecules/following-members/FollowingMembers.scss b/src/app/molecules/following-members/FollowingMembers.scss
deleted file mode 100644 (file)
index a0daf5a..0000000
+++ /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 (file)
index c1c45db..0000000
+++ /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 (
-    <RawModal
-      className="image-lightbox__modal"
-      overlayClassName="image-lightbox__overlay"
-      isOpen={isOpen}
-      onRequestClose={onRequestClose}
-      size="large"
-    >
-      <div className="image-lightbox__header">
-        <Text variant="b2" weight="medium">{alt}</Text>
-        <IconButton onClick={() => window.open(url)} size="small" src={ExternalSVG} />
-        <IconButton onClick={handleDownload} size="small" src={DownloadSVG} />
-      </div>
-      <div className="image-lightbox__content">
-        <img src={url} alt={alt} />
-      </div>
-    </RawModal>
-  );
-}
-
-ImageLightbox.propTypes = {
-  url: PropTypes.string.isRequired,
-  alt: PropTypes.string.isRequired,
-  isOpen: PropTypes.bool.isRequired,
-  onRequestClose: PropTypes.func.isRequired,
-};
-
-export default ImageLightbox;
diff --git a/src/app/molecules/image-lightbox/ImageLightbox.scss b/src/app/molecules/image-lightbox/ImageLightbox.scss
deleted file mode 100644 (file)
index 201fc91..0000000
+++ /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 (file)
index e2b6177..0000000
+++ /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 (
-    <div className="file-header">
-      <Text className="file-name" variant="b3">{name}</Text>
-      { link !== null && (
-        <>
-          {
-            external && (
-              <IconButton
-                size="extra-small"
-                tooltip="Open in new tab"
-                src={ExternalSVG}
-                onClick={() => window.open(url || link)}
-              />
-            )
-          }
-          <a href={url || link} download={name} target="_blank" rel="noreferrer">
-            <IconButton
-              size="extra-small"
-              tooltip="Download"
-              src={DownloadSVG}
-              onClick={handleDownload}
-            />
-          </a>
-        </>
-      )}
-    </div>
-  );
-}
-FileHeader.defaultProps = {
-  external: false,
-  file: null,
-  link: null,
-};
-FileHeader.propTypes = {
-  name: PropTypes.string.isRequired,
-  link: PropTypes.string,
-  external: PropTypes.bool,
-  file: PropTypes.shape({}),
-  type: PropTypes.string.isRequired,
-};
-
-function File({
-  name, link, file, type,
-}) {
-  return (
-    <div className="file-container">
-      <FileHeader name={name} link={link} file={file} type={type} />
-    </div>
-  );
-}
-File.defaultProps = {
-  file: null,
-  type: '',
-};
-File.propTypes = {
-  name: PropTypes.string.isRequired,
-  link: PropTypes.string.isRequired,
-  type: PropTypes.string,
-  file: PropTypes.shape({}),
-};
-
-function Image({
-  name, width, height, link, file, type, blurhash,
-}) {
-  const [url, setUrl] = useState(null);
-  const [blur, setBlur] = useState(true);
-  const [lightbox, setLightbox] = useState(false);
-
-  useEffect(() => {
-    let unmounted = false;
-    async function fetchUrl() {
-      const myUrl = await getUrl(link, type, file);
-      if (unmounted) return;
-      setUrl(myUrl);
-    }
-    fetchUrl();
-    return () => {
-      unmounted = true;
-    };
-  }, []);
-
-  const toggleLightbox = () => {
-    if (!url) return;
-    setLightbox(!lightbox);
-  };
-
-  return (
-    <>
-      <div className="file-container">
-        <div
-          style={{ height: width !== null ? getNativeHeight(width, height) : 'unset' }}
-          className="image-container"
-          role="button"
-          tabIndex="0"
-          onClick={toggleLightbox}
-          onKeyDown={toggleLightbox}
-        >
-          { blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
-          { url !== null && (
-            <img
-              style={{ display: blur ? 'none' : 'unset' }}
-              onLoad={() => setBlur(false)}
-              src={url || link}
-              alt={name}
-            />
-          )}
-        </div>
-      </div>
-      {url && (
-        <ImageLightbox
-          url={url}
-          alt={name}
-          isOpen={lightbox}
-          onRequestClose={toggleLightbox}
-        />
-      )}
-    </>
-  );
-}
-Image.defaultProps = {
-  file: null,
-  width: null,
-  height: null,
-  type: '',
-  blurhash: '',
-};
-Image.propTypes = {
-  name: PropTypes.string.isRequired,
-  width: PropTypes.number,
-  height: PropTypes.number,
-  link: PropTypes.string.isRequired,
-  file: PropTypes.shape({}),
-  type: PropTypes.string,
-  blurhash: PropTypes.string,
-};
-
-function Sticker({
-  name, height, width, link, file, type,
-}) {
-  const [url, setUrl] = useState(null);
-
-  useEffect(() => {
-    let unmounted = false;
-    async function fetchUrl() {
-      const myUrl = await getUrl(link, type, file);
-      if (unmounted) return;
-      setUrl(myUrl);
-    }
-    fetchUrl();
-    return () => {
-      unmounted = true;
-    };
-  }, []);
-
-  return (
-    <div className="sticker-container" style={{ height: width !== null ? getNativeHeight(width, height, 128) : 'unset' }}>
-      { url !== null && <img src={url || link} title={name} alt={name} />}
-    </div>
-  );
-}
-Sticker.defaultProps = {
-  file: null,
-  type: '',
-  width: null,
-  height: null,
-};
-Sticker.propTypes = {
-  name: PropTypes.string.isRequired,
-  width: PropTypes.number,
-  height: PropTypes.number,
-  link: PropTypes.string.isRequired,
-  file: PropTypes.shape({}),
-  type: PropTypes.string,
-};
-
-function Audio({
-  name, link, type, file,
-}) {
-  const [isLoading, setIsLoading] = useState(false);
-  const [url, setUrl] = useState(null);
-
-  async function loadAudio() {
-    const myUrl = await getUrl(link, type, file);
-    setUrl(myUrl);
-    setIsLoading(false);
-  }
-  function handlePlayAudio() {
-    setIsLoading(true);
-    loadAudio();
-  }
-
-  return (
-    <div className="file-container">
-      <FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
-      <div className="audio-container">
-        { url === null && isLoading && <Spinner size="small" /> }
-        { url === null && !isLoading && <IconButton onClick={handlePlayAudio} tooltip="Play audio" src={PlaySVG} />}
-        { url !== null && (
-          /* eslint-disable-next-line jsx-a11y/media-has-caption */
-          <audio autoPlay controls>
-            <source src={url} type={getBlobSafeMimeType(type)} />
-          </audio>
-        )}
-      </div>
-    </div>
-  );
-}
-Audio.defaultProps = {
-  file: null,
-  type: '',
-};
-Audio.propTypes = {
-  name: PropTypes.string.isRequired,
-  link: PropTypes.string.isRequired,
-  type: PropTypes.string,
-  file: PropTypes.shape({}),
-};
-
-function Video({
-  name, link, thumbnail, thumbnailFile, thumbnailType,
-  width, height, file, type, blurhash,
-}) {
-  const [isLoading, setIsLoading] = useState(false);
-  const [url, setUrl] = useState(null);
-  const [thumbUrl, setThumbUrl] = useState(null);
-  const [blur, setBlur] = useState(true);
-
-  useEffect(() => {
-    let unmounted = false;
-    async function fetchUrl() {
-      const myThumbUrl = await getUrl(thumbnail, thumbnailType, thumbnailFile);
-      if (unmounted) return;
-      setThumbUrl(myThumbUrl);
-    }
-    if (thumbnail !== null) fetchUrl();
-    return () => {
-      unmounted = true;
-    };
-  }, []);
-
-  const loadVideo = async () => {
-    const myUrl = await getUrl(link, type, file);
-    setUrl(myUrl);
-    setIsLoading(false);
-  };
-
-  const handlePlayVideo = () => {
-    setIsLoading(true);
-    loadVideo();
-  };
-
-  return (
-    <div className="file-container">
-      <FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
-      <div
-        style={{
-          height: width !== null ? getNativeHeight(width, height) : 'unset',
-        }}
-        className="video-container"
-      >
-        { url === null ? (
-          <>
-            { blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
-            { thumbUrl !== null && (
-              <img style={{ display: blur ? 'none' : 'unset' }} src={thumbUrl} onLoad={() => setBlur(false)} alt={name} />
-            )}
-            {isLoading && <Spinner size="small" />}
-            {!isLoading && <IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />}
-          </>
-        ) : (
-          /* eslint-disable-next-line jsx-a11y/media-has-caption */
-          <video autoPlay controls poster={thumbUrl}>
-            <source src={url} type={getBlobSafeMimeType(type)} />
-          </video>
-        )}
-      </div>
-    </div>
-  );
-}
-Video.defaultProps = {
-  width: null,
-  height: null,
-  file: null,
-  thumbnail: null,
-  thumbnailType: null,
-  thumbnailFile: null,
-  type: '',
-  blurhash: null,
-};
-Video.propTypes = {
-  name: PropTypes.string.isRequired,
-  link: PropTypes.string.isRequired,
-  thumbnail: PropTypes.string,
-  thumbnailFile: PropTypes.shape({}),
-  thumbnailType: PropTypes.string,
-  width: PropTypes.number,
-  height: PropTypes.number,
-  file: PropTypes.shape({}),
-  type: PropTypes.string,
-  blurhash: PropTypes.string,
-};
-
-export {
-  File, Image, Sticker, Audio, Video,
-};
diff --git a/src/app/molecules/media/Media.scss b/src/app/molecules/media/Media.scss
deleted file mode 100644 (file)
index 8d98c42..0000000
+++ /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 (file)
index 26a5b29..0000000
+++ /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 (
-    <div className="ph-msg">
-      <div className="ph-msg__avatar-container">
-        <div className="ph-msg__avatar" />
-      </div>
-      <div className="ph-msg__main-container">
-        <div className="ph-msg__header" />
-        <div className="ph-msg__body">
-          <div />
-          <div />
-          <div />
-          <div />
-        </div>
-      </div>
-    </div>
-  );
-}
-
-const MessageAvatar = React.memo(({
-  roomId, avatarSrc, userId, username,
-}) => (
-  <div className="message__avatar-container">
-    <button type="button" onClick={() => openProfileViewer(userId, roomId)}>
-      <Avatar imageSrc={avatarSrc} text={username} bgColor={colorMXID(userId)} size="small" />
-    </button>
-  </div>
-));
-
-const MessageHeader = React.memo(({
-  userId, username, timestamp, fullTime,
-}) => (
-  <div className="message__header">
-    <Text
-      style={{ color: colorMXID(userId) }}
-      className="message__profile"
-      variant="b1"
-      weight="medium"
-      span
-    >
-      <span>{twemojify(username)}</span>
-      <span>{twemojify(userId)}</span>
-    </Text>
-    <div className="message__time">
-      <Text variant="b3">
-        <Time timestamp={timestamp} fullTime={fullTime} />
-      </Text>
-    </div>
-  </div>
-));
-MessageHeader.defaultProps = {
-  fullTime: false,
-};
-MessageHeader.propTypes = {
-  userId: PropTypes.string.isRequired,
-  username: PropTypes.string.isRequired,
-  timestamp: PropTypes.number.isRequired,
-  fullTime: PropTypes.bool,
-};
-
-function MessageReply({ name, color, body }) {
-  return (
-    <div className="message__reply">
-      <Text variant="b2">
-        <RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
-        <span style={{ color }}>{twemojify(name)}</span>
-        {' '}
-        {twemojify(body)}
-      </Text>
-    </div>
-  );
-}
-
-MessageReply.propTypes = {
-  name: PropTypes.string.isRequired,
-  color: PropTypes.string.isRequired,
-  body: PropTypes.string.isRequired,
-};
-
-const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
-  const [reply, setReply] = useState(null);
-  const isMountedRef = useRef(true);
-
-  useEffect(() => {
-    const mx = initMatrix.matrixClient;
-    const timelineSet = roomTimeline.getUnfilteredTimelineSet();
-    const loadReply = async () => {
-      try {
-        const eTimeline = await mx.getEventTimeline(timelineSet, eventId);
-        await roomTimeline.decryptAllEventsOfTimeline(eTimeline);
-
-        let mEvent = eTimeline.getTimelineSet().findEventById(eventId);
-        const editedList = roomTimeline.editedTimeline.get(mEvent.getId());
-        if (editedList) {
-          mEvent = editedList[editedList.length - 1];
-        }
-
-        const rawBody = mEvent.getContent().body;
-        const username = getUsernameOfRoomMember(mEvent.sender);
-
-        if (isMountedRef.current === false) return;
-        const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***';
-        let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody;
-        if (editedList && parsedBody.startsWith(' * ')) {
-          parsedBody = parsedBody.slice(3);
-        }
-
-        setReply({
-          to: username,
-          color: colorMXID(mEvent.getSender()),
-          body: parsedBody,
-          event: mEvent,
-        });
-      } catch {
-        setReply({
-          to: '** Unknown user **',
-          color: 'var(--tc-danger-normal)',
-          body: '*** Unable to load reply ***',
-          event: null,
-        });
-      }
-    };
-    loadReply();
-
-    return () => {
-      isMountedRef.current = false;
-    };
-  }, []);
-
-  const focusReply = (ev) => {
-    if (!ev.key || ev.key === ' ' || ev.key === 'Enter') {
-      if (ev.key) ev.preventDefault();
-      if (reply?.event === null) return;
-      if (reply?.event.isRedacted()) return;
-      roomTimeline.loadEventTimeline(eventId);
-    }
-  };
-
-  return (
-    <div
-      className="message__reply-wrapper"
-      onClick={focusReply}
-      onKeyDown={focusReply}
-      role="button"
-      tabIndex="0"
-    >
-      {reply !== null && <MessageReply name={reply.to} color={reply.color} body={reply.body} />}
-    </div>
-  );
-});
-MessageReplyWrapper.propTypes = {
-  roomTimeline: PropTypes.shape({}).isRequired,
-  eventId: PropTypes.string.isRequired,
-};
-
-const MessageBody = React.memo(({
-  senderName,
-  body,
-  isCustomHTML,
-  isEdited,
-  msgType,
-}) => {
-  // if body is not string it is a React element.
-  if (typeof body !== 'string') return <div className="message__body">{body}</div>;
-
-  let content = null;
-  if (isCustomHTML) {
-    try {
-      content = twemojify(
-        sanitizeCustomHtml(initMatrix.matrixClient, body),
-        undefined,
-        true,
-        false,
-        true,
-      );
-    } catch {
-      console.error('Malformed custom html: ', body);
-      content = twemojify(body, undefined);
-    }
-  } else {
-    content = twemojify(body, undefined, true);
-  }
-
-  // Determine if this message should render with large emojis
-  // Criteria:
-  // - Contains only emoji
-  // - Contains no more than 10 emoji
-  let emojiOnly = false;
-  if (content.type === 'img') {
-    // If this messages contains only a single (inline) image
-    emojiOnly = true;
-  } else if (content.constructor.name === 'Array') {
-    // Otherwise, it might be an array of images / texb
-
-    // Count the number of emojis
-    const nEmojis = content.filter((e) => e.type === 'img').length;
-
-    // Make sure there's no text besides whitespace and variation selector U+FE0F
-    if (nEmojis <= 10 && content.every((element) => (
-      (typeof element === 'object' && element.type === 'img')
-      || (typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element))
-    ))) {
-      emojiOnly = true;
-    }
-  }
-
-  if (!isCustomHTML) {
-    // If this is a plaintext message, wrap it in a <p> element (automatically applying
-    // white-space: pre-wrap) in order to preserve newlines
-    content = (<p className="message__body-plain">{content}</p>);
-  }
-
-  return (
-    <div className="message__body">
-      <div dir="auto" className={`text ${emojiOnly ? 'text-h1' : 'text-b1'}`}>
-        { msgType === 'm.emote' && (
-          <>
-            {'* '}
-            {twemojify(senderName)}
-            {' '}
-          </>
-        )}
-        { content }
-      </div>
-      { isEdited && <Text className="message__body-edited" variant="b3">(edited)</Text>}
-    </div>
-  );
-});
-MessageBody.defaultProps = {
-  isCustomHTML: false,
-  isEdited: false,
-  msgType: null,
-};
-MessageBody.propTypes = {
-  senderName: PropTypes.string.isRequired,
-  body: PropTypes.node.isRequired,
-  isCustomHTML: PropTypes.bool,
-  isEdited: PropTypes.bool,
-  msgType: PropTypes.string,
-};
-
-function MessageEdit({ body, onSave, onCancel }) {
-  const editInputRef = useRef(null);
-
-  useEffect(() => {
-    // makes the cursor end up at the end of the line instead of the beginning
-    editInputRef.current.value = '';
-    editInputRef.current.value = body;
-  }, []);
-
-  const handleKeyDown = (e) => {
-    if (e.key === 'Escape') {
-      e.preventDefault();
-      onCancel();
-    }
-
-    if (e.key === 'Enter' && e.shiftKey === false) {
-      e.preventDefault();
-      onSave(editInputRef.current.value, body);
-    }
-  };
-
-  return (
-    <form className="message__edit" onSubmit={(e) => { e.preventDefault(); onSave(editInputRef.current.value, body); }}>
-      <Input
-        forwardRef={editInputRef}
-        onKeyDown={handleKeyDown}
-        value={body}
-        placeholder="Edit message"
-        required
-        resizable
-        autoFocus
-      />
-      <div className="message__edit-btns">
-        <Button type="submit" variant="primary">Save</Button>
-        <Button onClick={onCancel}>Cancel</Button>
-      </div>
-    </form>
-  );
-}
-MessageEdit.propTypes = {
-  body: PropTypes.string.isRequired,
-  onSave: PropTypes.func.isRequired,
-  onCancel: PropTypes.func.isRequired,
-};
-
-function getMyEmojiEvent(emojiKey, eventId, roomTimeline) {
-  const mx = initMatrix.matrixClient;
-  const rEvents = roomTimeline.reactionTimeline.get(eventId);
-  let rEvent = null;
-  rEvents?.find((rE) => {
-    if (rE.getRelation() === null) return false;
-    if (rE.getRelation().key === emojiKey && rE.getSender() === mx.getUserId()) {
-      rEvent = rE;
-      return true;
-    }
-    return false;
-  });
-  return rEvent;
-}
-
-function toggleEmoji(roomId, eventId, emojiKey, shortcode, roomTimeline) {
-  const myAlreadyReactEvent = getMyEmojiEvent(emojiKey, eventId, roomTimeline);
-  if (myAlreadyReactEvent) {
-    const rId = myAlreadyReactEvent.getId();
-    if (rId.startsWith('~')) return;
-    redactEvent(roomId, rId);
-    return;
-  }
-  sendReaction(roomId, eventId, emojiKey, shortcode);
-}
-
-function pickEmoji(e, roomId, eventId, roomTimeline) {
-  openEmojiBoard(getEventCords(e), (emoji) => {
-    toggleEmoji(roomId, eventId, emoji.mxc ?? emoji.unicode, emoji.shortcodes[0], roomTimeline);
-    e.target.click();
-  });
-}
-
-function genReactionMsg(userIds, reaction, shortcode) {
-  return (
-    <>
-      {userIds.map((userId, index) => (
-        <React.Fragment key={userId}>
-          {twemojify(getUsername(userId))}
-          {index < userIds.length - 1 && (
-            <span style={{ opacity: '.6' }}>
-              {index === userIds.length - 2 ? ' and ' : ', '}
-            </span>
-          )}
-        </React.Fragment>
-      ))}
-      <span style={{ opacity: '.6' }}>{' reacted with '}</span>
-      {twemojify(shortcode ? `:${shortcode}:` : reaction, { className: 'react-emoji' })}
-    </>
-  );
-}
-
-function MessageReaction({
-  reaction, shortcode, count, users, isActive, onClick,
-}) {
-  let customEmojiUrl = null;
-  if (reaction.match(/^mxc:\/\/\S+$/)) {
-    customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(reaction);
-  }
-  return (
-    <Tooltip
-      className="msg__reaction-tooltip"
-      content={<Text variant="b2">{users.length > 0 ? genReactionMsg(users, reaction, shortcode) : 'Unable to load who has reacted'}</Text>}
-    >
-      <button
-        onClick={onClick}
-        type="button"
-        className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
-      >
-        {
-          customEmojiUrl
-            ? <img className="react-emoji" draggable="false" alt={shortcode ?? reaction} src={customEmojiUrl} />
-            : twemojify(reaction, { className: 'react-emoji' })
-        }
-        <Text variant="b3" className="msg__reaction-count">{count}</Text>
-      </button>
-    </Tooltip>
-  );
-}
-MessageReaction.defaultProps = {
-  shortcode: undefined,
-};
-MessageReaction.propTypes = {
-  reaction: PropTypes.node.isRequired,
-  shortcode: PropTypes.string,
-  count: PropTypes.number.isRequired,
-  users: PropTypes.arrayOf(PropTypes.string).isRequired,
-  isActive: PropTypes.bool.isRequired,
-  onClick: PropTypes.func.isRequired,
-};
-
-function MessageReactionGroup({ roomTimeline, mEvent }) {
-  const { roomId, room, reactionTimeline } = roomTimeline;
-  const mx = initMatrix.matrixClient;
-  const reactions = {};
-  const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
-
-  const eventReactions = reactionTimeline.get(mEvent.getId());
-  const addReaction = (key, shortcode, count, senderId, isActive) => {
-    let reaction = reactions[key];
-    if (reaction === undefined) {
-      reaction = {
-        count: 0,
-        users: [],
-        isActive: false,
-      };
-    }
-    if (shortcode) reaction.shortcode = shortcode;
-    if (count) {
-      reaction.count = count;
-    } else {
-      reaction.users.push(senderId);
-      reaction.count = reaction.users.length;
-      if (isActive) reaction.isActive = isActive;
-    }
-
-    reactions[key] = reaction;
-  };
-  if (eventReactions) {
-    eventReactions.forEach((rEvent) => {
-      if (rEvent.getRelation() === null) return;
-      const reaction = rEvent.getRelation();
-      const senderId = rEvent.getSender();
-      const { shortcode } = rEvent.getContent();
-      const isActive = senderId === mx.getUserId();
-
-      addReaction(reaction.key, shortcode, undefined, senderId, isActive);
-    });
-  } else {
-    // Use aggregated reactions
-    const aggregatedReaction = mEvent.getServerAggregatedRelation('m.annotation')?.chunk;
-    if (!aggregatedReaction) return null;
-    aggregatedReaction.forEach((reaction) => {
-      if (reaction.type !== 'm.reaction') return;
-      addReaction(reaction.key, undefined, reaction.count, undefined, false);
-    });
-  }
-
-  return (
-    <div className="message__reactions text text-b3 noselect">
-      {
-        Object.keys(reactions).map((key) => (
-          <MessageReaction
-            key={key}
-            reaction={key}
-            shortcode={reactions[key].shortcode}
-            count={reactions[key].count}
-            users={reactions[key].users}
-            isActive={reactions[key].isActive}
-            onClick={() => {
-              toggleEmoji(roomId, mEvent.getId(), key, reactions[key].shortcode, roomTimeline);
-            }}
-          />
-        ))
-      }
-      {canSendReaction && (
-        <IconButton
-          onClick={(e) => {
-            pickEmoji(e, roomId, mEvent.getId(), roomTimeline);
-          }}
-          src={EmojiAddIC}
-          size="extra-small"
-          tooltip="Add reaction"
-        />
-      )}
-    </div>
-  );
-}
-MessageReactionGroup.propTypes = {
-  roomTimeline: PropTypes.shape({}).isRequired,
-  mEvent: PropTypes.shape({}).isRequired,
-};
-
-function isMedia(mE) {
-  return (
-    mE.getContent()?.msgtype === 'm.file'
-    || mE.getContent()?.msgtype === 'm.image'
-    || mE.getContent()?.msgtype === 'm.audio'
-    || mE.getContent()?.msgtype === 'm.video'
-    || mE.getType() === 'm.sticker'
-  );
-}
-
-// if editedTimeline has mEventId then pass editedMEvent else pass mEvent to openViewSource
-function handleOpenViewSource(mEvent, roomTimeline) {
-  const eventId = mEvent.getId();
-  const { editedTimeline } = roomTimeline ?? {};
-  let editedMEvent;
-  if (editedTimeline?.has(eventId)) {
-    const editedList = editedTimeline.get(eventId);
-    editedMEvent = editedList[editedList.length - 1];
-  }
-  openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent);
-}
-
-const MessageOptions = React.memo(({
-  roomTimeline, mEvent, edit, reply,
-}) => {
-  const { roomId, room } = roomTimeline;
-  const mx = initMatrix.matrixClient;
-  const senderId = mEvent.getSender();
-
-  const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel;
-  const canIRedact = room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
-  const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
-
-  return (
-    <div className="message__options">
-      {canSendReaction && (
-        <IconButton
-          onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
-          src={EmojiAddIC}
-          size="extra-small"
-          tooltip="Add reaction"
-        />
-      )}
-      <IconButton
-        onClick={() => reply()}
-        src={ReplyArrowIC}
-        size="extra-small"
-        tooltip="Reply"
-      />
-      {(senderId === mx.getUserId() && !isMedia(mEvent)) && (
-        <IconButton
-          onClick={() => edit(true)}
-          src={PencilIC}
-          size="extra-small"
-          tooltip="Edit"
-        />
-      )}
-      <ContextMenu
-        content={() => (
-          <>
-            <MenuHeader>Options</MenuHeader>
-            <MenuItem
-              iconSrc={TickMarkIC}
-              onClick={() => openReadReceipts(roomId, roomTimeline.getEventReaders(mEvent))}
-            >
-              Read receipts
-            </MenuItem>
-            <MenuItem
-              iconSrc={CmdIC}
-              onClick={() => handleOpenViewSource(mEvent, roomTimeline)}
-            >
-              View source
-            </MenuItem>
-            {(canIRedact || senderId === mx.getUserId()) && (
-              <>
-                <MenuBorder />
-                <MenuItem
-                  variant="danger"
-                  iconSrc={BinIC}
-                  onClick={async () => {
-                    const isConfirmed = await confirmDialog(
-                      'Delete message',
-                      'Are you sure that you want to delete this message?',
-                      'Delete',
-                      'danger',
-                    );
-                    if (!isConfirmed) return;
-                    redactEvent(roomId, mEvent.getId());
-                  }}
-                >
-                  Delete
-                </MenuItem>
-              </>
-            )}
-          </>
-        )}
-        render={(toggleMenu) => (
-          <IconButton
-            onClick={toggleMenu}
-            src={VerticalMenuIC}
-            size="extra-small"
-            tooltip="Options"
-          />
-        )}
-      />
-    </div>
-  );
-});
-MessageOptions.propTypes = {
-  roomTimeline: PropTypes.shape({}).isRequired,
-  mEvent: PropTypes.shape({}).isRequired,
-  edit: PropTypes.func.isRequired,
-  reply: PropTypes.func.isRequired,
-};
-
-function genMediaContent(mE) {
-  const mx = initMatrix.matrixClient;
-  const mContent = mE.getContent();
-  if (!mContent || !mContent.body) return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
-
-  let mediaMXC = mContent?.url;
-  const isEncryptedFile = typeof mediaMXC === 'undefined';
-  if (isEncryptedFile) mediaMXC = mContent?.file?.url;
-
-  let thumbnailMXC = mContent?.info?.thumbnail_url;
-
-  if (typeof mediaMXC === 'undefined' || mediaMXC === '') return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
-
-  let msgType = mE.getContent()?.msgtype;
-  const safeMimetype = getBlobSafeMimeType(mContent.info?.mimetype);
-  if (mE.getType() === 'm.sticker') {
-    msgType = 'm.sticker';
-  } else if (safeMimetype === 'application/octet-stream') {
-    msgType = 'm.file';
-  }
-
-  const blurhash = mContent?.info?.['xyz.amorgan.blurhash'];
-
-  switch (msgType) {
-    case 'm.file':
-      return (
-        <Media.File
-          name={mContent.body}
-          link={mx.mxcUrlToHttp(mediaMXC)}
-          type={mContent.info?.mimetype}
-          file={mContent.file || null}
-        />
-      );
-    case 'm.image':
-      return (
-        <Media.Image
-          name={mContent.body}
-          width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
-          height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
-          link={mx.mxcUrlToHttp(mediaMXC)}
-          file={isEncryptedFile ? mContent.file : null}
-          type={mContent.info?.mimetype}
-          blurhash={blurhash}
-        />
-      );
-    case 'm.sticker':
-      return (
-        <Media.Sticker
-          name={mContent.body}
-          width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
-          height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
-          link={mx.mxcUrlToHttp(mediaMXC)}
-          file={isEncryptedFile ? mContent.file : null}
-          type={mContent.info?.mimetype}
-        />
-      );
-    case 'm.audio':
-      return (
-        <Media.Audio
-          name={mContent.body}
-          link={mx.mxcUrlToHttp(mediaMXC)}
-          type={mContent.info?.mimetype}
-          file={mContent.file || null}
-        />
-      );
-    case 'm.video':
-      if (typeof thumbnailMXC === 'undefined') {
-        thumbnailMXC = mContent.info?.thumbnail_file?.url || null;
-      }
-      return (
-        <Media.Video
-          name={mContent.body}
-          link={mx.mxcUrlToHttp(mediaMXC)}
-          thumbnail={thumbnailMXC === null ? null : mx.mxcUrlToHttp(thumbnailMXC)}
-          thumbnailFile={isEncryptedFile ? mContent.info?.thumbnail_file : null}
-          thumbnailType={mContent.info?.thumbnail_info?.mimetype || null}
-          width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
-          height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
-          file={isEncryptedFile ? mContent.file : null}
-          type={mContent.info?.mimetype}
-          blurhash={blurhash}
-        />
-      );
-    default:
-      return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
-  }
-}
-
-function getEditedBody(editedMEvent) {
-  const newContent = editedMEvent.getContent()['m.new_content'];
-  if (typeof newContent === 'undefined') return [null, false, null];
-
-  const isCustomHTML = newContent.format === 'org.matrix.custom.html';
-  const parsedContent = parseReply(newContent.body);
-  if (parsedContent === null) {
-    return [newContent.body, isCustomHTML, newContent.formatted_body ?? null];
-  }
-  return [parsedContent.body, isCustomHTML, newContent.formatted_body ?? null];
-}
-
-function Message({
-  mEvent, isBodyOnly, roomTimeline,
-  focus, fullTime, isEdit, setEdit, cancelEdit,
-}) {
-  const roomId = mEvent.getRoomId();
-  const { editedTimeline, reactionTimeline } = roomTimeline ?? {};
-
-  const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')];
-  if (focus) className.push('message--focus');
-  const content = mEvent.getContent();
-  const eventId = mEvent.getId();
-  const msgType = content?.msgtype;
-  const senderId = mEvent.getSender();
-  let { body } = content;
-  const username = mEvent.sender ? getUsernameOfRoomMember(mEvent.sender) : getUsername(senderId);
-  const avatarSrc = mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
-  let isCustomHTML = content.format === 'org.matrix.custom.html';
-  let customHTML = isCustomHTML ? content.formatted_body : null;
-
-  const edit = useCallback(() => {
-    setEdit(eventId);
-  }, []);
-  const reply = useCallback(() => {
-    replyTo(senderId, mEvent.getId(), body, customHTML);
-  }, [body, customHTML]);
-
-  if (msgType === 'm.emote') className.push('message--type-emote');
-
-  const isEdited = roomTimeline ? editedTimeline.has(eventId) : false;
-  const haveReactions = roomTimeline
-    ? reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation')
-    : false;
-  const isReply = !!mEvent.replyEventId;
-
-  if (isEdited) {
-    const editedList = editedTimeline.get(eventId);
-    const editedMEvent = editedList[editedList.length - 1];
-    [body, isCustomHTML, customHTML] = getEditedBody(editedMEvent);
-  }
-
-  if (isReply) {
-    body = parseReply(body)?.body ?? body;
-    customHTML = trimHTMLReply(customHTML);
-  }
-
-  if (typeof body !== 'string') body = '';
-
-  return (
-    <div className={className.join(' ')}>
-      {
-        isBodyOnly
-          ? <div className="message__avatar-container" />
-          : (
-            <MessageAvatar
-              roomId={roomId}
-              avatarSrc={avatarSrc}
-              userId={senderId}
-              username={username}
-            />
-          )
-      }
-      <div className="message__main-container">
-        {!isBodyOnly && (
-          <MessageHeader
-            userId={senderId}
-            username={username}
-            timestamp={mEvent.getTs()}
-            fullTime={fullTime}
-          />
-        )}
-        {roomTimeline && isReply && (
-          <MessageReplyWrapper
-            roomTimeline={roomTimeline}
-            eventId={mEvent.replyEventId}
-          />
-        )}
-        {!isEdit && (
-          <MessageBody
-            senderName={username}
-            isCustomHTML={isCustomHTML}
-            body={isMedia(mEvent) ? genMediaContent(mEvent) : customHTML ?? body}
-            msgType={msgType}
-            isEdited={isEdited}
-          />
-        )}
-        {isEdit && (
-          <MessageEdit
-            body={(customHTML
-              ? html(customHTML, { kind: 'edit', onlyPlain: true }).plain
-              : plain(body, { kind: 'edit', onlyPlain: true }).plain)}
-            onSave={(newBody, oldBody) => {
-              if (newBody !== oldBody) {
-                initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
-              }
-              cancelEdit();
-            }}
-            onCancel={cancelEdit}
-          />
-        )}
-        {haveReactions && (
-          <MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
-        )}
-        {roomTimeline && !isEdit && (
-          <MessageOptions
-            roomTimeline={roomTimeline}
-            mEvent={mEvent}
-            edit={edit}
-            reply={reply}
-          />
-        )}
-      </div>
-    </div>
-  );
-}
-Message.defaultProps = {
-  isBodyOnly: false,
-  focus: false,
-  roomTimeline: null,
-  fullTime: false,
-  isEdit: false,
-  setEdit: null,
-  cancelEdit: null,
-};
-Message.propTypes = {
-  mEvent: PropTypes.shape({}).isRequired,
-  isBodyOnly: PropTypes.bool,
-  roomTimeline: PropTypes.shape({}),
-  focus: PropTypes.bool,
-  fullTime: PropTypes.bool,
-  isEdit: PropTypes.bool,
-  setEdit: PropTypes.func,
-  cancelEdit: PropTypes.func,
-};
-
-export { Message, MessageReply, PlaceholderMessage };
diff --git a/src/app/molecules/message/Message.scss b/src/app/molecules/message/Message.scss
deleted file mode 100644 (file)
index 5dda9c9..0000000
+++ /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 `<p>` 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 (file)
index bc6e913..0000000
+++ /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 (
-    <button style={{ cursor: onClick === null ? 'default' : 'pointer' }} onClick={onClick} type="button" className="timeline-change">
-      <div className="timeline-change__avatar-container">
-        <RawIcon src={iconSrc} size="extra-small" />
-      </div>
-      <div className="timeline-change__content">
-        <Text variant="b2">
-          {content}
-        </Text>
-      </div>
-      <div className="timeline-change__time">
-        <Text variant="b3">
-          <Time timestamp={timestamp} />
-        </Text>
-      </div>
-    </button>
-  );
-}
-
-TimelineChange.defaultProps = {
-  variant: 'other',
-  onClick: null,
-};
-
-TimelineChange.propTypes = {
-  variant: PropTypes.oneOf([
-    'join', 'leave', 'invite',
-    'invite-cancel', 'avatar', 'other',
-  ]),
-  content: PropTypes.oneOfType([
-    PropTypes.string,
-    PropTypes.node,
-  ]).isRequired,
-  timestamp: PropTypes.number.isRequired,
-  onClick: PropTypes.func,
-};
-
-export default TimelineChange;
diff --git a/src/app/molecules/message/TimelineChange.scss b/src/app/molecules/message/TimelineChange.scss
deleted file mode 100644 (file)
index c066a9a..0000000
+++ /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
index 8ea0587f5639e2ca6c10e110c093f3597a17d00a..7025aa7c4eb97b3b9620fefcbbe31c72d61975f5 100644 (file)
@@ -2,16 +2,12 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import './PeopleSelector.scss';
 
-import { twemojify } from '../../../util/twemojify';
-
 import { blurOnBubbling } from '../../atoms/button/script';
 
 import Text from '../../atoms/text/Text';
 import Avatar from '../../atoms/avatar/Avatar';
 
-function PeopleSelector({
-  avatarSrc, name, color, peopleRole, onClick,
-}) {
+function PeopleSelector({ avatarSrc, name, color, peopleRole, onClick }) {
   return (
     <div className="people-selector__container">
       <button
@@ -21,8 +17,14 @@ function PeopleSelector({
         type="button"
       >
         <Avatar imageSrc={avatarSrc} text={name} bgColor={color} size="extra-small" />
-        <Text className="people-selector__name" variant="b1">{twemojify(name)}</Text>
-        {peopleRole !== null && <Text className="people-selector__role" variant="b3">{peopleRole}</Text>}
+        <Text className="people-selector__name" variant="b1">
+          {name}
+        </Text>
+        {peopleRole !== null && (
+          <Text className="people-selector__role" variant="b3">
+            {peopleRole}
+          </Text>
+        )}
       </button>
     </div>
   );
index 4179f49a892fa86894e0c372e8765f6c558fb427..55872d6aee7fdea7887d3e66593f4b2c8f407ad3 100644 (file)
@@ -2,8 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import './PopupWindow.scss';
 
-import { twemojify } from '../../../util/twemojify';
-
 import Text from '../../atoms/text/Text';
 import IconButton from '../../atoms/button/IconButton';
 import { MenuItem } from '../../atoms/context-menu/ContextMenu';
@@ -13,19 +11,11 @@ import RawModal from '../../atoms/modal/RawModal';
 
 import ChevronLeftIC from '../../../../public/res/ic/outlined/chevron-left.svg';
 
-function PWContentSelector({
-  selected, variant, iconSrc,
-  type, onClick, children,
-}) {
+function PWContentSelector({ selected, variant, iconSrc, type, onClick, children }) {
   const pwcsClass = selected ? ' pw-content-selector--selected' : '';
   return (
     <div className={`pw-content-selector${pwcsClass}`}>
-      <MenuItem
-        variant={variant}
-        iconSrc={iconSrc}
-        type={type}
-        onClick={onClick}
-      >
+      <MenuItem variant={variant} iconSrc={iconSrc} type={type} onClick={onClick}>
         {children}
       </MenuItem>
     </div>
@@ -49,9 +39,16 @@ PWContentSelector.propTypes = {
 };
 
 function PopupWindow({
-  className, isOpen, title, contentTitle,
-  drawer, drawerOptions, contentOptions,
-  onAfterClose, onRequestClose, children,
+  className,
+  isOpen,
+  title,
+  contentTitle,
+  drawer,
+  drawerOptions,
+  contentOptions,
+  onAfterClose,
+  onRequestClose,
+  children,
 }) {
   const haveDrawer = drawer !== null;
   const cTitle = contentTitle !== null ? contentTitle : title;
@@ -69,21 +66,26 @@ function PopupWindow({
         {haveDrawer && (
           <div className="pw__drawer">
             <Header>
-              <IconButton size="small" src={ChevronLeftIC} onClick={onRequestClose} tooltip="Back" />
+              <IconButton
+                size="small"
+                src={ChevronLeftIC}
+                onClick={onRequestClose}
+                tooltip="Back"
+              />
               <TitleWrapper>
-                {
-                  typeof title === 'string'
-                    ? <Text variant="s1" weight="medium" primary>{twemojify(title)}</Text>
-                    : title
-                }
+                {typeof title === 'string' ? (
+                  <Text variant="s1" weight="medium" primary>
+                    {title}
+                  </Text>
+                ) : (
+                  title
+                )}
               </TitleWrapper>
               {drawerOptions}
             </Header>
             <div className="pw__drawer__content__wrapper">
               <ScrollView invisible>
-                <div className="pw__drawer__content">
-                  {drawer}
-                </div>
+                <div className="pw__drawer__content">{drawer}</div>
               </ScrollView>
             </div>
           </div>
@@ -91,19 +93,19 @@ function PopupWindow({
         <div className="pw__content">
           <Header>
             <TitleWrapper>
-              {
-                typeof cTitle === 'string'
-                  ? <Text variant="h2" weight="medium" primary>{twemojify(cTitle)}</Text>
-                  : cTitle
-              }
+              {typeof cTitle === 'string' ? (
+                <Text variant="h2" weight="medium" primary>
+                  {cTitle}
+                </Text>
+              ) : (
+                cTitle
+              )}
             </TitleWrapper>
             {contentOptions}
           </Header>
           <div className="pw__content__wrapper">
             <ScrollView autoHide>
-              <div className="pw__content-container">
-                {children}
-              </div>
+              <div className="pw__content-container">{children}</div>
             </ScrollView>
           </div>
         </div>
index 4adb1169e5ff6c74d1acccd703c61f8438ec5048..821ea50845e03b2968fc802113bcde6941798157 100644 (file)
@@ -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 (
     <div className="room-notification">
-      {
-        items.map((item) => (
-          <MenuItem
-            variant={activeType === item.type ? 'positive' : 'surface'}
-            key={item.type}
-            iconSrc={item.iconSrc}
-            onClick={() => setNotification(item)}
-          >
-            <Text varient="b1">
-              <span>{item.text}</span>
-              <RadioButton isActive={activeType === item.type} />
-            </Text>
-          </MenuItem>
-        ))
-      }
+      {items.map((item) => (
+        <MenuItem
+          variant={activeType === item.type ? 'positive' : 'surface'}
+          key={item.type}
+          iconSrc={item.iconSrc}
+          onClick={() => setNotification(item)}
+        >
+          <Text varient="b1">
+            <span>{item.text}</span>
+            <RadioButton isActive={activeType === item.type} />
+          </Text>
+        </MenuItem>
+      ))}
     </div>
   );
 }
diff --git a/src/app/molecules/room-options/RoomOptions.jsx b/src/app/molecules/room-options/RoomOptions.jsx
deleted file mode 100644 (file)
index af18d71..0000000
+++ /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 (
-    <div style={{ maxWidth: '256px' }}>
-      <MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
-      <MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
-      <MenuItem
-        iconSrc={AddUserIC}
-        onClick={handleInviteClick}
-        disabled={!canInvite}
-      >
-        Invite
-      </MenuItem>
-      <MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={handleLeaveClick}>Leave</MenuItem>
-      <MenuHeader>Notification</MenuHeader>
-      <RoomNotification roomId={roomId} />
-    </div>
-  );
-}
-
-RoomOptions.defaultProps = {
-  afterOptionSelect: null,
-};
-
-RoomOptions.propTypes = {
-  roomId: PropTypes.string.isRequired,
-  afterOptionSelect: PropTypes.func,
-};
-
-export default RoomOptions;
index 218119845e56b01bf2b416019022c8fef1a0e8c2..15273ebfd553a9174db19c9ec7316b752b99fb2b 100644 (file)
@@ -1,9 +1,9 @@
 import React, { useState, useEffect } from 'react';
 import PropTypes from 'prop-types';
+import { useAtomValue } from 'jotai';
+import Linkify from 'linkify-react';
 import './RoomProfile.scss';
 
-import { twemojify } from '../../../util/twemojify';
-
 import initMatrix from '../../../client/initMatrix';
 import cons from '../../../client/state/cons';
 import colorMXID from '../../../util/colorMXID';
@@ -20,6 +20,8 @@ import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
 import { useStore } from '../../hooks/useStore';
 import { useForceUpdate } from '../../hooks/useForceUpdate';
 import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
+import { mDirectAtom } from '../../state/mDirectList';
+import { LINKIFY_OPTS } from '../../plugins/react-custom-html-parser';
 
 function RoomProfile({ roomId }) {
   const isMountStore = useStore();
@@ -31,9 +33,12 @@ function RoomProfile({ roomId }) {
   });
 
   const mx = initMatrix.matrixClient;
-  const isDM = initMatrix.roomList.directs.has(roomId);
+  const mDirects = useAtomValue(mDirectAtom);
+  const isDM = mDirects.has(roomId);
   let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
-  avatarSrc = isDM ? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc;
+  avatarSrc = isDM
+    ? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop')
+    : avatarSrc;
   const room = mx.getRoom(roomId);
   const { currentState } = room;
   const roomName = room.name;
@@ -47,15 +52,14 @@ function RoomProfile({ roomId }) {
 
   useEffect(() => {
     isMountStore.setItem(true);
-    const { roomList } = initMatrix;
-    const handleProfileUpdate = (rId) => {
-      if (roomId !== rId) return;
+    const handleStateEvent = (mEvent) => {
+      if (mEvent.event.room_id !== roomId) return;
       forceUpdate();
     };
 
-    roomList.on(cons.events.roomList.ROOM_PROFILE_UPDATED, handleProfileUpdate);
+    mx.on('RoomState.events', handleStateEvent);
     return () => {
-      roomList.removeListener(cons.events.roomList.ROOM_PROFILE_UPDATED, handleProfileUpdate);
+      mx.removeListener('RoomState.events', handleStateEvent);
       isMountStore.setItem(false);
       setStatus({
         msg: null,
@@ -122,7 +126,7 @@ function RoomProfile({ roomId }) {
         'Remove avatar',
         'Are you sure that you want to remove room avatar?',
         'Remove',
-        'caution',
+        'caution'
       );
       if (isConfirmed) {
         await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
@@ -132,15 +136,45 @@ function RoomProfile({ roomId }) {
 
   const renderEditNameAndTopic = () => (
     <form className="room-profile__edit-form" onSubmit={handleOnSubmit}>
-      {canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label="Name" />}
-      {canChangeTopic && <Input value={roomTopic} name="room-topic" disabled={status.type === cons.status.IN_FLIGHT} minHeight={100} resizable label="Topic" />}
-      {(!canChangeName || !canChangeTopic) && <Text variant="b3">{`You have permission to change ${room.isSpaceRoom() ? 'space' : 'room'} ${canChangeName ? 'name' : 'topic'} only.`}</Text>}
-      { status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
-      { status.type === cons.status.SUCCESS && <Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">{status.msg}</Text>}
-      { status.type === cons.status.ERROR && <Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">{status.msg}</Text>}
-      { status.type !== cons.status.IN_FLIGHT && (
+      {canChangeName && (
+        <Input
+          value={roomName}
+          name="room-name"
+          disabled={status.type === cons.status.IN_FLIGHT}
+          label="Name"
+        />
+      )}
+      {canChangeTopic && (
+        <Input
+          value={roomTopic}
+          name="room-topic"
+          disabled={status.type === cons.status.IN_FLIGHT}
+          minHeight={100}
+          resizable
+          label="Topic"
+        />
+      )}
+      {(!canChangeName || !canChangeTopic) && (
+        <Text variant="b3">{`You have permission to change ${
+          room.isSpaceRoom() ? 'space' : 'room'
+        } ${canChangeName ? 'name' : 'topic'} only.`}</Text>
+      )}
+      {status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
+      {status.type === cons.status.SUCCESS && (
+        <Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">
+          {status.msg}
+        </Text>
+      )}
+      {status.type === cons.status.ERROR && (
+        <Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">
+          {status.msg}
+        </Text>
+      )}
+      {status.type !== cons.status.IN_FLIGHT && (
         <div>
-          <Button type="submit" variant="primary">Save</Button>
+          <Button type="submit" variant="primary">
+            Save
+          </Button>
           <Button onClick={handleCancelEditing}>Cancel</Button>
         </div>
       )}
@@ -148,10 +182,15 @@ function RoomProfile({ roomId }) {
   );
 
   const renderNameAndTopic = () => (
-    <div className="room-profile__display" style={{ marginBottom: avatarSrc && canChangeAvatar ? '24px' : '0' }}>
+    <div
+      className="room-profile__display"
+      style={{ marginBottom: avatarSrc && canChangeAvatar ? '24px' : '0' }}
+    >
       <div>
-        <Text variant="h2" weight="medium" primary>{twemojify(roomName)}</Text>
-        { (canChangeName || canChangeTopic) && (
+        <Text variant="h2" weight="medium" primary>
+          {roomName}
+        </Text>
+        {(canChangeName || canChangeTopic) && (
           <IconButton
             src={PencilIC}
             size="extra-small"
@@ -161,15 +200,21 @@ function RoomProfile({ roomId }) {
         )}
       </div>
       <Text variant="b3">{room.getCanonicalAlias() || room.roomId}</Text>
-      {roomTopic && <Text variant="b2">{twemojify(roomTopic, undefined, true)}</Text>}
+      {roomTopic && (
+        <Text variant="b2">
+          <Linkify options={LINKIFY_OPTS}>{roomTopic}</Linkify>
+        </Text>
+      )}
     </div>
   );
 
   return (
     <div className="room-profile">
       <div className="room-profile__content">
-        { !canChangeAvatar && <Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="large" />}
-        { canChangeAvatar && (
+        {!canChangeAvatar && (
+          <Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="large" />
+        )}
+        {canChangeAvatar && (
           <ImageUpload
             text={roomName}
             bgColor={colorMXID(roomId)}
diff --git a/src/app/molecules/room-search/RoomSearch.jsx b/src/app/molecules/room-search/RoomSearch.jsx
deleted file mode 100644 (file)
index 6009649..0000000
+++ /dev/null
@@ -1,201 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import PropTypes from 'prop-types';
-import './RoomSearch.scss';
-
-import initMatrix from '../../../client/initMatrix';
-import cons from '../../../client/state/cons';
-import { selectRoom } from '../../../client/action/navigation';
-
-import Text from '../../atoms/text/Text';
-import RawIcon from '../../atoms/system-icons/RawIcon';
-import Button from '../../atoms/button/Button';
-import Input from '../../atoms/input/Input';
-import Spinner from '../../atoms/spinner/Spinner';
-import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
-import { Message } from '../message/Message';
-
-import SearchIC from '../../../../public/res/ic/outlined/search.svg';
-
-import { useStore } from '../../hooks/useStore';
-
-const roomIdToBackup = new Map();
-
-function useRoomSearch(roomId) {
-  const [searchData, setSearchData] = useState(roomIdToBackup.get(roomId) ?? null);
-  const [status, setStatus] = useState({
-    type: cons.status.PRE_FLIGHT,
-    term: null,
-  });
-  const mountStore = useStore(roomId);
-  const mx = initMatrix.matrixClient;
-
-  useEffect(() => {
-    mountStore.setItem(true)
-  }, [roomId]);
-
-  useEffect(() => {
-    if (searchData?.results?.length > 0) {
-      roomIdToBackup.set(roomId, searchData);
-    } else {
-      roomIdToBackup.delete(roomId);
-    }
-  }, [searchData]);
-
-  const search = async (term) => {
-    setSearchData(null);
-    if (term === '') {
-      setStatus({ type: cons.status.PRE_FLIGHT, term: null });
-      return;
-    }
-    setStatus({ type: cons.status.IN_FLIGHT, term });
-    const body = {
-      search_categories: {
-        room_events: {
-          search_term: term,
-          filter: {
-            limit: 10,
-            rooms: [roomId],
-          },
-          order_by: 'recent',
-          event_context: {
-            before_limit: 0,
-            after_limit: 0,
-            include_profile: true,
-          },
-        },
-      },
-    };
-    try {
-      const res = await mx.search({ body });
-      const data = mx.processRoomEventsSearch({
-        _query: body,
-        results: [],
-        highlights: [],
-      }, res);
-      if (!mountStore.getItem()) return;
-      setStatus({ type: cons.status.SUCCESS, term });
-      setSearchData(data);
-      if (!mountStore.getItem()) return;
-    } catch (error) {
-      setSearchData(null);
-      setStatus({ type: cons.status.ERROR, term });
-    }
-  };
-
-  const paginate = async () => {
-    if (searchData === null) return;
-    const term = searchData._query.search_categories.room_events.search_term;
-
-    setStatus({ type: cons.status.IN_FLIGHT, term });
-    try {
-      const data = await mx.backPaginateRoomEventsSearch(searchData);
-      if (!mountStore.getItem()) return;
-      setStatus({ type: cons.status.SUCCESS, term });
-      setSearchData(data);
-    } catch (error) {
-      if (!mountStore.getItem()) return;
-      setSearchData(null);
-      setStatus({ type: cons.status.ERROR, term });
-    }
-  };
-
-  return [searchData, search, paginate, status];
-}
-
-function RoomSearch({ roomId }) {
-  const [searchData, search, paginate, status] = useRoomSearch(roomId);
-  const mx = initMatrix.matrixClient;
-  const isRoomEncrypted = mx.isRoomEncrypted(roomId);
-  const searchTerm = searchData?._query.search_categories.room_events.search_term ?? '';
-
-  const handleSearch = (e) => {
-    e.preventDefault();
-    if (isRoomEncrypted) return;
-    const searchTermInput = e.target.elements['room-search-input'];
-    const term = searchTermInput.value.trim();
-
-    search(term);
-  };
-
-  const renderTimeline = (timeline) => (
-    <div className="room-search__result-item" key={timeline[0].getId()}>
-      { timeline.map((mEvent) => {
-        const id = mEvent.getId();
-        return (
-          <React.Fragment key={id}>
-            <Message
-              mEvent={mEvent}
-              isBodyOnly={false}
-              fullTime
-            />
-            <Button onClick={() => selectRoom(roomId, id)}>View</Button>
-          </React.Fragment>
-        );
-      })}
-    </div>
-  );
-
-  return (
-    <div className="room-search">
-      <form className="room-search__form" onSubmit={handleSearch}>
-        <MenuHeader>Room search</MenuHeader>
-        <div>
-          <Input
-            placeholder="Search for keywords"
-            name="room-search-input"
-            disabled={isRoomEncrypted}
-            autoFocus
-          />
-          <Button iconSrc={SearchIC} variant="primary" type="submit">Search</Button>
-        </div>
-        {searchData?.results.length > 0 && (
-          <Text>{`${searchData.count} results for "${searchTerm}"`}</Text>
-        )}
-        {!isRoomEncrypted && searchData === null && (
-          <div className="room-search__help">
-            {status.type === cons.status.IN_FLIGHT && <Spinner />}
-            {status.type === cons.status.IN_FLIGHT && <Text>Searching room messages...</Text>}
-            {status.type === cons.status.PRE_FLIGHT && <RawIcon src={SearchIC} size="large" />}
-            {status.type === cons.status.PRE_FLIGHT && <Text>Search room messages</Text>}
-            {status.type === cons.status.ERROR && <Text>Failed to search messages</Text>}
-          </div>
-        )}
-
-        {!isRoomEncrypted && searchData?.results.length === 0 && (
-          <div className="room-search__help">
-            <Text>No results found</Text>
-          </div>
-        )}
-        {isRoomEncrypted && (
-          <div className="room-search__help">
-            <Text>Search does not work in encrypted room</Text>
-          </div>
-        )}
-      </form>
-      {searchData?.results.length > 0 && (
-        <>
-          <div className="room-search__content">
-            {searchData.results.map((searchResult) => {
-              const { timeline } = searchResult.context;
-              return renderTimeline(timeline);
-            })}
-          </div>
-          {searchData?.next_batch && (
-            <div className="room-search__more">
-              {status.type !== cons.status.IN_FLIGHT && (
-                <Button onClick={paginate}>Load more</Button>
-              )}
-              {status.type === cons.status.IN_FLIGHT && <Spinner />}
-            </div>
-          )}
-        </>
-      )}
-    </div>
-  );
-}
-
-RoomSearch.propTypes = {
-  roomId: PropTypes.string.isRequired,
-};
-
-export default RoomSearch;
diff --git a/src/app/molecules/room-search/RoomSearch.scss b/src/app/molecules/room-search/RoomSearch.scss
deleted file mode 100644 (file)
index a40945e..0000000
+++ /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
index fa6daa9e91ff53cb20c7005225c92bbda0617174..f865c95d02d34be36e0eebff24c24cd0a0f47026 100644 (file)
@@ -2,7 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import './RoomSelector.scss';
 
-import { twemojify } from '../../../util/twemojify';
 import colorMXID from '../../../util/colorMXID';
 
 import Text from '../../atoms/text/Text';
@@ -11,8 +10,13 @@ import NotificationBadge from '../../atoms/badge/NotificationBadge';
 import { blurOnBubbling } from '../../atoms/button/script';
 
 function RoomSelectorWrapper({
-  isSelected, isMuted, isUnread, onClick,
-  content, options, onContextMenu,
+  isSelected,
+  isMuted,
+  isUnread,
+  onClick,
+  content,
+  options,
+  onContextMenu,
 }) {
   const classes = ['room-selector'];
   if (isMuted) classes.push('room-selector--muted');
@@ -50,16 +54,26 @@ RoomSelectorWrapper.propTypes = {
 };
 
 function RoomSelector({
-  name, parentName, roomId, imageSrc, iconSrc,
-  isSelected, isMuted, isUnread, notificationCount, isAlert,
-  options, onClick, onContextMenu,
+  name,
+  parentName,
+  roomId,
+  imageSrc,
+  iconSrc,
+  isSelected,
+  isMuted,
+  isUnread,
+  notificationCount,
+  isAlert,
+  options,
+  onClick,
+  onContextMenu,
 }) {
   return (
     <RoomSelectorWrapper
       isSelected={isSelected}
       isMuted={isMuted}
       isUnread={isUnread}
-      content={(
+      content={
         <>
           <Avatar
             text={name}
@@ -70,22 +84,22 @@ function RoomSelector({
             size="extra-small"
           />
           <Text variant="b1" weight={isUnread ? 'medium' : 'normal'}>
-            {twemojify(name)}
+            {name}
             {parentName && (
               <Text variant="b3" span>
                 {' — '}
-                {twemojify(parentName)}
+                {parentName}
               </Text>
             )}
           </Text>
-          { isUnread && (
+          {isUnread && (
             <NotificationBadge
               alert={isAlert}
               content={notificationCount !== 0 ? notificationCount : null}
             />
           )}
         </>
-      )}
+      }
       options={options}
       onClick={onClick}
       onContextMenu={onContextMenu}
@@ -110,10 +124,7 @@ RoomSelector.propTypes = {
   isSelected: PropTypes.bool,
   isMuted: PropTypes.bool,
   isUnread: PropTypes.bool.isRequired,
-  notificationCount: PropTypes.oneOfType([
-    PropTypes.string,
-    PropTypes.number,
-  ]).isRequired,
+  notificationCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
   isAlert: PropTypes.bool.isRequired,
   options: PropTypes.node,
   onClick: PropTypes.func.isRequired,
index 95481ecec9aaf2c3d03a7fe5f3999b813d39ea8b..2e0a63f6080f8334d75731534c5608073527ed2e 100644 (file)
@@ -2,46 +2,35 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import './RoomTile.scss';
 
-import { twemojify } from '../../../util/twemojify';
-
 import colorMXID from '../../../util/colorMXID';
 
 import Text from '../../atoms/text/Text';
 import Avatar from '../../atoms/avatar/Avatar';
 
-function RoomTile({
-  avatarSrc, name, id,
-  inviterName, memberCount, desc, options,
-}) {
+function RoomTile({ avatarSrc, name, id, inviterName, memberCount, desc, options }) {
   return (
     <div className="room-tile">
       <div className="room-tile__avatar">
-        <Avatar
-          imageSrc={avatarSrc}
-          bgColor={colorMXID(id)}
-          text={name}
-        />
+        <Avatar imageSrc={avatarSrc} bgColor={colorMXID(id)} text={name} />
       </div>
       <div className="room-tile__content">
-        <Text variant="s1">{twemojify(name)}</Text>
+        <Text variant="s1">{name}</Text>
         <Text variant="b3">
-          {
-            inviterName !== null
-              ? `Invited by ${inviterName} to ${id}${memberCount === null ? '' : ` • ${memberCount} members`}`
-              : id + (memberCount === null ? '' : ` • ${memberCount} members`)
-          }
+          {inviterName !== null
+            ? `Invited by ${inviterName} to ${id}${
+                memberCount === null ? '' : ` • ${memberCount} members`
+              }`
+            : id + (memberCount === null ? '' : ` • ${memberCount} members`)}
         </Text>
-        {
-          desc !== null && (typeof desc === 'string')
-            ? <Text className="room-tile__content__desc" variant="b2">{twemojify(desc, undefined, true)}</Text>
-            : desc
-        }
+        {desc !== null && typeof desc === 'string' ? (
+          <Text className="room-tile__content__desc" variant="b2">
+            {desc}
+          </Text>
+        ) : (
+          desc
+        )}
       </div>
-      { options !== null && (
-        <div className="room-tile__options">
-          {options}
-        </div>
-      )}
+      {options !== null && <div className="room-tile__options">{options}</div>}
     </div>
   );
 }
@@ -58,10 +47,7 @@ RoomTile.propTypes = {
   name: PropTypes.string.isRequired,
   id: PropTypes.string.isRequired,
   inviterName: PropTypes.string,
-  memberCount: PropTypes.oneOfType([
-    PropTypes.string,
-    PropTypes.number,
-  ]),
+  memberCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
   desc: PropTypes.node,
   options: PropTypes.node,
 };
diff --git a/src/app/molecules/sidebar-avatar/SidebarAvatar.jsx b/src/app/molecules/sidebar-avatar/SidebarAvatar.jsx
deleted file mode 100644 (file)
index bc8b7c8..0000000
+++ /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 (
-    <Tooltip
-      content={<Text variant="b1">{twemojify(tooltip)}</Text>}
-      placement="right"
-    >
-      <button
-        ref={ref}
-        className={classes.join(' ')}
-        type="button"
-        onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
-        onClick={onClick}
-        onContextMenu={onContextMenu}
-      >
-        {avatar}
-        {notificationBadge}
-      </button>
-    </Tooltip>
-  );
-});
-SidebarAvatar.defaultProps = {
-  className: null,
-  active: false,
-  onClick: null,
-  onContextMenu: null,
-  notificationBadge: null,
-};
-
-SidebarAvatar.propTypes = {
-  className: PropTypes.string,
-  tooltip: PropTypes.string.isRequired,
-  active: PropTypes.bool,
-  onClick: PropTypes.func,
-  onContextMenu: PropTypes.func,
-  avatar: PropTypes.node.isRequired,
-  notificationBadge: PropTypes.node,
-};
-
-export default SidebarAvatar;
diff --git a/src/app/molecules/sidebar-avatar/SidebarAvatar.scss b/src/app/molecules/sidebar-avatar/SidebarAvatar.scss
deleted file mode 100644 (file)
index d76dbc8..0000000
+++ /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
index 547c0af5b99fbbe657f63e53acd0bf8edfd11799..3895ac7568b3bde07b821774e97a0bc47600567f 100644 (file)
@@ -1,9 +1,8 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, { useState, useEffect } from 'react';
 import PropTypes from 'prop-types';
+import { useAtomValue } from 'jotai';
 import './SpaceAddExisting.scss';
 
-import { twemojify } from '../../../util/twemojify';
-
 import initMatrix from '../../../client/initMatrix';
 import cons from '../../../client/state/cons';
 import navigation from '../../../client/state/navigation';
@@ -24,6 +23,9 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 import SearchIC from '../../../../public/res/ic/outlined/search.svg';
 
 import { useStore } from '../../hooks/useStore';
+import { roomToParentsAtom } from '../../state/room/roomToParents';
+import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList';
+import { allRoomsAtom } from '../../state/room-list/roomList';
 
 function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
   const mountStore = useStore(roomId);
@@ -33,7 +35,10 @@ function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
   const [selected, setSelected] = useState([]);
   const [searchIds, setSearchIds] = useState(null);
   const mx = initMatrix.matrixClient;
-  const { spaces, rooms, directs, roomIdToParents } = initMatrix.roomList;
+  const roomIdToParents = useAtomValue(roomToParentsAtom);
+  const spaces = useSpaces(mx, allRoomsAtom);
+  const rooms = useRooms(mx, allRoomsAtom);
+  const directs = useDirects(mx, allRoomsAtom);
 
   useEffect(() => {
     const roomIds = onlySpaces ? [...spaces] : [...rooms, ...directs];
@@ -217,7 +222,7 @@ function SpaceAddExisting() {
       className="space-add-existing"
       title={
         <Text variant="s1" weight="medium" primary>
-          {room && twemojify(room.name)}
+          {room && room.name}
           <span style={{ color: 'var(--tc-surface-low)' }}>
             {' '}
             — add existing {data?.spaces ? 'spaces' : 'rooms'}
diff --git a/src/app/molecules/space-options/SpaceOptions.jsx b/src/app/molecules/space-options/SpaceOptions.jsx
deleted file mode 100644 (file)
index 0c166c6..0000000
+++ /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 (
-    <div style={{ maxWidth: 'calc(var(--navigation-drawer-width) - var(--sp-normal))' }}>
-      <MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
-      <MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
-      <MenuItem
-        onClick={handleCategorizeClick}
-        iconSrc={isCategorized ? CategoryFilledIC : CategoryIC}
-      >
-        {isCategorized ? 'Uncategorize subspaces' : 'Categorize subspaces'}
-      </MenuItem>
-      <MenuItem
-        onClick={handlePinClick}
-        iconSrc={isPinned ? PinFilledIC : PinIC}
-      >
-        {isPinned ? 'Unpin from sidebar' : 'Pin to sidebar'}
-      </MenuItem>
-      <MenuItem
-        iconSrc={AddUserIC}
-        onClick={handleInviteClick}
-        disabled={!canInvite}
-      >
-        Invite
-      </MenuItem>
-      <MenuItem onClick={handleManageRoom} iconSrc={HashSearchIC}>Manage rooms</MenuItem>
-      <MenuItem onClick={handleSettingsClick} iconSrc={SettingsIC}>Settings</MenuItem>
-      <MenuItem
-        variant="danger"
-        onClick={handleLeaveClick}
-        iconSrc={LeaveArrowIC}
-      >
-        Leave
-      </MenuItem>
-    </div>
-  );
-}
-
-SpaceOptions.defaultProps = {
-  afterOptionSelect: null,
-};
-
-SpaceOptions.propTypes = {
-  roomId: PropTypes.string.isRequired,
-  afterOptionSelect: PropTypes.func,
-};
-
-export default SpaceOptions;
diff --git a/src/app/molecules/sso-buttons/SSOButtons.jsx b/src/app/molecules/sso-buttons/SSOButtons.jsx
deleted file mode 100644 (file)
index 0a653be..0000000
+++ /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 (
-    <div className="sso-buttons">
-      {identityProviders
-        .sort((idp, idp2) => {
-          if (typeof idp.icon !== 'string') return -1;
-          return idp.name.toLowerCase() > idp2.name.toLowerCase() ? 1 : -1;
-        })
-        .map((idp) => (
-          idp.icon
-            ? (
-              <button key={idp.id} type="button" className="sso-btn" onClick={() => handleClick(idp.id)}>
-                <img className="sso-btn__img" src={tempClient.mxcUrlToHttp(idp.icon)} alt={idp.name} />
-              </button>
-            ) : <Button key={idp.id} className="sso-btn__text-only" onClick={() => handleClick(idp.id)}>{`Login with ${idp.name}`}</Button>
-        ))}
-    </div>
-  );
-}
-
-SSOButtons.propTypes = {
-  identityProviders: PropTypes.arrayOf(
-    PropTypes.shape({}),
-  ).isRequired,
-  baseUrl: PropTypes.string.isRequired,
-  type: PropTypes.oneOf(['sso', 'cas']).isRequired,
-};
-
-export default SSOButtons;
diff --git a/src/app/molecules/sso-buttons/SSOButtons.scss b/src/app/molecules/sso-buttons/SSOButtons.scss
deleted file mode 100644 (file)
index 0650670..0000000
+++ /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
index 15be02d29cd67f7373d2e70b53ce17b8064cf9f1..ff00cca19ec4587204cf7cc146c696472e104e80 100644 (file)
@@ -2,11 +2,10 @@ import React, { useState, useEffect, useRef } from 'react';
 import PropTypes from 'prop-types';
 import './CreateRoom.scss';
 
-import { twemojify } from '../../../util/twemojify';
 import initMatrix from '../../../client/initMatrix';
 import cons from '../../../client/state/cons';
 import navigation from '../../../client/state/navigation';
-import { selectRoom, openReusableContextMenu } from '../../../client/action/navigation';
+import { openReusableContextMenu } from '../../../client/action/navigation';
 import * as roomActions from '../../../client/action/room';
 import { isRoomAliasAvailable, getIdServer } from '../../../util/matrixUtil';
 import { getEventCords } from '../../../util/common';
@@ -32,12 +31,14 @@ import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg';
 import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg';
 import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
 import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
+import { useRoomNavigate } from '../../hooks/useRoomNavigate';
 
 function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
   const [joinRule, setJoinRule] = useState(parentId ? 'restricted' : 'invite');
   const [isEncrypted, setIsEncrypted] = useState(true);
   const [isCreatingRoom, setIsCreatingRoom] = useState(false);
   const [creatingError, setCreatingError] = useState(null);
+  const { navigateRoom, navigateSpace } = useRoomNavigate();
 
   const [isValidAddress, setIsValidAddress] = useState(null);
   const [addressValue, setAddressValue] = useState(undefined);
@@ -48,25 +49,6 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
   const mx = initMatrix.matrixClient;
   const userHs = getIdServer(mx.getUserId());
 
-  useEffect(() => {
-    const { roomList } = initMatrix;
-    const onCreated = (roomId) => {
-      setIsCreatingRoom(false);
-      setCreatingError(null);
-      setIsValidAddress(null);
-      setAddressValue(undefined);
-
-      if (!mx.getRoom(roomId)?.isSpaceRoom()) {
-        selectRoom(roomId);
-      }
-      onRequestClose();
-    };
-    roomList.on(cons.events.roomList.ROOM_CREATED, onCreated);
-    return () => {
-      roomList.removeListener(cons.events.roomList.ROOM_CREATED, onCreated);
-    };
-  }, []);
-
   const handleSubmit = async (evt) => {
     evt.preventDefault();
     const { target } = evt;
@@ -87,16 +69,26 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
     const powerLevel = roleIndex === 1 ? 101 : undefined;
 
     try {
-      await roomActions.createRoom({
+      const data = await roomActions.createRoom({
         name,
         topic,
         joinRule,
         alias: roomAlias,
-        isEncrypted: (isSpace || joinRule === 'public') ? false : isEncrypted,
+        isEncrypted: isSpace || joinRule === 'public' ? false : isEncrypted,
         powerLevel,
         isSpace,
         parentId,
       });
+      setIsCreatingRoom(false);
+      setCreatingError(null);
+      setIsValidAddress(null);
+      setAddressValue(undefined);
+      onRequestClose();
+      if (isSpace) {
+        navigateSpace(data.room_id);
+      } else {
+        navigateRoom(data.room_id);
+      }
     } catch (e) {
       if (e.message === 'M_UNKNOWN: Invalid characters in room alias') {
         setCreatingError('ERROR: Invalid characters in address');
@@ -131,36 +123,35 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
 
   const joinRules = ['invite', 'restricted', 'public'];
   const joinRuleShortText = ['Private', 'Restricted', 'Public'];
-  const joinRuleText = ['Private (invite only)', 'Restricted (space member can join)', 'Public (anyone can join)'];
+  const joinRuleText = [
+    'Private (invite only)',
+    'Restricted (space member can join)',
+    'Public (anyone can join)',
+  ];
   const jrRoomIC = [HashLockIC, HashIC, HashGlobeIC];
   const jrSpaceIC = [SpaceLockIC, SpaceIC, SpaceGlobeIC];
   const handleJoinRule = (evt) => {
-    openReusableContextMenu(
-      'bottom',
-      getEventCords(evt, '.btn-surface'),
-      (closeMenu) => (
-        <>
-          <MenuHeader>Visibility (who can join)</MenuHeader>
-          {
-            joinRules.map((rule) => (
-              <MenuItem
-                key={rule}
-                variant={rule === joinRule ? 'positive' : 'surface'}
-                iconSrc={
-                  isSpace
-                    ? jrSpaceIC[joinRules.indexOf(rule)]
-                    : jrRoomIC[joinRules.indexOf(rule)]
-                }
-                onClick={() => { closeMenu(); setJoinRule(rule); }}
-                disabled={!parentId && rule === 'restricted'}
-              >
-                { joinRuleText[joinRules.indexOf(rule)] }
-              </MenuItem>
-            ))
-          }
-        </>
-      ),
-    );
+    openReusableContextMenu('bottom', getEventCords(evt, '.btn-surface'), (closeMenu) => (
+      <>
+        <MenuHeader>Visibility (who can join)</MenuHeader>
+        {joinRules.map((rule) => (
+          <MenuItem
+            key={rule}
+            variant={rule === joinRule ? 'positive' : 'surface'}
+            iconSrc={
+              isSpace ? jrSpaceIC[joinRules.indexOf(rule)] : jrRoomIC[joinRules.indexOf(rule)]
+            }
+            onClick={() => {
+              closeMenu();
+              setJoinRule(rule);
+            }}
+            disabled={!parentId && rule === 'restricted'}
+          >
+            {joinRuleText[joinRules.indexOf(rule)]}
+          </MenuItem>
+        ))}
+      </>
+    ));
   };
 
   return (
@@ -168,50 +159,64 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
       <form className="create-room__form" onSubmit={handleSubmit}>
         <SettingTile
           title="Visibility"
-          options={(
+          options={
             <Button onClick={handleJoinRule} iconSrc={ChevronBottomIC}>
               {joinRuleShortText[joinRules.indexOf(joinRule)]}
             </Button>
-          )}
-          content={<Text variant="b3">{`Select who can join this ${isSpace ? 'space' : 'room'}.`}</Text>}
+          }
+          content={
+            <Text variant="b3">{`Select who can join this ${isSpace ? 'space' : 'room'}.`}</Text>
+          }
         />
         {joinRule === 'public' && (
           <div>
-            <Text className="create-room__address__label" variant="b2">{isSpace ? 'Space address' : 'Room address'}</Text>
+            <Text className="create-room__address__label" variant="b2">
+              {isSpace ? 'Space address' : 'Room address'}
+            </Text>
             <div className="create-room__address">
               <Text variant="b1">#</Text>
               <Input
                 value={addressValue}
                 onChange={validateAddress}
-                state={(isValidAddress === false) ? 'error' : 'normal'}
+                state={isValidAddress === false ? 'error' : 'normal'}
                 forwardRef={addressRef}
                 placeholder="my_address"
                 required
               />
               <Text variant="b1">{`:${userHs}`}</Text>
             </div>
-            {isValidAddress === false && <Text className="create-room__address__tip" variant="b3"><span style={{ color: 'var(--bg-danger)' }}>{`#${addressValue}:${userHs} is already in use`}</span></Text>}
+            {isValidAddress === false && (
+              <Text className="create-room__address__tip" variant="b3">
+                <span
+                  style={{ color: 'var(--bg-danger)' }}
+                >{`#${addressValue}:${userHs} is already in use`}</span>
+              </Text>
+            )}
           </div>
         )}
         {!isSpace && joinRule !== 'public' && (
           <SettingTile
             title="Enable end-to-end encryption"
             options={<Toggle isActive={isEncrypted} onToggle={setIsEncrypted} />}
-            content={<Text variant="b3">You can’t disable this later. Bridges & most bots won’t work yet.</Text>}
+            content={
+              <Text variant="b3">
+                You can’t disable this later. Bridges & most bots won’t work yet.
+              </Text>
+            }
           />
         )}
         <SettingTile
           title="Select your role"
-          options={(
+          options={
             <SegmentControl
               selected={roleIndex}
               segments={[{ text: 'Admin' }, { text: 'Founder' }]}
               onSelect={setRoleIndex}
             />
-          )}
-          content={(
+          }
+          content={
             <Text variant="b3">Selecting Admin sets 100 power level whereas Founder sets 101.</Text>
-          )}
+          }
         />
         <Input name="topic" minHeight={174} resizable label="Topic (optional)" />
         <div className="create-room__name-wrapper">
@@ -231,7 +236,11 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
             <Text>{`Creating ${isSpace ? 'space' : 'room'}...`}</Text>
           </div>
         )}
-        {typeof creatingError === 'string' && <Text className="create-room__error" variant="b3">{creatingError}</Text>}
+        {typeof creatingError === 'string' && (
+          <Text className="create-room__error" variant="b3">
+            {creatingError}
+          </Text>
+        )}
       </form>
     </div>
   );
@@ -275,27 +284,22 @@ function CreateRoom() {
   return (
     <Dialog
       isOpen={create !== null}
-      title={(
+      title={
         <Text variant="s1" weight="medium" primary>
-          {parentId ? twemojify(room.name) : 'Home'}
+          {parentId ? room.name : 'Home'}
           <span style={{ color: 'var(--tc-surface-low)' }}>
             {` — create ${isSpace ? 'space' : 'room'}`}
           </span>
         </Text>
-      )}
+      }
       contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
       onRequestClose={onRequestClose}
     >
-      {
-        create
-          ? (
-            <CreateRoomContent
-              isSpace={isSpace}
-              parentId={parentId}
-              onRequestClose={onRequestClose}
-            />
-          ) : <div />
-      }
+      {create ? (
+        <CreateRoomContent isSpace={isSpace} parentId={parentId} onRequestClose={onRequestClose} />
+      ) : (
+        <div />
+      )}
     </Dialog>
   );
 }
diff --git a/src/app/organisms/emoji-board/EmojiBoard.jsx b/src/app/organisms/emoji-board/EmojiBoard.jsx
deleted file mode 100644 (file)
index 84c4130..0000000
+++ /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(
-          <span key={emojiIndex}>
-            {emoji.hexcode ? (
-              // This is a unicode emoji, and should be rendered with twemoji
-              parse(
-                twemoji.parse(emoji.unicode, {
-                  attributes: () => ({
-                    unicode: emoji.unicode,
-                    shortcodes: emoji.shortcodes?.toString(),
-                    hexcode: emoji.hexcode,
-                    loading: 'lazy',
-                  }),
-                  base: TWEMOJI_BASE_URL,
-                })
-              )
-            ) : (
-              // This is a custom emoji, and should be render as an mxc
-              <img
-                className="emoji"
-                draggable="false"
-                loading="lazy"
-                alt={emoji.shortcode}
-                unicode={`:${emoji.shortcode}:`}
-                shortcodes={emoji.shortcode}
-                src={initMatrix.matrixClient.mxcUrlToHttp(emoji.mxc)}
-                data-mx-emoticon={emoji.mxc}
-              />
-            )}
-          </span>
-        );
-      }
-      emojiBoard.push(
-        <div key={r} className="emoji-row">
-          {emojiRow}
-        </div>
-      );
-    }
-    return emojiBoard;
-  }
-
-  return (
-    <div className="emoji-group">
-      <Text className="emoji-group__header" variant="b2" weight="bold">
-        {name}
-      </Text>
-      {groupEmojis.length !== 0 && <div className="emoji-set noselect">{getEmojiBoard()}</div>}
-    </div>
-  );
-});
-
-EmojiGroup.propTypes = {
-  name: PropTypes.string.isRequired,
-  groupEmojis: PropTypes.arrayOf(
-    PropTypes.shape({
-      length: PropTypes.number,
-      unicode: PropTypes.string,
-      hexcode: PropTypes.string,
-      mxc: PropTypes.string,
-      shortcode: PropTypes.string,
-      shortcodes: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
-    })
-  ).isRequired,
-};
-
-const asyncSearch = new AsyncSearch();
-asyncSearch.setup(emojis, { keys: ['shortcode'], isContain: true, limit: 40 });
-function SearchedEmoji() {
-  const [searchedEmojis, setSearchedEmojis] = useState(null);
-
-  function handleSearchEmoji(resultEmojis, term) {
-    if (term === '' || resultEmojis.length === 0) {
-      if (term === '') setSearchedEmojis(null);
-      else setSearchedEmojis({ emojis: [] });
-      return;
-    }
-    setSearchedEmojis({ emojis: resultEmojis });
-  }
-
-  useEffect(() => {
-    asyncSearch.on(asyncSearch.RESULT_SENT, handleSearchEmoji);
-    return () => {
-      asyncSearch.removeListener(asyncSearch.RESULT_SENT, handleSearchEmoji);
-    };
-  }, []);
-
-  if (searchedEmojis === null) return false;
-
-  return (
-    <EmojiGroup
-      key="-1"
-      name={searchedEmojis.emojis.length === 0 ? 'No search result found' : 'Search results'}
-      groupEmojis={searchedEmojis.emojis}
-    />
-  );
-}
-
-function EmojiBoard({ onSelect, searchRef }) {
-  const scrollEmojisRef = useRef(null);
-  const emojiInfo = useRef(null);
-
-  function isTargetNotEmoji(target) {
-    return target.classList.contains('emoji') === false;
-  }
-  function getEmojiDataFromTarget(target) {
-    const unicode = target.getAttribute('unicode');
-    const hexcode = target.getAttribute('hexcode');
-    const mxc = target.getAttribute('data-mx-emoticon');
-    let shortcodes = target.getAttribute('shortcodes');
-    if (typeof shortcodes === 'undefined') shortcodes = undefined;
-    else shortcodes = shortcodes.split(',');
-    return {
-      unicode,
-      hexcode,
-      shortcodes,
-      mxc,
-    };
-  }
-
-  function selectEmoji(e) {
-    if (isTargetNotEmoji(e.target)) return;
-
-    const emoji = getEmojiDataFromTarget(e.target);
-    onSelect(emoji);
-    if (emoji.hexcode) addRecentEmoji(emoji.unicode);
-  }
-
-  function setEmojiInfo(emoji) {
-    const infoEmoji = emojiInfo.current.firstElementChild.firstElementChild;
-    const infoShortcode = emojiInfo.current.lastElementChild;
-
-    infoEmoji.src = emoji.src;
-    infoEmoji.alt = emoji.unicode;
-    infoShortcode.textContent = `:${emoji.shortcode}:`;
-  }
-
-  function hoverEmoji(e) {
-    if (isTargetNotEmoji(e.target)) return;
-
-    const emoji = e.target;
-    const { shortcodes, unicode } = getEmojiDataFromTarget(emoji);
-    const { src } = e.target;
-
-    if (typeof shortcodes === 'undefined') {
-      searchRef.current.placeholder = 'Search';
-      setEmojiInfo({
-        unicode: '🙂',
-        shortcode: 'slight_smile',
-        src: 'https://twemoji.maxcdn.com/v/13.1.0/72x72/1f642.png',
-      });
-      return;
-    }
-    if (searchRef.current.placeholder === shortcodes[0]) return;
-    searchRef.current.setAttribute('placeholder', shortcodes[0]);
-    setEmojiInfo({ shortcode: shortcodes[0], src, unicode });
-  }
-
-  function handleSearchChange() {
-    const term = searchRef.current.value;
-    asyncSearch.search(term);
-    scrollEmojisRef.current.scrollTop = 0;
-  }
-
-  const [availableEmojis, setAvailableEmojis] = useState([]);
-  const [recentEmojis, setRecentEmojis] = useState([]);
-
-  const recentOffset = recentEmojis.length > 0 ? 1 : 0;
-
-  useEffect(() => {
-    const updateAvailableEmoji = (selectedRoomId) => {
-      if (!selectedRoomId) {
-        setAvailableEmojis([]);
-        return;
-      }
-
-      const mx = initMatrix.matrixClient;
-      const room = mx.getRoom(selectedRoomId);
-      const parentIds = initMatrix.roomList.getAllParentSpaces(room.roomId);
-      const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
-      if (room) {
-        const packs = getRelevantPacks(room.client, [room, ...parentRooms]).filter(
-          (pack) => pack.getEmojis().length !== 0
-        );
-
-        // Set an index for each pack so that we know where to jump when the user uses the nav
-        for (let i = 0; i < packs.length; i += 1) {
-          packs[i].packIndex = i;
-        }
-        setAvailableEmojis(packs);
-      }
-    };
-
-    const onOpen = () => {
-      searchRef.current.value = '';
-      handleSearchChange();
-
-      // only update when board is getting opened to prevent shifting UI
-      setRecentEmojis(getRecentEmojis(3 * ROW_EMOJIS_COUNT));
-    };
-
-    navigation.on(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
-    navigation.on(cons.events.navigation.EMOJIBOARD_OPENED, onOpen);
-    return () => {
-      navigation.removeListener(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
-      navigation.removeListener(cons.events.navigation.EMOJIBOARD_OPENED, onOpen);
-    };
-  }, []);
-
-  function openGroup(groupOrder) {
-    let tabIndex = groupOrder;
-    const $emojiContent = scrollEmojisRef.current.firstElementChild;
-    const groupCount = $emojiContent.childElementCount;
-    if (groupCount > emojiGroups.length) {
-      tabIndex += groupCount - emojiGroups.length - availableEmojis.length - recentOffset;
-    }
-    $emojiContent.children[tabIndex].scrollIntoView();
-  }
-
-  return (
-    <div id="emoji-board" className="emoji-board">
-      <ScrollView invisible>
-        <div className="emoji-board__nav">
-          {recentEmojis.length > 0 && (
-            <IconButton
-              onClick={() => openGroup(0)}
-              src={RecentClockIC}
-              tooltip="Recent"
-              tooltipPlacement="left"
-            />
-          )}
-          <div className="emoji-board__nav-custom">
-            {availableEmojis.map((pack) => {
-              const src = initMatrix.matrixClient.mxcUrlToHttp(
-                pack.avatarUrl ?? pack.getEmojis()[0].mxc
-              );
-              return (
-                <IconButton
-                  onClick={() => openGroup(recentOffset + pack.packIndex)}
-                  src={src}
-                  key={pack.packIndex}
-                  tooltip={pack.displayName ?? 'Unknown'}
-                  tooltipPlacement="left"
-                  isImage
-                />
-              );
-            })}
-          </div>
-          <div className="emoji-board__nav-twemoji">
-            {[
-              [0, EmojiIC, 'Smilies'],
-              [1, DogIC, 'Animals'],
-              [2, CupIC, 'Food'],
-              [3, BallIC, 'Activities'],
-              [4, PhotoIC, 'Travel'],
-              [5, BulbIC, 'Objects'],
-              [6, PeaceIC, 'Symbols'],
-              [7, FlagIC, 'Flags'],
-            ].map(([indx, ico, name]) => (
-              <IconButton
-                onClick={() => openGroup(recentOffset + availableEmojis.length + indx)}
-                key={indx}
-                src={ico}
-                tooltip={name}
-                tooltipPlacement="left"
-              />
-            ))}
-          </div>
-        </div>
-      </ScrollView>
-      <div className="emoji-board__content">
-        <div className="emoji-board__content__search">
-          <RawIcon size="small" src={SearchIC} />
-          <Input onChange={handleSearchChange} forwardRef={searchRef} placeholder="Search" />
-        </div>
-        <div className="emoji-board__content__emojis">
-          <ScrollView ref={scrollEmojisRef} autoHide>
-            <div onMouseMove={hoverEmoji} onClick={selectEmoji}>
-              <SearchedEmoji />
-              {recentEmojis.length > 0 && (
-                <EmojiGroup name="Recently used" groupEmojis={recentEmojis} />
-              )}
-              {availableEmojis.map((pack) => (
-                <EmojiGroup
-                  name={pack.displayName ?? 'Unknown'}
-                  key={pack.packIndex}
-                  groupEmojis={pack.getEmojis()}
-                  className="custom-emoji-group"
-                />
-              ))}
-              {emojiGroups.map((group) => (
-                <EmojiGroup key={group.name} name={group.name} groupEmojis={group.emojis} />
-              ))}
-            </div>
-          </ScrollView>
-        </div>
-        <div ref={emojiInfo} className="emoji-board__content__info">
-          <div>{parse(twemoji.parse('🙂', { base: TWEMOJI_BASE_URL }))}</div>
-          <Text>:slight_smile:</Text>
-        </div>
-      </div>
-    </div>
-  );
-}
-
-EmojiBoard.propTypes = {
-  onSelect: PropTypes.func.isRequired,
-  searchRef: PropTypes.shape({}).isRequired,
-};
-
-export default EmojiBoard;
diff --git a/src/app/organisms/emoji-board/EmojiBoard.scss b/src/app/organisms/emoji-board/EmojiBoard.scss
deleted file mode 100644 (file)
index 683026f..0000000
+++ /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 (file)
index 32b7a83..0000000
+++ /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 (
-    <ContextMenu
-      content={(
-        <EmojiBoard onSelect={addEmoji} searchRef={searchRef} />
-      )}
-      afterToggle={afterEmojiBoardToggle}
-      render={(toggleMenu) => (
-        <input
-          ref={openerRef}
-          onClick={toggleMenu}
-          type="button"
-          style={{
-            width: '32px',
-            height: '32px',
-            backgroundColor: 'transparent',
-            position: 'absolute',
-            top: 0,
-            left: 0,
-            padding: 0,
-            border: 'none',
-            visibility: 'hidden',
-          }}
-        />
-      )}
-    />
-  );
-}
-
-export default EmojiBoardOpener;
index 09b605228435049d36e1afb960705018468f7b2e..4ca2088f716047f2b0c8d72da34175270343631e 100644 (file)
@@ -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 (file)
index 3cbd0b8..0000000
+++ /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 (file)
index dff67fb..0000000
+++ /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),
-  });
-}
index 3ae1f2948c663910a640f738c2ff92cd928d4c12..1b543c052ca5ab7ddd07c1ead8dc2af504b622ca 100644 (file)
@@ -2,7 +2,6 @@
 import React, { useState, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import './EmojiVerification.scss';
-import { twemojify } from '../../../util/twemojify';
 
 import initMatrix from '../../../client/initMatrix';
 import cons from '../../../client/state/cons';
@@ -30,8 +29,9 @@ function EmojiVerificationContent({ data, requestClose }) {
 
   const beginVerification = async () => {
     if (
-      isCrossVerified(mx.deviceId)
-      && (mx.getCrossSigningId() === null || await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing') === false)
+      isCrossVerified(mx.deviceId) &&
+      (mx.getCrossSigningId() === null ||
+        (await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing')) === false)
     ) {
       if (!hasPrivateKey(getDefaultSSKey())) {
         const keyData = await accessSecretStorage('Emoji verification');
@@ -106,16 +106,20 @@ function EmojiVerificationContent({ data, requestClose }) {
           {sas.sas.emoji.map((emoji, i) => (
             // eslint-disable-next-line react/no-array-index-key
             <div className="emoji-verification__emoji-block" key={`${emoji[1]}-${i}`}>
-              <Text variant="h1">{twemojify(emoji[0])}</Text>
+              <Text variant="h1">{emoji[0]}</Text>
               <Text>{emoji[1]}</Text>
             </div>
           ))}
         </div>
         <div className="emoji-verification__buttons">
-          {process ? renderWait() : (
+          {process ? (
+            renderWait()
+          ) : (
             <>
-              <Button variant="primary" onClick={sasConfirm}>They match</Button>
-              <Button onClick={sasMismatch}>{'They don\'t match'}</Button>
+              <Button variant="primary" onClick={sasConfirm}>
+                They match
+              </Button>
+              <Button onClick={sasMismatch}>No match</Button>
             </>
           )}
         </div>
@@ -127,9 +131,7 @@ function EmojiVerificationContent({ data, requestClose }) {
     return (
       <div className="emoji-verification__content">
         <Text>Please accept the request from other device.</Text>
-        <div className="emoji-verification__buttons">
-          {renderWait()}
-        </div>
+        <div className="emoji-verification__buttons">{renderWait()}</div>
       </div>
     );
   }
@@ -138,11 +140,13 @@ function EmojiVerificationContent({ data, requestClose }) {
     <div className="emoji-verification__content">
       <Text>Click accept to start the verification process.</Text>
       <div className="emoji-verification__buttons">
-        {
-          process
-            ? renderWait()
-            : <Button variant="primary" onClick={beginVerification}>Accept</Button>
-        }
+        {process ? (
+          renderWait()
+        ) : (
+          <Button variant="primary" onClick={beginVerification}>
+            Accept
+          </Button>
+        )}
       </div>
     </div>
   );
@@ -180,19 +184,19 @@ function EmojiVerification() {
     <Dialog
       isOpen={data !== null}
       className="emoji-verification"
-      title={(
+      title={
         <Text variant="s1" weight="medium" primary>
           Emoji verification
         </Text>
-      )}
+      }
       contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
       onRequestClose={requestClose}
     >
-      {
-        data !== null
-          ? <EmojiVerificationContent data={data} requestClose={requestClose} />
-          : <div />
-      }
+      {data !== null ? (
+        <EmojiVerificationContent data={data} requestClose={requestClose} />
+      ) : (
+        <div />
+      )}
     </Dialog>
   );
 }
diff --git a/src/app/organisms/invite-list/InviteList.jsx b/src/app/organisms/invite-list/InviteList.jsx
deleted file mode 100644 (file)
index 231928f..0000000
+++ /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 (
-      <RoomTile
-        key={myRoom.roomId}
-        name={roomName}
-        avatarSrc={initMatrix.matrixClient.getRoom(roomId).getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop')}
-        id={roomAlias}
-        inviterName={inviterName}
-        options={
-          procInvite.has(myRoom.roomId)
-            ? (<Spinner size="small" />)
-            : (
-              <div className="invite-btn__container">
-                <Button onClick={() => rejectInvite(myRoom.roomId)}>Reject</Button>
-                <Button onClick={() => acceptInvite(myRoom.roomId)} variant="primary">Accept</Button>
-              </div>
-            )
-        }
-      />
-    );
-  }
-
-  return (
-    <PopupWindow
-      isOpen={isOpen}
-      title="Invites"
-      contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
-      onRequestClose={onRequestClose}
-    >
-      <div className="invites-content">
-        { initMatrix.roomList.inviteDirects.size !== 0 && (
-          <div className="invites-content__subheading">
-            <Text variant="b3" weight="bold">Direct Messages</Text>
-          </div>
-        )}
-        {
-          Array.from(initMatrix.roomList.inviteDirects).map((roomId) => {
-            const myRoom = initMatrix.matrixClient.getRoom(roomId);
-            if (myRoom === null) return null;
-            const roomName = myRoom.name;
-            return (
-              <RoomTile
-                key={myRoom.roomId}
-                name={roomName}
-                id={myRoom.getDMInviter() || roomId}
-                options={
-                  procInvite.has(myRoom.roomId)
-                    ? (<Spinner size="small" />)
-                    : (
-                      <div className="invite-btn__container">
-                        <Button onClick={() => rejectInvite(myRoom.roomId, true)}>Reject</Button>
-                        <Button onClick={() => acceptInvite(myRoom.roomId, true)} variant="primary">Accept</Button>
-                      </div>
-                    )
-                }
-              />
-            );
-          })
-        }
-        { initMatrix.roomList.inviteSpaces.size !== 0 && (
-          <div className="invites-content__subheading">
-            <Text variant="b3" weight="bold">Spaces</Text>
-          </div>
-        )}
-        { Array.from(initMatrix.roomList.inviteSpaces).map(renderRoomTile) }
-
-        { initMatrix.roomList.inviteRooms.size !== 0 && (
-          <div className="invites-content__subheading">
-            <Text variant="b3" weight="bold">Rooms</Text>
-          </div>
-        )}
-        { Array.from(initMatrix.roomList.inviteRooms).map(renderRoomTile) }
-      </div>
-    </PopupWindow>
-  );
-}
-
-InviteList.propTypes = {
-  isOpen: PropTypes.bool.isRequired,
-  onRequestClose: PropTypes.func.isRequired,
-};
-
-export default InviteList;
diff --git a/src/app/organisms/invite-list/InviteList.scss b/src/app/organisms/invite-list/InviteList.scss
deleted file mode 100644 (file)
index da1968c..0000000
+++ /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
index 7519510245cfe263f6a991bbadd0bb226515b738..10f55f9f644b81074e44dedd70d1ac37c2d4a142 100644 (file)
@@ -3,10 +3,8 @@ import PropTypes from 'prop-types';
 import './InviteUser.scss';
 
 import initMatrix from '../../../client/initMatrix';
-import cons from '../../../client/state/cons';
 import * as roomActions from '../../../client/action/room';
-import { selectRoom } from '../../../client/action/navigation';
-import { hasDMWith, hasDevices } from '../../../util/matrixUtil';
+import { hasDevices } from '../../../util/matrixUtil';
 
 import Text from '../../atoms/text/Text';
 import Button from '../../atoms/button/Button';
@@ -18,10 +16,10 @@ import RoomTile from '../../molecules/room-tile/RoomTile';
 
 import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 import UserIC from '../../../../public/res/ic/outlined/user.svg';
+import { useRoomNavigate } from '../../hooks/useRoomNavigate';
+import { getDMRoomFor } from '../../utils/matrix';
 
-function InviteUser({
-  isOpen, roomId, searchTerm, onRequestClose,
-}) {
+function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) {
   const [isSearching, updateIsSearching] = useState(false);
   const [searchQuery, updateSearchQuery] = useState({});
   const [users, updateUsers] = useState([]);
@@ -37,6 +35,7 @@ function InviteUser({
   const usernameRef = useRef(null);
 
   const mx = initMatrix.matrixClient;
+  const { navigateRoom } = useRoomNavigate();
 
   function getMapCopy(myMap) {
     const newMap = new Map();
@@ -76,11 +75,13 @@ function InviteUser({
     if (isInputUserId) {
       try {
         const result = await mx.getProfileInfo(inputUsername);
-        updateUsers([{
-          user_id: inputUsername,
-          display_name: result.displayname,
-          avatar_url: result.avatar_url,
-        }]);
+        updateUsers([
+          {
+            user_id: inputUsername,
+            display_name: result.displayname,
+            avatar_url: result.avatar_url,
+          },
+        ]);
       } catch (e) {
         updateSearchQuery({ error: `${inputUsername} not found!` });
       }
@@ -105,9 +106,9 @@ function InviteUser({
 
   async function createDM(userId) {
     if (mx.getUserId() === userId) return;
-    const dmRoomId = hasDMWith(userId);
+    const dmRoomId = getDMRoomFor(mx, userId)?.roomId;
     if (dmRoomId) {
-      selectRoom(dmRoomId);
+      navigateRoom(dmRoomId);
       onRequestClose();
       return;
     }
@@ -120,6 +121,7 @@ function InviteUser({
       const result = await roomActions.createDM(userId, await hasDevices(userId));
       roomIdToUserId.set(result.room_id, userId);
       updateRoomIdToUserId(getMapCopy(roomIdToUserId));
+      onDMCreated(result.room_id);
     } catch (e) {
       deleteUserFromProc(userId);
       if (typeof e.message === 'string') procUserError.set(userId, e.message);
@@ -150,7 +152,13 @@ function InviteUser({
 
   function renderUserList() {
     const renderOptions = (userId) => {
-      const messageJSX = (message, isPositive) => <Text variant="b2"><span style={{ color: isPositive ? 'var(--bg-positive)' : 'var(--bg-negative)' }}>{message}</span></Text>;
+      const messageJSX = (message, isPositive) => (
+        <Text variant="b2">
+          <span style={{ color: isPositive ? 'var(--bg-positive)' : 'var(--bg-negative)' }}>
+            {message}
+          </span>
+        </Text>
+      );
 
       if (mx.getUserId() === userId) return null;
       if (procUsers.has(userId)) {
@@ -158,7 +166,16 @@ function InviteUser({
       }
       if (createdDM.has(userId)) {
         // eslint-disable-next-line max-len
-        return <Button onClick={() => { selectRoom(createdDM.get(userId)); onRequestClose(); }}>Open</Button>;
+        return (
+          <Button
+            onClick={() => {
+              navigateRoom(createdDM.get(userId));
+              onRequestClose();
+            }}
+          >
+            Open
+          </Button>
+        );
       }
       if (invitedUserIds.has(userId)) {
         return messageJSX('Invited', true);
@@ -178,13 +195,23 @@ function InviteUser({
           }
         }
       }
-      return (typeof roomId === 'string')
-        ? <Button onClick={() => inviteToRoom(userId)} variant="primary">Invite</Button>
-        : <Button onClick={() => createDM(userId)} variant="primary">Message</Button>;
+      return typeof roomId === 'string' ? (
+        <Button onClick={() => inviteToRoom(userId)} variant="primary">
+          Invite
+        </Button>
+      ) : (
+        <Button onClick={() => createDM(userId)} variant="primary">
+          Message
+        </Button>
+      );
     };
     const renderError = (userId) => {
       if (!procUserError.has(userId)) return null;
-      return <Text variant="b2"><span style={{ color: 'var(--bg-danger)' }}>{procUserError.get(userId)}</span></Text>;
+      return (
+        <Text variant="b2">
+          <span style={{ color: 'var(--bg-danger)' }}>{procUserError.get(userId)}</span>
+        </Text>
+      );
     };
 
     return users.map((user) => {
@@ -193,7 +220,11 @@ function InviteUser({
       return (
         <RoomTile
           key={userId}
-          avatarSrc={typeof user.avatar_url === 'string' ? mx.mxcUrlToHttp(user.avatar_url, 42, 42, 'crop') : null}
+          avatarSrc={
+            typeof user.avatar_url === 'string'
+              ? mx.mxcUrlToHttp(user.avatar_url, 42, 42, 'crop')
+              : null
+          }
           name={name}
           id={userId}
           options={renderOptions(userId)}
@@ -217,48 +248,43 @@ function InviteUser({
     };
   }, [isOpen, searchTerm]);
 
-  useEffect(() => {
-    initMatrix.roomList.on(cons.events.roomList.ROOM_CREATED, onDMCreated);
-    return () => {
-      initMatrix.roomList.removeListener(cons.events.roomList.ROOM_CREATED, onDMCreated);
-    };
-  }, [isOpen, procUsers, createdDM, roomIdToUserId]);
-
   return (
     <PopupWindow
       isOpen={isOpen}
-      title={(typeof roomId === 'string' ? `Invite to ${mx.getRoom(roomId).name}` : 'Direct message')}
+      title={typeof roomId === 'string' ? `Invite to ${mx.getRoom(roomId).name}` : 'Direct message'}
       contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
       onRequestClose={onRequestClose}
     >
       <div className="invite-user">
-        <form className="invite-user__form" onSubmit={(e) => { e.preventDefault(); searchUser(usernameRef.current.value); }}>
+        <form
+          className="invite-user__form"
+          onSubmit={(e) => {
+            e.preventDefault();
+            searchUser(usernameRef.current.value);
+          }}
+        >
           <Input value={searchTerm} forwardRef={usernameRef} label="Name or userId" />
-          <Button disabled={isSearching} iconSrc={UserIC} variant="primary" type="submit">Search</Button>
+          <Button disabled={isSearching} iconSrc={UserIC} variant="primary" type="submit">
+            Search
+          </Button>
         </form>
         <div className="invite-user__search-status">
-          {
-            typeof searchQuery.username !== 'undefined' && isSearching && (
-              <div className="flex--center">
-                <Spinner size="small" />
-                <Text variant="b2">{`Searching for user "${searchQuery.username}"...`}</Text>
-              </div>
-            )
-          }
-          {
-            typeof searchQuery.username !== 'undefined' && !isSearching && (
-              <Text variant="b2">{`Search result for user "${searchQuery.username}"`}</Text>
-            )
-          }
-          {
-            searchQuery.error && <Text className="invite-user__search-error" variant="b2">{searchQuery.error}</Text>
-          }
+          {typeof searchQuery.username !== 'undefined' && isSearching && (
+            <div className="flex--center">
+              <Spinner size="small" />
+              <Text variant="b2">{`Searching for user "${searchQuery.username}"...`}</Text>
+            </div>
+          )}
+          {typeof searchQuery.username !== 'undefined' && !isSearching && (
+            <Text variant="b2">{`Search result for user "${searchQuery.username}"`}</Text>
+          )}
+          {searchQuery.error && (
+            <Text className="invite-user__search-error" variant="b2">
+              {searchQuery.error}
+            </Text>
+          )}
         </div>
-        { users.length !== 0 && (
-          <div className="invite-user__content">
-            {renderUserList()}
-          </div>
-        )}
+        {users.length !== 0 && <div className="invite-user__content">{renderUserList()}</div>}
       </div>
     </PopupWindow>
   );
index bc0a8adb7b8e893ae859f8f981e58c6f6d8a66e1..9fa5542dfe1ba050d294f6bbb9906324228a2474 100644 (file)
@@ -6,7 +6,6 @@ import initMatrix from '../../../client/initMatrix';
 import cons from '../../../client/state/cons';
 import navigation from '../../../client/state/navigation';
 import { join } from '../../../client/action/room';
-import { selectRoom, selectTab } from '../../../client/action/navigation';
 
 import Text from '../../atoms/text/Text';
 import IconButton from '../../atoms/button/IconButton';
@@ -18,36 +17,24 @@ import Dialog from '../../molecules/dialog/Dialog';
 import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 
 import { useStore } from '../../hooks/useStore';
+import { useRoomNavigate } from '../../hooks/useRoomNavigate';
 
 const ALIAS_OR_ID_REG = /^[#|!].+:.+\..+$/;
 
 function JoinAliasContent({ term, requestClose }) {
   const [process, setProcess] = useState(false);
   const [error, setError] = useState(undefined);
-  const [lastJoinId, setLastJoinId] = useState(undefined);
 
   const mx = initMatrix.matrixClient;
   const mountStore = useStore();
 
+  const { navigateRoom } = useRoomNavigate();
+
   const openRoom = (roomId) => {
-    const room = mx.getRoom(roomId);
-    if (!room) return;
-    if (room.isSpaceRoom()) selectTab(roomId);
-    else selectRoom(roomId);
+    navigateRoom(roomId);
     requestClose();
   };
 
-  useEffect(() => {
-    const handleJoin = (roomId) => {
-      if (lastJoinId !== roomId) return;
-      openRoom(roomId);
-    };
-    initMatrix.roomList.on(cons.events.roomList.ROOM_JOINED, handleJoin);
-    return () => {
-      initMatrix.roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleJoin);
-    };
-  }, [lastJoinId]);
-
   const handleSubmit = async (e) => {
     e.preventDefault();
     mountStore.setItem(true);
@@ -70,13 +57,14 @@ function JoinAliasContent({ term, requestClose }) {
       } catch (err) {
         if (!mountStore.getItem()) return;
         setProcess(false);
-        setError(`Unable to find room/space with ${alias}. Either room/space is private or doesn't exist.`);
+        setError(
+          `Unable to find room/space with ${alias}. Either room/space is private or doesn't exist.`
+        );
       }
     }
     try {
       const roomId = await join(alias, false, via);
       if (!mountStore.getItem()) return;
-      setLastJoinId(roomId);
       openRoom(roomId);
     } catch {
       if (!mountStore.getItem()) return;
@@ -87,24 +75,23 @@ function JoinAliasContent({ term, requestClose }) {
 
   return (
     <form className="join-alias" onSubmit={handleSubmit}>
-      <Input
-        label="Address"
-        value={term}
-        name="alias"
-        required
-      />
-      {error && <Text className="join-alias__error" variant="b3">{error}</Text>}
+      <Input label="Address" value={term} name="alias" required />
+      {error && (
+        <Text className="join-alias__error" variant="b3">
+          {error}
+        </Text>
+      )}
       <div className="join-alias__btn">
-        {
-          process
-            ? (
-              <>
-                <Spinner size="small" />
-                <Text>{process}</Text>
-              </>
-            )
-            : <Button variant="primary" type="submit">Join</Button>
-        }
+        {process ? (
+          <>
+            <Spinner size="small" />
+            <Text>{process}</Text>
+          </>
+        ) : (
+          <Button variant="primary" type="submit">
+            Join
+          </Button>
+        )}
       </div>
     </form>
   );
@@ -141,13 +128,15 @@ function JoinAlias() {
   return (
     <Dialog
       isOpen={data !== null}
-      title={(
-        <Text variant="s1" weight="medium" primary>Join with address</Text>
-      )}
+      title={
+        <Text variant="s1" weight="medium" primary>
+          Join with address
+        </Text>
+      }
       contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
       onRequestClose={requestClose}
     >
-      { data ? <JoinAliasContent term={data.term} requestClose={requestClose} /> : <div /> }
+      {data ? <JoinAliasContent term={data.term} requestClose={requestClose} /> : <div />}
     </Dialog>
   );
 }
diff --git a/src/app/organisms/navigation/Directs.jsx b/src/app/organisms/navigation/Directs.jsx
deleted file mode 100644 (file)
index e65c8af..0000000
+++ /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 <RoomsCategory name="People" hideHeader roomIds={directIds} drawerPostie={drawerPostie} />;
-}
-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 (file)
index 0795e46..0000000
+++ /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 (
-    <div className="drawer">
-      <DrawerHeader selectedTab={selectedTab} spaceId={spaceId} />
-      <div className="drawer__content-wrapper">
-        {navigation.selectedSpacePath.length > 1 && selectedTab !== cons.tabs.DIRECTS && (
-          <DrawerBreadcrumb spaceId={spaceId} />
-        )}
-        <div className="rooms__wrapper">
-          <ScrollView ref={scrollRef} autoHide>
-            <div className="rooms-container">
-              {selectedTab !== cons.tabs.DIRECTS ? (
-                <Home spaceId={spaceId} />
-              ) : (
-                <Directs size={roomList.directs.size} />
-              )}
-            </div>
-          </ScrollView>
-        </div>
-      </div>
-      {systemState !== null && (
-        <div className="drawer__state">
-          <Text>{systemState.status}</Text>
-        </div>
-      )}
-    </div>
-  );
-}
-
-export default Drawer;
diff --git a/src/app/organisms/navigation/Drawer.scss b/src/app/organisms/navigation/Drawer.scss
deleted file mode 100644 (file)
index 4e54c5f..0000000
+++ /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 (file)
index face349..0000000
+++ /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 (
-    <div className="drawer-breadcrumb__wrapper">
-      <ScrollView ref={scrollRef} horizontal vertical={false} invisible>
-        <div className="drawer-breadcrumb">
-          {
-            spacePath.map((id, index) => {
-              const noti = (id !== cons.tabs.HOME && index < spacePath.length)
-                ? getNotiExcept(id, (index === spacePath.length - 1) ? null : spacePath[index + 1])
-                : getHomeNotiExcept((index === spacePath.length - 1) ? null : spacePath[index + 1]);
-
-              return (
-                <React.Fragment
-                  key={id}
-                >
-                  { index !== 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
-                  <Button
-                    className={index === spacePath.length - 1 ? 'drawer-breadcrumb__btn--selected' : ''}
-                    onClick={() => {
-                      if (id === cons.tabs.HOME) selectTab(id);
-                      else selectSpace(id);
-                    }}
-                  >
-                    <Text variant="b2">{id === cons.tabs.HOME ? 'Home' : twemojify(mx.getRoom(id).name)}</Text>
-                    { noti !== null && (
-                      <NotificationBadge
-                        alert={noti.highlight !== 0}
-                        content={noti.total > 0 ? abbreviateNumber(noti.total) : null}
-                      />
-                    )}
-                  </Button>
-                </React.Fragment>
-              );
-            })
-          }
-          <div style={{ width: 'var(--sp-extra-tight)', height: '100%' }} />
-        </div>
-      </ScrollView>
-    </div>
-  );
-}
-
-DrawerBreadcrumb.defaultProps = {
-  spaceId: null,
-};
-
-DrawerBreadcrumb.propTypes = {
-  spaceId: PropTypes.string,
-};
-
-export default DrawerBreadcrumb;
diff --git a/src/app/organisms/navigation/DrawerBreadcrumb.scss b/src/app/organisms/navigation/DrawerBreadcrumb.scss
deleted file mode 100644 (file)
index 0b7bacc..0000000
+++ /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 (file)
index e8782e3..0000000
+++ /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 (
-    <>
-      <MenuHeader>Add rooms or spaces</MenuHeader>
-      <MenuItem
-        iconSrc={SpacePlusIC}
-        onClick={() => { afterOptionSelect(); openCreateRoom(true, spaceId); }}
-        disabled={!canManage}
-      >
-        Create new space
-      </MenuItem>
-      <MenuItem
-        iconSrc={HashPlusIC}
-        onClick={() => { afterOptionSelect(); openCreateRoom(false, spaceId); }}
-        disabled={!canManage}
-      >
-        Create new room
-      </MenuItem>
-      { !spaceId && (
-        <MenuItem
-          iconSrc={HashGlobeIC}
-          onClick={() => { afterOptionSelect(); openPublicRooms(); }}
-        >
-          Explore public rooms
-        </MenuItem>
-      )}
-      { !spaceId && (
-        <MenuItem
-          iconSrc={PlusIC}
-          onClick={() => { afterOptionSelect(); openJoinAlias(); }}
-        >
-          Join with address
-        </MenuItem>
-      )}
-      { spaceId && (
-        <MenuItem
-          iconSrc={PlusIC}
-          onClick={() => { afterOptionSelect(); openSpaceAddExisting(spaceId); }}
-          disabled={!canManage}
-        >
-          Add existing
-        </MenuItem>
-      )}
-      { spaceId && (
-        <MenuItem
-          onClick={() => { afterOptionSelect(); openSpaceManage(spaceId); }}
-          iconSrc={HashSearchIC}
-        >
-          Manage rooms
-        </MenuItem>
-      )}
-    </>
-  );
-}
-HomeSpaceOptions.defaultProps = {
-  spaceId: null,
-};
-HomeSpaceOptions.propTypes = {
-  spaceId: PropTypes.string,
-  afterOptionSelect: PropTypes.func.isRequired,
-};
-
-function DrawerHeader({ selectedTab, spaceId }) {
-  const mx = initMatrix.matrixClient;
-  const tabName = selectedTab !== cons.tabs.DIRECTS ? 'Home' : 'Direct messages';
-
-  const isDMTab = selectedTab === cons.tabs.DIRECTS;
-  const room = mx.getRoom(spaceId);
-  const spaceName = isDMTab ? null : (room?.name || null);
-
-  const openSpaceOptions = (e) => {
-    e.preventDefault();
-    openReusableContextMenu(
-      'bottom',
-      getEventCords(e, '.header'),
-      (closeMenu) => <SpaceOptions roomId={spaceId} afterOptionSelect={closeMenu} />,
-    );
-  };
-
-  const openHomeSpaceOptions = (e) => {
-    e.preventDefault();
-    openReusableContextMenu(
-      'right',
-      getEventCords(e, '.ic-btn'),
-      (closeMenu) => <HomeSpaceOptions spaceId={spaceId} afterOptionSelect={closeMenu} />,
-    );
-  };
-
-  return (
-    <Header>
-      {spaceName ? (
-        <button
-          className="drawer-header__btn"
-          onClick={openSpaceOptions}
-          type="button"
-          onMouseUp={(e) => blurOnBubbling(e, '.drawer-header__btn')}
-        >
-          <TitleWrapper>
-            <Text variant="s1" weight="medium" primary>{twemojify(spaceName)}</Text>
-          </TitleWrapper>
-          <RawIcon size="small" src={ChevronBottomIC} />
-        </button>
-      ) : (
-        <TitleWrapper>
-          <Text variant="s1" weight="medium" primary>{tabName}</Text>
-        </TitleWrapper>
-      )}
-
-      { isDMTab && <IconButton onClick={() => openInviteUser()} tooltip="Start DM" src={PlusIC} size="small" /> }
-      { !isDMTab && <IconButton onClick={openHomeSpaceOptions} tooltip="Add rooms/spaces" src={PlusIC} size="small" /> }
-    </Header>
-  );
-}
-
-DrawerHeader.defaultProps = {
-  spaceId: null,
-};
-DrawerHeader.propTypes = {
-  selectedTab: PropTypes.string.isRequired,
-  spaceId: PropTypes.string,
-};
-
-export default DrawerHeader;
diff --git a/src/app/organisms/navigation/DrawerHeader.scss b/src/app/organisms/navigation/DrawerHeader.scss
deleted file mode 100644 (file)
index 9ed17e4..0000000
+++ /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 (file)
index 6bfa6c0..0000000
+++ /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 && (
-        <RoomsCategory name="Spaces" roomIds={spaceIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
-      )}
-
-      { roomIds.length !== 0 && (
-        <RoomsCategory name="Rooms" roomIds={roomIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
-      )}
-
-      { directIds.length !== 0 && (
-        <RoomsCategory name="People" roomIds={directIds.sort(roomIdByActivity)} drawerPostie={drawerPostie} />
-      )}
-
-      { isCategorized && [...categories.keys()].sort(roomIdByAtoZ).map((catId) => {
-        const rms = [];
-        const dms = [];
-        categories.get(catId).forEach((id) => {
-          if (directs.has(id)) dms.push(id);
-          else rms.push(id);
-        });
-        rms.sort(roomIdByAtoZ);
-        dms.sort(roomIdByActivity);
-        return (
-          <RoomsCategory
-            key={catId}
-            spaceId={catId}
-            name={mx.getRoom(catId).name}
-            roomIds={rms.concat(dms)}
-            drawerPostie={drawerPostie}
-          />
-        );
-      })}
-    </>
-  );
-}
-Home.defaultProps = {
-  spaceId: null,
-};
-Home.propTypes = {
-  spaceId: PropTypes.string,
-};
-
-export default Home;
diff --git a/src/app/organisms/navigation/Navigation.jsx b/src/app/organisms/navigation/Navigation.jsx
deleted file mode 100644 (file)
index 24bd1bd..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react';
-import './Navigation.scss';
-
-import SideBar from './SideBar';
-import Drawer from './Drawer';
-
-function Navigation() {
-  return (
-    <div className="navigation">
-      <SideBar />
-      <Drawer />
-    </div>
-  );
-}
-
-export default Navigation;
diff --git a/src/app/organisms/navigation/Navigation.scss b/src/app/organisms/navigation/Navigation.scss
deleted file mode 100644 (file)
index 4a932c7..0000000
+++ /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 (file)
index b566651..0000000
+++ /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) => <SpaceOptions roomId={spaceId} afterOptionSelect={closeMenu} />,
-    );
-  };
-
-  const openHomeSpaceOptions = (e) => {
-    e.preventDefault();
-    openReusableContextMenu(
-      'right',
-      getEventCords(e, '.ic-btn'),
-      (closeMenu) => <HomeSpaceOptions spaceId={spaceId} afterOptionSelect={closeMenu} />,
-    );
-  };
-
-  const renderSelector = (roomId) => {
-    const isSpace = spaces.has(roomId);
-    const isDM = directs.has(roomId);
-
-    return (
-      <Selector
-        key={roomId}
-        roomId={roomId}
-        isDM={isDM}
-        drawerPostie={drawerPostie}
-        onClick={() => (isSpace ? selectSpace(roomId) : selectRoom(roomId))}
-      />
-    );
-  };
-
-  return (
-    <div className="room-category">
-      {!hideHeader && (
-        <div className="room-category__header">
-          <button className="room-category__toggle" onClick={() => setIsOpen(!isOpen)} type="button">
-            <RawIcon src={isOpen ? ChevronBottomIC : ChevronRightIC} size="extra-small" />
-            <Text className="cat-header" variant="b3" weight="medium">{name}</Text>
-          </button>
-          {spaceId && <IconButton onClick={openSpaceOptions} tooltip="Space options" src={HorizontalMenuIC} size="extra-small" />}
-          {spaceId && <IconButton onClick={openHomeSpaceOptions} tooltip="Add rooms/spaces" src={PlusIC} size="extra-small" />}
-        </div>
-      )}
-      {(isOpen || hideHeader) && (
-        <div className="room-category__content">
-          {roomIds.map(renderSelector)}
-        </div>
-      )}
-    </div>
-  );
-}
-RoomsCategory.defaultProps = {
-  spaceId: null,
-  hideHeader: false,
-};
-RoomsCategory.propTypes = {
-  spaceId: PropTypes.string,
-  name: PropTypes.string.isRequired,
-  hideHeader: PropTypes.bool,
-  roomIds: PropTypes.arrayOf(PropTypes.string).isRequired,
-  drawerPostie: PropTypes.shape({}).isRequired,
-};
-
-export default RoomsCategory;
diff --git a/src/app/organisms/navigation/RoomsCategory.scss b/src/app/organisms/navigation/RoomsCategory.scss
deleted file mode 100644 (file)
index 841290c..0000000
+++ /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 (file)
index cb1086e..0000000
+++ /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) => <SpaceOptions roomId={roomId} afterOptionSelect={closeMenu} />
-        : (closeMenu) => <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />,
-    );
-  };
-
-  return (
-    <RoomSelector
-      key={roomId}
-      name={room.name}
-      roomId={roomId}
-      imageSrc={isDM ? imageSrc : null}
-      iconSrc={isDM ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())}
-      isSelected={navigation.selectedRoomId === roomId}
-      isMuted={isMuted}
-      isUnread={!isMuted && noti.hasNoti(roomId)}
-      notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
-      isAlert={noti.getHighlightNoti(roomId) !== 0}
-      onClick={onClick}
-      onContextMenu={openOptions}
-      options={(
-        <IconButton
-          size="extra-small"
-          tooltip="Options"
-          tooltipPlacement="right"
-          src={VerticalMenuIC}
-          onClick={openOptions}
-        />
-      )}
-    />
-  );
-}
-
-Selector.defaultProps = {
-  isDM: true,
-};
-
-Selector.propTypes = {
-  roomId: PropTypes.string.isRequired,
-  isDM: PropTypes.bool,
-  drawerPostie: PropTypes.shape({}).isRequired,
-  onClick: PropTypes.func.isRequired,
-};
-
-export default Selector;
diff --git a/src/app/organisms/navigation/SideBar.jsx b/src/app/organisms/navigation/SideBar.jsx
deleted file mode 100644 (file)
index 5318696..0000000
+++ /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 (
-    <SidebarAvatar
-      onClick={openSettings}
-      tooltip="Settings"
-      avatar={(
-        <Avatar
-          text={profile.displayName}
-          bgColor={colorMXID(mx.getUserId())}
-          size="normal"
-          imageSrc={profile.avatarUrl !== null ? mx.mxcUrlToHttp(profile.avatarUrl, 42, 42, 'crop') : null}
-        />
-      )}
-    />
-  );
-}
-
-function CrossSigninAlert() {
-  const deviceList = useDeviceList();
-  const unverified = deviceList?.filter((device) => isCrossVerified(device.device_id) === false);
-
-  if (!unverified?.length) return null;
-
-  return (
-    <SidebarAvatar
-      className="sidebar__cross-signin-alert"
-      tooltip={`${unverified.length} unverified sessions`}
-      onClick={() => openSettings(settingTabText.SECURITY)}
-      avatar={<Avatar iconSrc={ShieldUserIC} iconColor="var(--ic-danger-normal)" size="normal" />}
-    />
-  );
-}
-
-function FeaturedTab() {
-  const { roomList, accountData, notifications } = initMatrix;
-  const [selectedTab] = useSelectedTab();
-  useNotificationUpdate();
-
-  function getHomeNoti() {
-    const orphans = roomList.getOrphans();
-    let noti = null;
-
-    orphans.forEach((roomId) => {
-      if (accountData.spaceShortcut.has(roomId)) return;
-      if (!notifications.hasNoti(roomId)) return;
-      if (noti === null) noti = { total: 0, highlight: 0 };
-      const childNoti = notifications.getNoti(roomId);
-      noti.total += childNoti.total;
-      noti.highlight += childNoti.highlight;
-    });
-
-    return noti;
-  }
-  function getDMsNoti() {
-    if (roomList.directs.size === 0) return null;
-    let noti = null;
-
-    [...roomList.directs].forEach((roomId) => {
-      if (!notifications.hasNoti(roomId)) return;
-      if (noti === null) noti = { total: 0, highlight: 0 };
-      const childNoti = notifications.getNoti(roomId);
-      noti.total += childNoti.total;
-      noti.highlight += childNoti.highlight;
-    });
-
-    return noti;
-  }
-
-  const dmsNoti = getDMsNoti();
-  const homeNoti = getHomeNoti();
-
-  return (
-    <>
-      <SidebarAvatar
-        tooltip="Home"
-        active={selectedTab === cons.tabs.HOME}
-        onClick={() => selectTab(cons.tabs.HOME)}
-        avatar={<Avatar iconSrc={HomeIC} size="normal" />}
-        notificationBadge={homeNoti ? (
-          <NotificationBadge
-            alert={homeNoti?.highlight > 0}
-            content={abbreviateNumber(homeNoti.total) || null}
-          />
-        ) : null}
-      />
-      <SidebarAvatar
-        tooltip="People"
-        active={selectedTab === cons.tabs.DIRECTS}
-        onClick={() => selectTab(cons.tabs.DIRECTS)}
-        avatar={<Avatar iconSrc={UserIC} size="normal" />}
-        notificationBadge={dmsNoti ? (
-          <NotificationBadge
-            alert={dmsNoti?.highlight > 0}
-            content={abbreviateNumber(dmsNoti.total) || null}
-          />
-        ) : null}
-      />
-    </>
-  );
-}
-
-function DraggableSpaceShortcut({
-  isActive, spaceId, index, moveShortcut, onDrop,
-}) {
-  const mx = initMatrix.matrixClient;
-  const { notifications } = initMatrix;
-  const room = mx.getRoom(spaceId);
-  const shortcutRef = useRef(null);
-  const avatarRef = useRef(null);
-
-  const openSpaceOptions = (e, sId) => {
-    e.preventDefault();
-    openReusableContextMenu(
-      'right',
-      getEventCords(e, '.sidebar-avatar'),
-      (closeMenu) => <SpaceOptions roomId={sId} afterOptionSelect={closeMenu} />,
-    );
-  };
-
-  const [, drop] = useDrop({
-    accept: 'SPACE_SHORTCUT',
-    collect(monitor) {
-      return {
-        handlerId: monitor.getHandlerId(),
-      };
-    },
-    drop(item) {
-      onDrop(item.index, item.spaceId);
-    },
-    hover(item, monitor) {
-      if (!shortcutRef.current) return;
-
-      const dragIndex = item.index;
-      const hoverIndex = index;
-      if (dragIndex === hoverIndex) return;
-
-      const hoverBoundingRect = shortcutRef.current?.getBoundingClientRect();
-      const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
-      const clientOffset = monitor.getClientOffset();
-      const hoverClientY = clientOffset.y - hoverBoundingRect.top;
-
-      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
-        return;
-      }
-      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
-        return;
-      }
-      moveShortcut(dragIndex, hoverIndex);
-      // eslint-disable-next-line no-param-reassign
-      item.index = hoverIndex;
-    },
-  });
-  const [{ isDragging }, drag] = useDrag({
-    type: 'SPACE_SHORTCUT',
-    item: () => ({ spaceId, index }),
-    collect: (monitor) => ({
-      isDragging: monitor.isDragging(),
-    }),
-  });
-
-  drag(avatarRef);
-  drop(shortcutRef);
-
-  if (shortcutRef.current) {
-    if (isDragging) shortcutRef.current.style.opacity = 0;
-    else shortcutRef.current.style.opacity = 1;
-  }
-
-  return (
-    <SidebarAvatar
-      ref={shortcutRef}
-      active={isActive}
-      tooltip={room.name}
-      onClick={() => selectTab(spaceId)}
-      onContextMenu={(e) => openSpaceOptions(e, spaceId)}
-      avatar={(
-        <Avatar
-          ref={avatarRef}
-          text={room.name}
-          bgColor={colorMXID(room.roomId)}
-          size="normal"
-          imageSrc={room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop') || null}
-        />
-      )}
-      notificationBadge={notifications.hasNoti(spaceId) ? (
-        <NotificationBadge
-          alert={notifications.getHighlightNoti(spaceId) > 0}
-          content={abbreviateNumber(notifications.getTotalNoti(spaceId)) || null}
-        />
-      ) : null}
-    />
-  );
-}
-
-DraggableSpaceShortcut.propTypes = {
-  spaceId: PropTypes.string.isRequired,
-  isActive: PropTypes.bool.isRequired,
-  index: PropTypes.number.isRequired,
-  moveShortcut: PropTypes.func.isRequired,
-  onDrop: PropTypes.func.isRequired,
-};
-
-function SpaceShortcut() {
-  const { accountData } = initMatrix;
-  const [selectedTab] = useSelectedTab();
-  useNotificationUpdate();
-  const [spaceShortcut, setSpaceShortcut] = useState([...accountData.spaceShortcut]);
-
-  useEffect(() => {
-    const handleShortcut = () => setSpaceShortcut([...accountData.spaceShortcut]);
-    accountData.on(cons.events.accountData.SPACE_SHORTCUT_UPDATED, handleShortcut);
-    return () => {
-      accountData.removeListener(cons.events.accountData.SPACE_SHORTCUT_UPDATED, handleShortcut);
-    };
-  }, []);
-
-  const moveShortcut = (dragIndex, hoverIndex) => {
-    const dragSpaceId = spaceShortcut[dragIndex];
-    const newShortcuts = [...spaceShortcut];
-    newShortcuts.splice(dragIndex, 1);
-    newShortcuts.splice(hoverIndex, 0, dragSpaceId);
-    setSpaceShortcut(newShortcuts);
-  };
-
-  const handleDrop = (dragIndex, dragSpaceId) => {
-    if ([...accountData.spaceShortcut][dragIndex] === dragSpaceId) return;
-    moveSpaceShortcut(dragSpaceId, dragIndex);
-  };
-
-  return (
-    <DndProvider backend={HTML5Backend}>
-      {
-        spaceShortcut.map((shortcut, index) => (
-          <DraggableSpaceShortcut
-            key={shortcut}
-            index={index}
-            spaceId={shortcut}
-            isActive={selectedTab === shortcut}
-            moveShortcut={moveShortcut}
-            onDrop={handleDrop}
-          />
-        ))
-      }
-    </DndProvider>
-  );
-}
-
-function useTotalInvites() {
-  const { roomList } = initMatrix;
-  const totalInviteCount = () => roomList.inviteRooms.size
-    + roomList.inviteSpaces.size
-    + roomList.inviteDirects.size;
-  const [totalInvites, updateTotalInvites] = useState(totalInviteCount());
-
-  useEffect(() => {
-    const onInviteListChange = () => {
-      updateTotalInvites(totalInviteCount());
-    };
-    roomList.on(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
-    return () => {
-      roomList.removeListener(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
-    };
-  }, []);
-
-  return [totalInvites];
-}
-
-function SideBar() {
-  const [totalInvites] = useTotalInvites();
-
-  return (
-    <div className="sidebar">
-      <div className="sidebar__scrollable">
-        <ScrollView invisible>
-          <div className="scrollable-content">
-            <div className="featured-container">
-              <FeaturedTab />
-            </div>
-            <div className="sidebar-divider" />
-            <div className="space-container">
-              <SpaceShortcut />
-              <SidebarAvatar
-                tooltip="Pin spaces"
-                onClick={() => openShortcutSpaces()}
-                avatar={<Avatar iconSrc={AddPinIC} size="normal" />}
-              />
-            </div>
-          </div>
-        </ScrollView>
-      </div>
-      <div className="sidebar__sticky">
-        <div className="sidebar-divider" />
-        <div className="sticky-container">
-          <SidebarAvatar
-            tooltip="Search"
-            onClick={() => openSearch()}
-            avatar={<Avatar iconSrc={SearchIC} size="normal" />}
-          />
-          { totalInvites !== 0 && (
-            <SidebarAvatar
-              tooltip="Invites"
-              onClick={() => openInviteList()}
-              avatar={<Avatar iconSrc={InviteIC} size="normal" />}
-              notificationBadge={<NotificationBadge alert content={totalInvites} />}
-            />
-          )}
-          <CrossSigninAlert />
-          <ProfileAvatarMenu />
-        </div>
-      </div>
-    </div>
-  );
-}
-
-export default SideBar;
diff --git a/src/app/organisms/navigation/SideBar.scss b/src/app/organisms/navigation/SideBar.scss
deleted file mode 100644 (file)
index 401947a..0000000
+++ /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);
-  }
-}
index bb7359da310d2957ac5f8669bed5873093303c86..c21c82fa479d08d68fad04a5c1fe38f5f869f65e 100644 (file)
@@ -1,6 +1,5 @@
 import React, { useState, useEffect, useRef } from 'react';
 import PropTypes from 'prop-types';
-import { twemojify } from '../../../util/twemojify';
 
 import initMatrix from '../../../client/initMatrix';
 import colorMXID from '../../../util/colorMXID';
@@ -22,7 +21,9 @@ function ProfileEditor({ userId }) {
   const user = mx.getUser(mx.getUserId());
 
   const displayNameRef = useRef(null);
-  const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null);
+  const [avatarSrc, setAvatarSrc] = useState(
+    user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null
+  );
   const [username, setUsername] = useState(user.displayName);
   const [disabled, setDisabled] = useState(true);
 
@@ -44,7 +45,7 @@ function ProfileEditor({ userId }) {
         'Remove avatar',
         'Are you sure that you want to remove avatar?',
         'Remove',
-        'caution',
+        'caution'
       );
       if (isConfirmed) {
         mx.setAvatarUrl('');
@@ -79,7 +80,10 @@ function ProfileEditor({ userId }) {
     <form
       className="profile-editor__form"
       style={{ marginBottom: avatarSrc ? '24px' : '0' }}
-      onSubmit={(e) => { e.preventDefault(); saveDisplayName(); }}
+      onSubmit={(e) => {
+        e.preventDefault();
+        saveDisplayName();
+      }}
     >
       <Input
         label={`Display name of ${mx.getUserId()}`}
@@ -87,7 +91,9 @@ function ProfileEditor({ userId }) {
         value={mx.getUser(mx.getUserId()).displayName}
         forwardRef={displayNameRef}
       />
-      <Button variant="primary" type="submit" disabled={disabled}>Save</Button>
+      <Button variant="primary" type="submit" disabled={disabled}>
+        Save
+      </Button>
       <Button onClick={cancelDisplayNameChanges}>Cancel</Button>
     </form>
   );
@@ -95,7 +101,9 @@ function ProfileEditor({ userId }) {
   const renderInfo = () => (
     <div className="profile-editor__info" style={{ marginBottom: avatarSrc ? '24px' : '0' }}>
       <div>
-        <Text variant="h2" primary weight="medium">{twemojify(username) ?? userId}</Text>
+        <Text variant="h2" primary weight="medium">
+          {username ?? userId}
+        </Text>
         <IconButton
           src={PencilIC}
           size="extra-small"
@@ -116,9 +124,7 @@ function ProfileEditor({ userId }) {
         onUpload={handleAvatarUpload}
         onRequestRemove={() => handleAvatarUpload(null)}
       />
-      {
-        isEditing ? renderForm() : renderInfo()
-      }
+      {isEditing ? renderForm() : renderInfo()}
     </div>
   );
 }
index b6ce426e08c876288aa1d8e278c28f0921e136aa..b19c9c8676c203c6c8089379657c4046ced8d52d 100644 (file)
@@ -2,16 +2,17 @@ import React, { useState, useEffect, useRef } from 'react';
 import PropTypes from 'prop-types';
 import './ProfileViewer.scss';
 
-import { twemojify } from '../../../util/twemojify';
-
 import initMatrix from '../../../client/initMatrix';
 import cons from '../../../client/state/cons';
 import navigation from '../../../client/state/navigation';
-import { selectRoom, openReusableContextMenu } from '../../../client/action/navigation';
+import { openReusableContextMenu } from '../../../client/action/navigation';
 import * as roomActions from '../../../client/action/room';
 
 import {
-  getUsername, getUsernameOfRoomMember, getPowerLabel, hasDMWith, hasDevices,
+  getUsername,
+  getUsernameOfRoomMember,
+  getPowerLabel,
+  hasDevices,
 } from '../../../util/matrixUtil';
 import { getEventCords } from '../../../util/common';
 import colorMXID from '../../../util/colorMXID';
@@ -33,26 +34,24 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 
 import { useForceUpdate } from '../../hooks/useForceUpdate';
 import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
+import { useRoomNavigate } from '../../hooks/useRoomNavigate';
+import { getDMRoomFor } from '../../utils/matrix';
 
-function ModerationTools({
-  roomId, userId,
-}) {
+function ModerationTools({ roomId, userId }) {
   const mx = initMatrix.matrixClient;
   const room = mx.getRoom(roomId);
   const roomMember = room.getMember(userId);
 
   const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
   const powerLevel = roomMember?.powerLevel || 0;
-  const canIKick = (
-    roomMember?.membership === 'join'
-    && room.currentState.hasSufficientPowerLevelFor('kick', myPowerLevel)
-    && powerLevel < myPowerLevel
-  );
-  const canIBan = (
-    ['join', 'leave'].includes(roomMember?.membership)
-    && room.currentState.hasSufficientPowerLevelFor('ban', myPowerLevel)
-    && powerLevel < myPowerLevel
-  );
+  const canIKick =
+    roomMember?.membership === 'join' &&
+    room.currentState.hasSufficientPowerLevelFor('kick', myPowerLevel) &&
+    powerLevel < myPowerLevel;
+  const canIBan =
+    ['join', 'leave'].includes(roomMember?.membership) &&
+    room.currentState.hasSufficientPowerLevelFor('ban', myPowerLevel) &&
+    powerLevel < myPowerLevel;
 
   const handleKick = (e) => {
     e.preventDefault();
@@ -120,13 +119,14 @@ function SessionInfo({ userId }) {
       <div className="session-info__chips">
         {devices === null && <Text variant="b2">Loading sessions...</Text>}
         {devices?.length === 0 && <Text variant="b2">No session found.</Text>}
-        {devices !== null && (devices.map((device) => (
-          <Chip
-            key={device.deviceId}
-            iconSrc={ShieldEmptyIC}
-            text={device.getDisplayName() || device.deviceId}
-          />
-        )))}
+        {devices !== null &&
+          devices.map((device) => (
+            <Chip
+              key={device.deviceId}
+              iconSrc={ShieldEmptyIC}
+              text={device.getDisplayName() || device.deviceId}
+            />
+          ))}
       </div>
     );
   }
@@ -137,7 +137,11 @@ function SessionInfo({ userId }) {
         onClick={() => setIsVisible(!isVisible)}
         iconSrc={isVisible ? ChevronBottomIC : ChevronRightIC}
       >
-        <Text variant="b2">{`View ${devices?.length > 0 ? `${devices.length} ${devices.length == 1 ? 'session' : 'sessions'}` : 'sessions'}`}</Text>
+        <Text variant="b2">{`View ${
+          devices?.length > 0
+            ? `${devices.length} ${devices.length == 1 ? 'session' : 'sessions'}`
+            : 'sessions'
+        }`}</Text>
       </MenuItem>
       {renderSessionChips()}
     </div>
@@ -155,6 +159,7 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
 
   const isMountedRef = useRef(true);
   const mx = initMatrix.matrixClient;
+  const { navigateRoom } = useRoomNavigate();
   const room = mx.getRoom(roomId);
   const member = room.getMember(userId);
   const isInvitable = member?.membership !== 'join' && member?.membership !== 'ban';
@@ -164,25 +169,18 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
 
   const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
   const userPL = room.getMember(userId)?.powerLevel || 0;
-  const canIKick = room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
+  const canIKick =
+    room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
 
   const isBanned = member?.membership === 'ban';
 
   const onCreated = (dmRoomId) => {
     if (isMountedRef.current === false) return;
     setIsCreatingDM(false);
-    selectRoom(dmRoomId);
+    navigateRoom(dmRoomId);
     onRequestClose();
   };
 
-  useEffect(() => {
-    const { roomList } = initMatrix;
-    roomList.on(cons.events.roomList.ROOM_CREATED, onCreated);
-    return () => {
-      isMountedRef.current = false;
-      roomList.removeListener(cons.events.roomList.ROOM_CREATED, onCreated);
-    };
-  }, []);
   useEffect(() => {
     setIsUserIgnored(initMatrix.matrixClient.isUserIgnored(userId));
     setIsIgnoring(false);
@@ -191,9 +189,9 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
 
   const openDM = async () => {
     // Check and open if user already have a DM with userId.
-    const dmRoomId = hasDMWith(userId);
+    const dmRoomId = getDMRoomFor(mx, userId)?.roomId;
     if (dmRoomId) {
-      selectRoom(dmRoomId);
+      navigateRoom(dmRoomId);
       onRequestClose();
       return;
     }
@@ -201,7 +199,8 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
     // Create new DM
     try {
       setIsCreatingDM(true);
-      await roomActions.createDM(userId, await hasDevices(userId));
+      const result = await roomActions.createDM(userId, await hasDevices(userId));
+      onCreated(result.room_id);
     } catch {
       if (isMountedRef.current === false) return;
       setIsCreatingDM(false);
@@ -246,31 +245,19 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
 
   return (
     <div className="profile-viewer__buttons">
-      <Button
-        variant="primary"
-        onClick={openDM}
-        disabled={isCreatingDM}
-      >
+      <Button variant="primary" onClick={openDM} disabled={isCreatingDM}>
         {isCreatingDM ? 'Creating room...' : 'Message'}
       </Button>
-      { isBanned && canIKick && (
-        <Button
-          variant="positive"
-          onClick={() => roomActions.unban(roomId, userId)}
-        >
+      {isBanned && canIKick && (
+        <Button variant="positive" onClick={() => roomActions.unban(roomId, userId)}>
           Unban
         </Button>
       )}
-      { (isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
-        <Button
-          onClick={toggleInvite}
-          disabled={isInviting}
-        >
-          {
-            isInvited
-              ? `${isInviting ? 'Disinviting...' : 'Disinvite'}`
-              : `${isInviting ? 'Inviting...' : 'Invite'}`
-          }
+      {(isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
+        <Button onClick={toggleInvite} disabled={isInviting}>
+          {isInvited
+            ? `${isInviting ? 'Disinviting...' : 'Disinvite'}`
+            : `${isInviting ? 'Inviting...' : 'Invite'}`}
         </Button>
       )}
       <Button
@@ -278,11 +265,9 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
         onClick={toggleIgnore}
         disabled={isIgnoring}
       >
-        {
-          isUserIgnored
-            ? `${isIgnoring ? 'Unignoring...' : 'Unignore'}`
-            : `${isIgnoring ? 'Ignoring...' : 'Ignore'}`
-        }
+        {isUserIgnored
+          ? `${isIgnoring ? 'Unignoring...' : 'Unignore'}`
+          : `${isIgnoring ? 'Ignoring...' : 'Ignore'}`}
       </Button>
     </div>
   );
@@ -326,8 +311,8 @@ function useRerenderOnProfileChange(roomId, userId) {
   useEffect(() => {
     const handleProfileChange = (mEvent, member) => {
       if (
-        mEvent.getRoomId() === roomId
-        && (member.userId === userId || member.userId === mx.getUserId())
+        mEvent.getRoomId() === roomId &&
+        (member.userId === userId || member.userId === mx.getUserId())
       ) {
         forceUpdate();
       }
@@ -352,20 +337,22 @@ function ProfileViewer() {
     const roomMember = room.getMember(userId);
     const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(userId);
     const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl;
-    const avatarUrl = (avatarMxc && avatarMxc !== 'null') ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null;
+    const avatarUrl =
+      avatarMxc && avatarMxc !== 'null' ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null;
 
     const powerLevel = roomMember?.powerLevel || 0;
     const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
 
-    const canChangeRole = (
-      room.currentState.maySendEvent('m.room.power_levels', mx.getUserId())
-      && (powerLevel < myPowerLevel || userId === mx.getUserId())
-    );
+    const canChangeRole =
+      room.currentState.maySendEvent('m.room.power_levels', mx.getUserId()) &&
+      (powerLevel < myPowerLevel || userId === mx.getUserId());
 
     const handleChangePowerLevel = async (newPowerLevel) => {
       if (newPowerLevel === powerLevel) return;
-      const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
-      const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?';
+      const SHARED_POWER_MSG =
+        'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
+      const DEMOTING_MYSELF_MSG =
+        'You will not be able to undo this change as you are demoting yourself. Are you sure?';
 
       const isSharedPower = newPowerLevel === myPowerLevel;
       const isDemotingMyself = userId === mx.getUserId();
@@ -374,7 +361,7 @@ function ProfileViewer() {
           'Change power level',
           isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG,
           'Change',
-          'caution',
+          'caution'
         );
         if (!isConfirmed) return;
         roomActions.setPowerLevel(roomId, userId, newPowerLevel);
@@ -384,20 +371,16 @@ function ProfileViewer() {
     };
 
     const handlePowerSelector = (e) => {
-      openReusableContextMenu(
-        'bottom',
-        getEventCords(e, '.btn-surface'),
-        (closeMenu) => (
-          <PowerLevelSelector
-            value={powerLevel}
-            max={myPowerLevel}
-            onSelect={(pl) => {
-              closeMenu();
-              handleChangePowerLevel(pl);
-            }}
-          />
-        ),
-      );
+      openReusableContextMenu('bottom', getEventCords(e, '.btn-surface'), (closeMenu) => (
+        <PowerLevelSelector
+          value={powerLevel}
+          max={myPowerLevel}
+          onSelect={(pl) => {
+            closeMenu();
+            handleChangePowerLevel(pl);
+          }}
+        />
+      ));
     };
 
     return (
@@ -405,8 +388,10 @@ function ProfileViewer() {
         <div className="profile-viewer__user">
           <Avatar imageSrc={avatarUrl} text={username} bgColor={colorMXID(userId)} size="large" />
           <div className="profile-viewer__user__info">
-            <Text variant="s1" weight="medium">{twemojify(username)}</Text>
-            <Text variant="b2">{twemojify(userId)}</Text>
+            <Text variant="s1" weight="medium">
+              {username}
+            </Text>
+            <Text variant="b2">{userId}</Text>
           </div>
           <div className="profile-viewer__user__role">
             <Text variant="b3">Role</Text>
@@ -420,7 +405,7 @@ function ProfileViewer() {
         </div>
         <ModerationTools roomId={roomId} userId={userId} />
         <SessionInfo userId={userId} />
-        { userId !== mx.getUserId() && (
+        {userId !== mx.getUserId() && (
           <ProfileFooter roomId={roomId} userId={userId} onRequestClose={closeDialog} />
         )}
       </div>
diff --git a/src/app/organisms/public-rooms/PublicRooms.jsx b/src/app/organisms/public-rooms/PublicRooms.jsx
deleted file mode 100644 (file)
index d1674c3..0000000
+++ /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 (
-    <div className="try-join-with-alias">
-      {status.roomId === null && !status.isJoining && status.error === null && (
-        <Button onClick={() => joinWithAlias()}>{`Try joining ${alias}`}</Button>
-      )}
-      {status.isJoining && (
-        <>
-          <Spinner size="small" />
-          <Text>{`Joining ${alias}...`}</Text>
-        </>
-      )}
-      {status.roomId !== null && (
-        <Button onClick={() => { onRequestClose(); selectRoom(status.roomId); }}>Open</Button>
-      )}
-      {status.error !== null && <Text variant="b2"><span style={{ color: 'var(--bg-danger)' }}>{status.error}</span></Text>}
-    </div>
-  );
-}
-
-TryJoinWithAlias.propTypes = {
-  alias: PropTypes.string.isRequired,
-  onRequestClose: PropTypes.func.isRequired,
-};
-
-function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
-  const [isSearching, updateIsSearching] = useState(false);
-  const [isViewMore, updateIsViewMore] = useState(false);
-  const [publicRooms, updatePublicRooms] = useState([]);
-  const [nextBatch, updateNextBatch] = useState(undefined);
-  const [searchQuery, updateSearchQuery] = useState({});
-  const [joiningRooms, updateJoiningRooms] = useState(new Set());
-
-  const roomNameRef = useRef(null);
-  const hsRef = useRef(null);
-  const userId = initMatrix.matrixClient.getUserId();
-
-  async function searchRooms(viewMore) {
-    let inputRoomName = roomNameRef?.current?.value || searchTerm;
-    let isInputAlias = false;
-    if (typeof inputRoomName === 'string') {
-      isInputAlias = inputRoomName[0] === '#' && inputRoomName.indexOf(':') > 1;
-    }
-    const hsFromAlias = (isInputAlias) ? inputRoomName.slice(inputRoomName.indexOf(':') + 1) : null;
-    let inputHs = hsFromAlias || hsRef?.current?.value;
-
-    if (typeof inputHs !== 'string') inputHs = userId.slice(userId.indexOf(':') + 1);
-    if (typeof inputRoomName !== 'string') inputRoomName = '';
-
-    if (isSearching) return;
-    if (viewMore !== true
-      && inputRoomName === searchQuery.name
-      && inputHs === searchQuery.homeserver
-    ) return;
-
-    updateSearchQuery({
-      name: inputRoomName,
-      homeserver: inputHs,
-    });
-    if (isViewMore !== viewMore) updateIsViewMore(viewMore);
-    updateIsSearching(true);
-
-    try {
-      const result = await initMatrix.matrixClient.publicRooms({
-        server: inputHs,
-        limit: SEARCH_LIMIT,
-        since: viewMore ? nextBatch : undefined,
-        include_all_networks: true,
-        filter: {
-          generic_search_term: inputRoomName,
-        },
-      });
-
-      const totalRooms = viewMore ? publicRooms.concat(result.chunk) : result.chunk;
-      updatePublicRooms(totalRooms);
-      updateNextBatch(result.next_batch);
-      updateIsSearching(false);
-      updateIsViewMore(false);
-      if (totalRooms.length === 0) {
-        updateSearchQuery({
-          error: inputRoomName === ''
-            ? `No public rooms on ${inputHs}`
-            : `No result found for "${inputRoomName}" on ${inputHs}`,
-          alias: isInputAlias ? inputRoomName : null,
-        });
-      }
-    } catch (e) {
-      updatePublicRooms([]);
-      let err = 'Something went wrong!';
-      if (e?.httpStatus >= 400 && e?.httpStatus < 500) {
-        err = e.message;
-      }
-      updateSearchQuery({
-        error: err,
-        alias: isInputAlias ? inputRoomName : null,
-      });
-      updateIsSearching(false);
-      updateNextBatch(undefined);
-      updateIsViewMore(false);
-    }
-  }
-
-  useEffect(() => {
-    if (isOpen) searchRooms();
-  }, [isOpen]);
-
-  function handleOnRoomAdded(roomId) {
-    if (joiningRooms.has(roomId)) {
-      joiningRooms.delete(roomId);
-      updateJoiningRooms(new Set(Array.from(joiningRooms)));
-    }
-  }
-  useEffect(() => {
-    initMatrix.roomList.on(cons.events.roomList.ROOM_JOINED, handleOnRoomAdded);
-    return () => {
-      initMatrix.roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleOnRoomAdded);
-    };
-  }, [joiningRooms]);
-
-  function handleViewRoom(roomId) {
-    const room = initMatrix.matrixClient.getRoom(roomId);
-    if (room.isSpaceRoom()) selectTab(roomId);
-    else selectRoom(roomId);
-    onRequestClose();
-  }
-
-  function joinRoom(roomIdOrAlias) {
-    joiningRooms.add(roomIdOrAlias);
-    updateJoiningRooms(new Set(Array.from(joiningRooms)));
-    roomActions.join(roomIdOrAlias, false);
-  }
-
-  function renderRoomList(rooms) {
-    return rooms.map((room) => {
-      const alias = typeof room.canonical_alias === 'string' ? room.canonical_alias : room.room_id;
-      const name = typeof room.name === 'string' ? room.name : alias;
-      const isJoined = initMatrix.matrixClient.getRoom(room.room_id)?.getMyMembership() === 'join';
-      return (
-        <RoomTile
-          key={room.room_id}
-          avatarSrc={typeof room.avatar_url === 'string' ? initMatrix.matrixClient.mxcUrlToHttp(room.avatar_url, 42, 42, 'crop') : null}
-          name={name}
-          id={alias}
-          memberCount={room.num_joined_members}
-          desc={typeof room.topic === 'string' ? room.topic : null}
-          options={(
-            <>
-              {isJoined && <Button onClick={() => handleViewRoom(room.room_id)}>Open</Button>}
-              {!isJoined && (joiningRooms.has(room.room_id) ? <Spinner size="small" /> : <Button onClick={() => joinRoom(room.aliases?.[0] || room.room_id)} variant="primary">Join</Button>)}
-            </>
-          )}
-        />
-      );
-    });
-  }
-
-  return (
-    <PopupWindow
-      isOpen={isOpen}
-      title="Public rooms"
-      contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
-      onRequestClose={onRequestClose}
-    >
-      <div className="public-rooms">
-        <form className="public-rooms__form" onSubmit={(e) => { e.preventDefault(); searchRooms(); }}>
-          <div className="public-rooms__input-wrapper">
-            <Input value={searchTerm} forwardRef={roomNameRef} label="Room name or alias" />
-            <Input forwardRef={hsRef} value={userId.slice(userId.indexOf(':') + 1)} label="Homeserver" required />
-          </div>
-          <Button disabled={isSearching} iconSrc={HashSearchIC} variant="primary" type="submit">Search</Button>
-        </form>
-        <div className="public-rooms__search-status">
-          {
-            typeof searchQuery.name !== 'undefined' && isSearching && (
-              searchQuery.name === ''
-                ? (
-                  <div className="flex--center">
-                    <Spinner size="small" />
-                    <Text variant="b2">{`Loading public rooms from ${searchQuery.homeserver}...`}</Text>
-                  </div>
-                )
-                : (
-                  <div className="flex--center">
-                    <Spinner size="small" />
-                    <Text variant="b2">{`Searching for "${searchQuery.name}" on ${searchQuery.homeserver}...`}</Text>
-                  </div>
-                )
-            )
-          }
-          {
-            typeof searchQuery.name !== 'undefined' && !isSearching && (
-              searchQuery.name === ''
-                ? <Text variant="b2">{`Public rooms on ${searchQuery.homeserver}.`}</Text>
-                : <Text variant="b2">{`Search result for "${searchQuery.name}" on ${searchQuery.homeserver}.`}</Text>
-            )
-          }
-          { searchQuery.error && (
-            <>
-              <Text className="public-rooms__search-error" variant="b2">{searchQuery.error}</Text>
-              {typeof searchQuery.alias === 'string' && (
-                <TryJoinWithAlias onRequestClose={onRequestClose} alias={searchQuery.alias} />
-              )}
-            </>
-          )}
-        </div>
-        { publicRooms.length !== 0 && (
-          <div className="public-rooms__content">
-            { renderRoomList(publicRooms) }
-          </div>
-        )}
-        { publicRooms.length !== 0 && publicRooms.length % SEARCH_LIMIT === 0 && (
-          <div className="public-rooms__view-more">
-            { isViewMore !== true && (
-              <Button onClick={() => searchRooms(true)}>View more</Button>
-            )}
-            { isViewMore && <Spinner /> }
-          </div>
-        )}
-      </div>
-    </PopupWindow>
-  );
-}
-
-PublicRooms.defaultProps = {
-  searchTerm: undefined,
-};
-
-PublicRooms.propTypes = {
-  isOpen: PropTypes.bool.isRequired,
-  searchTerm: PropTypes.string,
-  onRequestClose: PropTypes.func.isRequired,
-};
-
-export default PublicRooms;
diff --git a/src/app/organisms/public-rooms/PublicRooms.scss b/src/app/organisms/public-rooms/PublicRooms.scss
deleted file mode 100644 (file)
index dc55c94..0000000
+++ /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
index a51d07e14d45e6d3bf8b1e89ce21c3874fe84dc3..cc77cf18e5f43dd4a8da49610845872813694629 100644 (file)
@@ -1,11 +1,8 @@
 import React from 'react';
 
-import ReadReceipts from '../read-receipts/ReadReceipts';
 import ProfileViewer from '../profile-viewer/ProfileViewer';
-import ShortcutSpaces from '../shortcut-spaces/ShortcutSpaces';
 import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExisting';
 import Search from '../search/Search';
-import ViewSource from '../view-source/ViewSource';
 import CreateRoom from '../create-room/CreateRoom';
 import JoinAlias from '../join-alias/JoinAlias';
 import EmojiVerification from '../emoji-verification/EmojiVerification';
@@ -15,10 +12,7 @@ import ReusableDialog from '../../molecules/dialog/ReusableDialog';
 function Dialogs() {
   return (
     <>
-      <ReadReceipts />
-      <ViewSource />
       <ProfileViewer />
-      <ShortcutSpaces />
       <CreateRoom />
       <JoinAlias />
       <SpaceAddExisting />
index 835b70334c243b27aea57657e070d5b19fa26b41..3ee997695d566c6ecb67a7855cf89a149a509dbd 100644 (file)
@@ -3,35 +3,18 @@ import React, { useState, useEffect } from 'react';
 import cons from '../../../client/state/cons';
 import navigation from '../../../client/state/navigation';
 
-import InviteList from '../invite-list/InviteList';
-import PublicRooms from '../public-rooms/PublicRooms';
 import InviteUser from '../invite-user/InviteUser';
 import Settings from '../settings/Settings';
 import SpaceSettings from '../space-settings/SpaceSettings';
-import SpaceManage from '../space-manage/SpaceManage';
 import RoomSettings from '../room/RoomSettings';
 
 function Windows() {
-  const [isInviteList, changeInviteList] = useState(false);
-  const [publicRooms, changePublicRooms] = useState({
-    isOpen: false,
-    searchTerm: undefined,
-  });
   const [inviteUser, changeInviteUser] = useState({
     isOpen: false,
     roomId: undefined,
     term: undefined,
   });
 
-  function openInviteList() {
-    changeInviteList(true);
-  }
-  function openPublicRooms(searchTerm) {
-    changePublicRooms({
-      isOpen: true,
-      searchTerm,
-    });
-  }
   function openInviteUser(roomId, searchTerm) {
     changeInviteUser({
       isOpen: true,
@@ -41,24 +24,14 @@ function Windows() {
   }
 
   useEffect(() => {
-    navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
-    navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
     navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
     return () => {
-      navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
-      navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
       navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
     };
   }, []);
 
   return (
     <>
-      <InviteList isOpen={isInviteList} onRequestClose={() => changeInviteList(false)} />
-      <PublicRooms
-        isOpen={publicRooms.isOpen}
-        searchTerm={publicRooms.searchTerm}
-        onRequestClose={() => changePublicRooms({ isOpen: false, searchTerm: undefined })}
-      />
       <InviteUser
         isOpen={inviteUser.isOpen}
         roomId={inviteUser.roomId}
@@ -68,7 +41,6 @@ function Windows() {
       <Settings />
       <SpaceSettings />
       <RoomSettings />
-      <SpaceManage />
     </>
   );
 }
diff --git a/src/app/organisms/read-receipts/ReadReceipts.jsx b/src/app/organisms/read-receipts/ReadReceipts.jsx
deleted file mode 100644 (file)
index 1e648e0..0000000
+++ /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 (
-      <PeopleSelector
-        key={userId}
-        onClick={() => {
-          setIsOpen(false);
-          openProfileViewer(userId, roomId);
-        }}
-        avatarSrc={member?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 24, 24, 'crop')}
-        name={getUserDisplayName(userId)}
-        color={colorMXID(userId)}
-      />
-    );
-  }
-
-  return (
-    <Dialog
-      isOpen={isOpen}
-      title="Seen by"
-      onAfterClose={handleAfterClose}
-      onRequestClose={() => setIsOpen(false)}
-      contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
-    >
-      <div style={{ marginTop: 'var(--sp-tight)', marginBottom: 'var(--sp-extra-loose)' }}>
-        {
-          readers.map(renderPeople)
-        }
-      </div>
-    </Dialog>
-  );
-}
-
-export default ReadReceipts;
diff --git a/src/app/organisms/room/EventLimit.js b/src/app/organisms/room/EventLimit.js
deleted file mode 100644 (file)
index de87da3..0000000
+++ /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 (file)
index 8f98324..0000000
+++ /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 (
-    <div className="people-drawer">
-      <Header>
-        <TitleWrapper>
-          <Text variant="s1" primary>
-            People
-            <Text className="people-drawer__member-count" variant="b3">{`${room.getJoinedMemberCount()} members`}</Text>
-          </Text>
-        </TitleWrapper>
-        <IconButton onClick={() => openInviteUser(roomId)} tooltip="Invite" src={AddUserIC} disabled={!canInvite} />
-      </Header>
-      <div className="people-drawer__content-wrapper">
-        <div className="people-drawer__scrollable">
-          <ScrollView autoHide>
-            <div className="people-drawer__content">
-              <SegmentedControl
-                selected={
-                  (() => {
-                    const getSegmentIndex = {
-                      join: 0,
-                      invite: 1,
-                      ban: 2,
-                    };
-                    return getSegmentIndex[membership];
-                  })()
-                }
-                segments={[{ text: 'Joined' }, { text: 'Invited' }, { text: 'Banned' }]}
-                onSelect={(index) => {
-                  const selectSegment = [
-                    () => setMembership('join'),
-                    () => setMembership('invite'),
-                    () => setMembership('ban'),
-                  ];
-                  selectSegment[index]?.();
-                }}
-              />
-              {
-                mList.map((member) => (
-                  <PeopleSelector
-                    key={member.userId}
-                    onClick={() => openProfileViewer(member.userId, roomId)}
-                    avatarSrc={member.avatarSrc}
-                    name={member.name}
-                    color={colorMXID(member.userId)}
-                    peopleRole={member.peopleRole}
-                  />
-                ))
-              }
-              {
-                (searchedMembers?.data.length === 0 || memberList.length === 0)
-                && (
-                  <div className="people-drawer__noresult">
-                    <Text variant="b2">No results found!</Text>
-                  </div>
-                )
-              }
-              <div className="people-drawer__load-more">
-                {
-                  mList.length !== 0
-                  && memberList.length > itemCount
-                  && searchedMembers === null
-                  && (
-                    <Button onClick={loadMorePeople}>View more</Button>
-                  )
-                }
-              </div>
-            </div>
-          </ScrollView>
-        </div>
-        <div className="people-drawer__sticky">
-          <form onSubmit={(e) => e.preventDefault()} className="people-search">
-            <RawIcon size="small" src={SearchIC} />
-            <Input forwardRef={searchRef} type="text" onChange={handleSearch} placeholder="Search" required />
-            {
-              searchedMembers !== null
-              && <IconButton onClick={handleSearch} size="small" src={CrossIC} />
-            }
-          </form>
-        </div>
-      </div>
-    </div>
-  );
-}
-
-PeopleDrawer.propTypes = {
-  roomId: PropTypes.string.isRequired,
-};
-
-export default PeopleDrawer;
diff --git a/src/app/organisms/room/PeopleDrawer.scss b/src/app/organisms/room/PeopleDrawer.scss
deleted file mode 100644 (file)
index cfc5f6c..0000000
+++ /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 (file)
index 69f8f9d..0000000
+++ /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;
-  }
-}
index 1e617ae7ee2879a4ef950182767ddf2bca23c5b9..2b8f28e66f7d77851be82808c75ae08aefa6d6a3 100644 (file)
@@ -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 (file)
index c70c2b0..0000000
+++ /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 (file)
index 0d21123..0000000
+++ /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 (
-    <button className="cmd-item" onClick={onClick} type="button">
-      {children}
-    </button>
-  );
-}
-CmdItem.propTypes = {
-  onClick: PropTypes.func.isRequired,
-  children: PropTypes.node.isRequired,
-};
-
-function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
-  function renderCmdSuggestions(cmdPrefix, cmds) {
-    const cmdOptString = typeof option === 'string' ? `/${option}` : '/?';
-    return cmds.map((cmd) => (
-      <CmdItem
-        key={cmd}
-        onClick={() => {
-          fireCmd({
-            prefix: cmdPrefix,
-            option,
-            result: commands[cmd],
-          });
-        }}
-      >
-        <Text variant="b2">{`${cmd}${cmd.isOptions ? cmdOptString : ''}`}</Text>
-      </CmdItem>
-    ));
-  }
-
-  function renderEmojiSuggestion(emPrefix, emos) {
-    const mx = initMatrix.matrixClient;
-
-    // Renders a small Twemoji
-    function renderTwemoji(emoji) {
-      return parse(
-        twemoji.parse(emoji.unicode, {
-          attributes: () => ({
-            unicode: emoji.unicode,
-            shortcodes: emoji.shortcodes?.toString(),
-          }),
-          base: TWEMOJI_BASE_URL,
-        })
-      );
-    }
-
-    // Render a custom emoji
-    function renderCustomEmoji(emoji) {
-      return (
-        <img
-          className="emoji"
-          src={mx.mxcUrlToHttp(emoji.mxc)}
-          data-mx-emoticon=""
-          alt={`:${emoji.shortcode}:`}
-        />
-      );
-    }
-
-    // Dynamically render either a custom emoji or twemoji based on what the input is
-    function renderEmoji(emoji) {
-      if (emoji.mxc) {
-        return renderCustomEmoji(emoji);
-      }
-      return renderTwemoji(emoji);
-    }
-
-    return emos.map((emoji) => (
-      <CmdItem
-        key={emoji.shortcode}
-        onClick={() =>
-          fireCmd({
-            prefix: emPrefix,
-            result: emoji,
-          })
-        }
-      >
-        <Text variant="b1">{renderEmoji(emoji)}</Text>
-        <Text variant="b2">{`:${emoji.shortcode}:`}</Text>
-      </CmdItem>
-    ));
-  }
-
-  function renderNameSuggestion(namePrefix, members) {
-    return members.map((member) => (
-      <CmdItem
-        key={member.userId}
-        onClick={() => {
-          fireCmd({
-            prefix: namePrefix,
-            result: member,
-          });
-        }}
-      >
-        <Text variant="b2">{twemojify(member.name)}</Text>
-      </CmdItem>
-    ));
-  }
-
-  const cmd = {
-    '/': (cmds) => renderCmdSuggestions(prefix, cmds),
-    ':': (emos) => renderEmojiSuggestion(prefix, emos),
-    '@': (members) => renderNameSuggestion(prefix, members),
-  };
-  return cmd[prefix]?.(suggestions);
-}
-
-const asyncSearch = new AsyncSearch();
-let cmdPrefix;
-let cmdOption;
-function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
-  const [cmd, setCmd] = useState(null);
-
-  function displaySuggestions(suggestions) {
-    if (suggestions.length === 0) {
-      setCmd({ prefix: cmd?.prefix || cmdPrefix, error: 'No suggestion found.' });
-      viewEvent.emit('cmd_error');
-      return;
-    }
-    setCmd({ prefix: cmd?.prefix || cmdPrefix, suggestions, option: cmdOption });
-  }
-
-  function processCmd(prefix, slug) {
-    let searchTerm = slug;
-    cmdOption = undefined;
-    cmdPrefix = prefix;
-    if (prefix === '/') {
-      const cmdSlugParts = slug.split('/');
-      [searchTerm, cmdOption] = cmdSlugParts;
-    }
-    if (prefix === ':') {
-      if (searchTerm.length <= 3) {
-        if (searchTerm.match(/^[-]?(\))$/)) searchTerm = 'smile';
-        else if (searchTerm.match(/^[-]?(s|S)$/)) searchTerm = 'confused';
-        else if (searchTerm.match(/^[-]?(o|O|0)$/)) searchTerm = 'astonished';
-        else if (searchTerm.match(/^[-]?(\|)$/)) searchTerm = 'neutral_face';
-        else if (searchTerm.match(/^[-]?(d|D)$/)) searchTerm = 'grin';
-        else if (searchTerm.match(/^[-]?(\/)$/)) searchTerm = 'frown';
-        else if (searchTerm.match(/^[-]?(p|P)$/)) searchTerm = 'stuck_out_tongue';
-        else if (searchTerm.match(/^'[-]?(\()$/)) searchTerm = 'cry';
-        else if (searchTerm.match(/^[-]?(x|X)$/)) searchTerm = 'dizzy_face';
-        else if (searchTerm.match(/^[-]?(\()$/)) searchTerm = 'pleading_face';
-        else if (searchTerm.match(/^[-]?(\$)$/)) searchTerm = 'money';
-        else if (searchTerm.match(/^(<3)$/)) searchTerm = 'heart';
-        else if (searchTerm.match(/^(c|ca|cat)$/)) searchTerm = '_cat';
-      }
-    }
-
-    asyncSearch.search(searchTerm);
-  }
-  function activateCmd(prefix) {
-    cmdPrefix = prefix;
-    cmdPrefix = undefined;
-
-    const mx = initMatrix.matrixClient;
-    const setupSearch = {
-      '/': () => {
-        asyncSearch.setup(Object.keys(commands), { isContain: true });
-        setCmd({ prefix, suggestions: Object.keys(commands) });
-      },
-      ':': () => {
-        const parentIds = initMatrix.roomList.getAllParentSpaces(roomId);
-        const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
-        const emojis = getEmojiForCompletion(mx, [mx.getRoom(roomId), ...parentRooms]);
-        const recentEmoji = getRecentEmojis(20);
-        asyncSearch.setup(emojis, { keys: ['shortcode'], isContain: true, limit: 20 });
-        setCmd({
-          prefix,
-          suggestions: recentEmoji.length > 0 ? recentEmoji : emojis.slice(26, 46),
-        });
-      },
-      '@': () => {
-        const members = mx
-          .getRoom(roomId)
-          .getJoinedMembers()
-          .map((member) => ({
-            name: member.name,
-            userId: member.userId.slice(1),
-          }));
-        asyncSearch.setup(members, { keys: ['name', 'userId'], limit: 20 });
-        const endIndex = members.length > 20 ? 20 : members.length;
-        setCmd({ prefix, suggestions: members.slice(0, endIndex) });
-      },
-    };
-    setupSearch[prefix]?.();
-  }
-  function deactivateCmd() {
-    setCmd(null);
-    cmdOption = undefined;
-    cmdPrefix = undefined;
-  }
-  function fireCmd(myCmd) {
-    if (myCmd.prefix === '/') {
-      viewEvent.emit('cmd_fired', {
-        replace: `/${myCmd.result.name}`,
-      });
-    }
-    if (myCmd.prefix === ':') {
-      if (!myCmd.result.mxc) addRecentEmoji(myCmd.result.unicode);
-      viewEvent.emit('cmd_fired', {
-        replace: myCmd.result.mxc ? `:${myCmd.result.shortcode}: ` : myCmd.result.unicode,
-      });
-    }
-    if (myCmd.prefix === '@') {
-      viewEvent.emit('cmd_fired', {
-        replace: `@${myCmd.result.userId}`,
-      });
-    }
-    deactivateCmd();
-  }
-
-  function listenKeyboard(event) {
-    const { activeElement } = document;
-    const lastCmdItem = document.activeElement.parentNode.lastElementChild;
-    if (event.key === 'Escape') {
-      if (activeElement.className !== 'cmd-item') return;
-      viewEvent.emit('focus_msg_input');
-    }
-    if (event.key === 'Tab') {
-      if (lastCmdItem.className !== 'cmd-item') return;
-      if (lastCmdItem !== activeElement) return;
-      if (event.shiftKey) return;
-      viewEvent.emit('focus_msg_input');
-      event.preventDefault();
-    }
-  }
-
-  useEffect(() => {
-    viewEvent.on('cmd_activate', activateCmd);
-    viewEvent.on('cmd_deactivate', deactivateCmd);
-    return () => {
-      deactivateCmd();
-      viewEvent.removeListener('cmd_activate', activateCmd);
-      viewEvent.removeListener('cmd_deactivate', deactivateCmd);
-    };
-  }, [roomId]);
-
-  useEffect(() => {
-    if (cmd !== null) document.body.addEventListener('keydown', listenKeyboard);
-    viewEvent.on('cmd_process', processCmd);
-    asyncSearch.on(asyncSearch.RESULT_SENT, displaySuggestions);
-    return () => {
-      if (cmd !== null) document.body.removeEventListener('keydown', listenKeyboard);
-
-      viewEvent.removeListener('cmd_process', processCmd);
-      asyncSearch.removeListener(asyncSearch.RESULT_SENT, displaySuggestions);
-    };
-  }, [cmd]);
-
-  const isError = typeof cmd?.error === 'string';
-  if (cmd === null || isError) {
-    return (
-      <div className="cmd-bar">
-        <FollowingMembers roomTimeline={roomTimeline} />
-      </div>
-    );
-  }
-
-  return (
-    <div className="cmd-bar">
-      <div className="cmd-bar__info">
-        <Text variant="b3">TAB</Text>
-      </div>
-      <div className="cmd-bar__content">
-        <ScrollView horizontal vertical={false} invisible>
-          <div className="cmd-bar__content-suggestions">{renderSuggestions(cmd, fireCmd)}</div>
-        </ScrollView>
-      </div>
-    </div>
-  );
-}
-RoomViewCmdBar.propTypes = {
-  roomId: PropTypes.string.isRequired,
-  roomTimeline: PropTypes.shape({}).isRequired,
-  viewEvent: PropTypes.shape({}).isRequired,
-};
-
-export default RoomViewCmdBar;
diff --git a/src/app/organisms/room/RoomViewCmdBar.scss b/src/app/organisms/room/RoomViewCmdBar.scss
deleted file mode 100644 (file)
index 3f03fb0..0000000
+++ /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 (file)
index 5726fe1..0000000
+++ /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(<PlaceholderMessage key={`placeholder-${i}${key}`} />);
-    }
-    return pl;
-  };
-
-  return (
-    <React.Fragment key={`placeholder-container${key}`}>
-      {genPlaceholders()}
-    </React.Fragment>
-  );
-}
-
-function RoomIntroContainer({ event, timeline }) {
-  const [, nameForceUpdate] = useForceUpdate();
-  const mx = initMatrix.matrixClient;
-  const { roomList } = initMatrix;
-  const { room } = timeline;
-  const roomTopic = room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
-  const isDM = roomList.directs.has(timeline.roomId);
-  let avatarSrc = room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop');
-  avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc;
-
-  const heading = isDM ? room.name : `Welcome to ${room.name}`;
-  const topic = twemojify(roomTopic || '', undefined, true);
-  const nameJsx = twemojify(room.name);
-  const desc = isDM
-    ? (
-      <>
-        This is the beginning of your direct message history with @
-        <b>{nameJsx}</b>
-        {'. '}
-        {topic}
-      </>
-    )
-    : (
-      <>
-        {'This is the beginning of the '}
-        <b>{nameJsx}</b>
-        {' room. '}
-        {topic}
-      </>
-    );
-
-  useEffect(() => {
-    const handleUpdate = () => nameForceUpdate();
-
-    roomList.on(cons.events.roomList.ROOM_PROFILE_UPDATED, handleUpdate);
-    return () => {
-      roomList.removeListener(cons.events.roomList.ROOM_PROFILE_UPDATED, handleUpdate);
-    };
-  }, []);
-
-  return (
-    <RoomIntro
-      roomId={timeline.roomId}
-      avatarSrc={avatarSrc}
-      name={room.name}
-      heading={twemojify(heading)}
-      desc={desc}
-      time={event ? `Created at ${dateFormat(event.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
-    />
-  );
-}
-
-function handleOnClickCapture(e) {
-  const { target, nativeEvent } = e;
-
-  const userId = target.getAttribute('data-mx-pill');
-  if (userId) {
-    const roomId = navigation.selectedRoomId;
-    openProfileViewer(userId, roomId);
-  }
-
-  const spoiler = nativeEvent.composedPath().find((el) => el?.hasAttribute?.('data-mx-spoiler'));
-  if (spoiler) {
-    if (!spoiler.classList.contains('data-mx-spoiler--visible')) e.preventDefault();
-    spoiler.classList.toggle('data-mx-spoiler--visible');
-  }
-}
-
-function renderEvent(
-  roomTimeline,
-  mEvent,
-  prevMEvent,
-  isFocus,
-  isEdit,
-  setEdit,
-  cancelEdit,
-) {
-  const isBodyOnly = (prevMEvent !== null
-    && prevMEvent.getSender() === mEvent.getSender()
-    && prevMEvent.getType() !== 'm.room.member'
-    && prevMEvent.getType() !== 'm.room.create'
-    && diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES
-  );
-  const timestamp = mEvent.getTs();
-
-  if (mEvent.getType() === 'm.room.member') {
-    const timelineChange = parseTimelineChange(mEvent);
-    if (timelineChange === null) return <div key={mEvent.getId()} />;
-    return (
-      <TimelineChange
-        key={mEvent.getId()}
-        variant={timelineChange.variant}
-        content={timelineChange.content}
-        timestamp={timestamp}
-      />
-    );
-  }
-  return (
-    <Message
-      key={mEvent.getId()}
-      mEvent={mEvent}
-      isBodyOnly={isBodyOnly}
-      roomTimeline={roomTimeline}
-      focus={isFocus}
-      fullTime={false}
-      isEdit={isEdit}
-      setEdit={setEdit}
-      cancelEdit={cancelEdit}
-    />
-  );
-}
-
-function useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef) {
-  const [timelineInfo, setTimelineInfo] = useState(null);
-
-  const setEventTimeline = async (eId) => {
-    if (typeof eId === 'string') {
-      const isLoaded = await roomTimeline.loadEventTimeline(eId);
-      if (isLoaded) return;
-      // if eventTimeline failed to load,
-      // we will load live timeline as fallback.
-    }
-    roomTimeline.loadLiveTimeline();
-  };
-
-  useEffect(() => {
-    const limit = eventLimitRef.current;
-    const initTimeline = (eId) => {
-      // NOTICE: eId can be id of readUpto, reply or specific event.
-      // readUpTo: when user click jump to unread message button.
-      // reply: when user click reply from timeline.
-      // specific event when user open a link of event. behave same as ^^^^
-      const readUpToId = roomTimeline.getReadUpToEventId();
-      let focusEventIndex = -1;
-      const isSpecificEvent = eId && eId !== readUpToId;
-
-      if (isSpecificEvent) {
-        focusEventIndex = roomTimeline.getEventIndex(eId);
-      }
-      if (!readUptoEvtStore.getItem() && roomTimeline.hasEventInTimeline(readUpToId)) {
-        // either opening live timeline or jump to unread.
-        readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
-      }
-      if (readUptoEvtStore.getItem() && !isSpecificEvent) {
-        focusEventIndex = roomTimeline.getUnreadEventIndex(readUptoEvtStore.getItem().getId());
-      }
-
-      if (focusEventIndex > -1) {
-        limit.setFrom(focusEventIndex - Math.round(limit.maxEvents / 2));
-      } else {
-        limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
-      }
-      setTimelineInfo({ focusEventId: isSpecificEvent ? eId : null });
-    };
-
-    roomTimeline.on(cons.events.roomTimeline.READY, initTimeline);
-    setEventTimeline(eventId);
-    return () => {
-      roomTimeline.removeListener(cons.events.roomTimeline.READY, initTimeline);
-      limit.setFrom(0);
-    };
-  }, [roomTimeline, eventId]);
-
-  return timelineInfo;
-}
-
-function usePaginate(
-  roomTimeline,
-  readUptoEvtStore,
-  forceUpdateLimit,
-  timelineScrollRef,
-  eventLimitRef,
-) {
-  const [info, setInfo] = useState(null);
-
-  useEffect(() => {
-    const handlePaginatedFromServer = (backwards, loaded) => {
-      const limit = eventLimitRef.current;
-      if (loaded === 0) return;
-      if (!readUptoEvtStore.getItem()) {
-        const readUpToId = roomTimeline.getReadUpToEventId();
-        readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
-      }
-      limit.paginate(backwards, PAG_LIMIT, roomTimeline.timeline.length);
-      setTimeout(() => setInfo({
-        backwards,
-        loaded,
-      }));
-    };
-    roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
-    return () => {
-      roomTimeline.removeListener(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
-    };
-  }, [roomTimeline]);
-
-  const autoPaginate = useCallback(async () => {
-    const timelineScroll = timelineScrollRef.current;
-    const limit = eventLimitRef.current;
-    if (roomTimeline.isOngoingPagination) return;
-    const tLength = roomTimeline.timeline.length;
-
-    if (timelineScroll.bottom < SCROLL_TRIGGER_POS) {
-      if (limit.length < tLength) {
-        // paginate from memory
-        limit.paginate(false, PAG_LIMIT, tLength);
-        forceUpdateLimit();
-      } else if (roomTimeline.canPaginateForward()) {
-        // paginate from server.
-        await roomTimeline.paginateTimeline(false, PAG_LIMIT);
-        return;
-      }
-    }
-    if (timelineScroll.top < SCROLL_TRIGGER_POS) {
-      if (limit.from > 0) {
-        // paginate from memory
-        limit.paginate(true, PAG_LIMIT, tLength);
-        forceUpdateLimit();
-      } else if (roomTimeline.canPaginateBackward()) {
-        // paginate from server.
-        await roomTimeline.paginateTimeline(true, PAG_LIMIT);
-      }
-    }
-  }, [roomTimeline]);
-
-  return [info, autoPaginate];
-}
-
-function useHandleScroll(
-  roomTimeline,
-  autoPaginate,
-  readUptoEvtStore,
-  forceUpdateLimit,
-  timelineScrollRef,
-  eventLimitRef,
-) {
-  const handleScroll = useCallback(() => {
-    const timelineScroll = timelineScrollRef.current;
-    const limit = eventLimitRef.current;
-    requestAnimationFrame(() => {
-      // emit event to toggle scrollToBottom button visibility
-      const isAtBottom = (
-        timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()
-        && limit.length >= roomTimeline.timeline.length
-      );
-      roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
-      if (isAtBottom && readUptoEvtStore.getItem()) {
-        requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
-      }
-    });
-    autoPaginate();
-  }, [roomTimeline]);
-
-  const handleScrollToLive = useCallback(() => {
-    const timelineScroll = timelineScrollRef.current;
-    const limit = eventLimitRef.current;
-    if (readUptoEvtStore.getItem()) {
-      requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
-    }
-    if (roomTimeline.isServingLiveTimeline()) {
-      limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
-      timelineScroll.scrollToBottom();
-      forceUpdateLimit();
-      return;
-    }
-    roomTimeline.loadLiveTimeline();
-  }, [roomTimeline]);
-
-  return [handleScroll, handleScrollToLive];
-}
-
-function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef) {
-  const myUserId = initMatrix.matrixClient.getUserId();
-  const [newEvent, setEvent] = useState(null);
-
-  useEffect(() => {
-    const timelineScroll = timelineScrollRef.current;
-    const limit = eventLimitRef.current;
-    const trySendReadReceipt = (event) => {
-      if (myUserId === event.getSender()) {
-        requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
-        return;
-      }
-      const readUpToEvent = readUptoEvtStore.getItem();
-      const readUpToId = roomTimeline.getReadUpToEventId();
-      const isUnread = readUpToEvent ? readUpToEvent?.getId() === readUpToId : true;
-
-      if (isUnread === false) {
-        if (document.visibilityState === 'visible' && timelineScroll.bottom < 16) {
-          requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
-        } else {
-          readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
-        }
-        return;
-      }
-
-      const { timeline } = roomTimeline;
-      const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToId;
-      if (unreadMsgIsLast) {
-        requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
-      }
-    };
-
-    const handleEvent = (event) => {
-      const tLength = roomTimeline.timeline.length;
-      const isViewingLive = roomTimeline.isServingLiveTimeline() && limit.length >= tLength - 1;
-      const isAttached = timelineScroll.bottom < SCROLL_TRIGGER_POS;
-
-      if (isViewingLive && isAttached && document.hasFocus()) {
-        limit.setFrom(tLength - limit.maxEvents);
-        trySendReadReceipt(event);
-        setEvent(event);
-        return;
-      }
-      const isRelates = (event.getType() === 'm.reaction' || event.getRelation()?.rel_type === 'm.replace');
-      if (isRelates) {
-        setEvent(event);
-        return;
-      }
-
-      if (isViewingLive) {
-        // This stateUpdate will help to put the
-        // loading msg placeholder at bottom
-        setEvent(event);
-      }
-    };
-
-    const handleEventRedact = (event) => setEvent(event);
-
-    roomTimeline.on(cons.events.roomTimeline.EVENT, handleEvent);
-    roomTimeline.on(cons.events.roomTimeline.EVENT_REDACTED, handleEventRedact);
-    return () => {
-      roomTimeline.removeListener(cons.events.roomTimeline.EVENT, handleEvent);
-      roomTimeline.removeListener(cons.events.roomTimeline.EVENT_REDACTED, handleEventRedact);
-    };
-  }, [roomTimeline]);
-
-  return newEvent;
-}
-
-let jumpToItemIndex = -1;
-
-function RoomViewContent({ roomInputRef, eventId, roomTimeline }) {
-  const [throttle] = useState(new Throttle());
-
-  const timelineSVRef = useRef(null);
-  const timelineScrollRef = useRef(null);
-  const eventLimitRef = useRef(null);
-  const [editEventId, setEditEventId] = useState(null);
-  const cancelEdit = () => setEditEventId(null);
-
-  const readUptoEvtStore = useStore(roomTimeline);
-  const [onLimitUpdate, forceUpdateLimit] = useForceUpdate();
-
-  const timelineInfo = useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef);
-  const [paginateInfo, autoPaginate] = usePaginate(
-    roomTimeline,
-    readUptoEvtStore,
-    forceUpdateLimit,
-    timelineScrollRef,
-    eventLimitRef,
-  );
-  const [handleScroll, handleScrollToLive] = useHandleScroll(
-    roomTimeline,
-    autoPaginate,
-    readUptoEvtStore,
-    forceUpdateLimit,
-    timelineScrollRef,
-    eventLimitRef,
-  );
-  const newEvent = useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef);
-
-  const { timeline } = roomTimeline;
-
-  useLayoutEffect(() => {
-    if (!roomTimeline.initialized) {
-      timelineScrollRef.current = new TimelineScroll(timelineSVRef.current);
-      eventLimitRef.current = new EventLimit();
-    }
-  });
-
-  // when active timeline changes
-  useEffect(() => {
-    if (!roomTimeline.initialized) return undefined;
-    const timelineScroll = timelineScrollRef.current;
-
-    if (timeline.length > 0) {
-      if (jumpToItemIndex === -1) {
-        timelineScroll.scrollToBottom();
-      } else {
-        timelineScroll.scrollToIndex(jumpToItemIndex, 80);
-      }
-      if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) {
-        const readUpToId = roomTimeline.getReadUpToEventId();
-        if (readUptoEvtStore.getItem()?.getId() === readUpToId || readUpToId === null) {
-          requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
-        }
-      }
-      jumpToItemIndex = -1;
-    }
-    autoPaginate();
-
-    roomTimeline.on(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
-    return () => {
-      if (timelineSVRef.current === null) return;
-      roomTimeline.removeListener(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
-    };
-  }, [timelineInfo]);
-
-  // when paginating from server
-  useEffect(() => {
-    if (!roomTimeline.initialized) return;
-    const timelineScroll = timelineScrollRef.current;
-    timelineScroll.tryRestoringScroll();
-    autoPaginate();
-  }, [paginateInfo]);
-
-  // when paginating locally
-  useEffect(() => {
-    if (!roomTimeline.initialized) return;
-    const timelineScroll = timelineScrollRef.current;
-    timelineScroll.tryRestoringScroll();
-  }, [onLimitUpdate]);
-
-  useEffect(() => {
-    const timelineScroll = timelineScrollRef.current;
-    if (!roomTimeline.initialized) return;
-    if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
-      timelineScroll.scrollToBottom();
-    } else {
-      timelineScroll.tryRestoringScroll();
-    }
-  }, [newEvent]);
-
-  useResizeObserver(
-    useCallback((entries) => {
-      if (!roomInputRef.current) return;
-      const editorBaseEntry = getResizeObserverEntry(roomInputRef.current, entries);
-      if (!editorBaseEntry) return;
-
-      const timelineScroll = timelineScrollRef.current;
-      if (!roomTimeline.initialized) return;
-      if (timelineScroll.bottom < 40 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
-        timelineScroll.scrollToBottom();
-      }
-    }, [roomInputRef]),
-    useCallback(() => roomInputRef.current, [roomInputRef]),
-  );
-  
-  const listenKeyboard = useCallback((event) => {
-    if (event.ctrlKey || event.altKey || event.metaKey) return;
-    if (event.key !== 'ArrowUp') return;
-    if (navigation.isRawModalVisible) return;
-
-    if (document.activeElement.id !== 'message-textarea') return;
-    if (document.activeElement.value !== '') return;
-
-    const {
-      timeline: tl, activeTimeline, liveTimeline, matrixClient: mx,
-    } = roomTimeline;
-    const limit = eventLimitRef.current;
-    if (activeTimeline !== liveTimeline) return;
-    if (tl.length > limit.length) return;
-
-    const mTypes = ['m.text'];
-    for (let i = tl.length - 1; i >= 0; i -= 1) {
-      const mE = tl[i];
-      if (
-        mE.getSender() === mx.getUserId()
-        && mE.getType() === 'm.room.message'
-        && mTypes.includes(mE.getContent()?.msgtype)
-      ) {
-        setEditEventId(mE.getId());
-        return;
-      }
-    }
-  }, [roomTimeline]);
-
-  useEffect(() => {
-    document.body.addEventListener('keydown', listenKeyboard);
-    return () => {
-      document.body.removeEventListener('keydown', listenKeyboard);
-    };
-  }, [listenKeyboard]);
-
-  const handleTimelineScroll = (event) => {
-    const timelineScroll = timelineScrollRef.current;
-    if (!event.target) return;
-
-    throttle._(() => {
-      const backwards = timelineScroll?.calcScroll();
-      if (typeof backwards !== 'boolean') return;
-      handleScroll(backwards);
-    }, 200)();
-  };
-
-  const renderTimeline = () => {
-    const tl = [];
-    const limit = eventLimitRef.current;
-
-    let itemCountIndex = 0;
-    jumpToItemIndex = -1;
-    const readUptoEvent = readUptoEvtStore.getItem();
-    let unreadDivider = false;
-
-    if (roomTimeline.canPaginateBackward() || limit.from > 0) {
-      tl.push(loadingMsgPlaceholders(1, PLACEHOLDER_COUNT));
-      itemCountIndex += PLACEHOLDER_COUNT;
-    }
-    for (let i = limit.from; i < limit.length; i += 1) {
-      if (i >= timeline.length) break;
-      const mEvent = timeline[i];
-      const prevMEvent = timeline[i - 1] ?? null;
-
-      if (i === 0 && !roomTimeline.canPaginateBackward()) {
-        if (mEvent.getType() === 'm.room.create') {
-          tl.push(
-            <RoomIntroContainer key={mEvent.getId()} event={mEvent} timeline={roomTimeline} />,
-          );
-          itemCountIndex += 1;
-          // eslint-disable-next-line no-continue
-          continue;
-        } else {
-          tl.push(<RoomIntroContainer key="room-intro" event={null} timeline={roomTimeline} />);
-          itemCountIndex += 1;
-        }
-      }
-
-      let isNewEvent = false;
-      if (!unreadDivider) {
-        unreadDivider = (readUptoEvent
-          && prevMEvent?.getTs() <= readUptoEvent.getTs()
-          && readUptoEvent.getTs() < mEvent.getTs());
-        if (unreadDivider) {
-          isNewEvent = true;
-          tl.push(<Divider key={`new-${mEvent.getId()}`} variant="positive" text="New messages" />);
-          itemCountIndex += 1;
-          if (jumpToItemIndex === -1) jumpToItemIndex = itemCountIndex;
-        }
-      }
-      const dayDivider = prevMEvent && !isInSameDay(mEvent.getDate(), prevMEvent.getDate());
-      if (dayDivider) {
-        tl.push(<Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />);
-        itemCountIndex += 1;
-      }
-
-      const focusId = timelineInfo.focusEventId;
-      const isFocus = focusId === mEvent.getId();
-      if (isFocus) jumpToItemIndex = itemCountIndex;
-
-      tl.push(renderEvent(
-        roomTimeline,
-        mEvent,
-        isNewEvent ? null : prevMEvent,
-        isFocus,
-        editEventId === mEvent.getId(),
-        setEditEventId,
-        cancelEdit,
-      ));
-      itemCountIndex += 1;
-    }
-    if (roomTimeline.canPaginateForward() || limit.length < timeline.length) {
-      tl.push(loadingMsgPlaceholders(2, PLACEHOLDER_COUNT));
-    }
-
-    return tl;
-  };
-
-  return (
-    <ScrollView onScroll={handleTimelineScroll} ref={timelineSVRef} autoHide>
-      <div className="room-view__content" onClick={handleOnClickCapture}>
-        <div className="timeline__wrapper">
-          { roomTimeline.initialized ? renderTimeline() : loadingMsgPlaceholders('loading', 3) }
-        </div>
-      </div>
-    </ScrollView>
-  );
-}
-
-RoomViewContent.defaultProps = {
-  eventId: null,
-};
-RoomViewContent.propTypes = {
-  eventId: PropTypes.string,
-  roomTimeline: PropTypes.shape({}).isRequired,
-  roomInputRef: PropTypes.shape({
-    current: PropTypes.shape({})
-  }).isRequired
-};
-
-export default RoomViewContent;
diff --git a/src/app/organisms/room/RoomViewContent.scss b/src/app/organisms/room/RoomViewContent.scss
deleted file mode 100644 (file)
index 1afd187..0000000
+++ /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 (file)
index d027aff..0000000
+++ /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 (
-    <>
-      <div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}>
-        <Button iconSrc={MessageUnreadIC} onClick={jumpToEvent} variant="primary">
-          <Text variant="b3" weight="medium">Jump to unread messages</Text>
-        </Button>
-        <Button iconSrc={TickMarkIC} onClick={cancelJumpToEvent} variant="primary">
-          <Text variant="b3" weight="bold">Mark as read</Text>
-        </Button>
-      </div>
-      <div className={`room-view__typing${typingMembers.size > 0 ? ' room-view__typing--open' : ''}`}>
-        <div className="bouncing-loader"><div /></div>
-        <Text variant="b2">{getUsersActionJsx(roomId, [...typingMembers], 'typing...')}</Text>
-      </div>
-      <div className={`room-view__STB${isAtBottom ? '' : ' room-view__STB--open'}`}>
-        <Button iconSrc={MessageIC} onClick={handleScrollToBottom}>
-          <Text variant="b3" weight="medium">Jump to latest</Text>
-        </Button>
-      </div>
-    </>
-  );
-}
-RoomViewFloating.propTypes = {
-  roomId: PropTypes.string.isRequired,
-  roomTimeline: PropTypes.shape({}).isRequired,
-};
-
-export default RoomViewFloating;
diff --git a/src/app/organisms/room/RoomViewFloating.scss b/src/app/organisms/room/RoomViewFloating.scss
deleted file mode 100644 (file)
index 7580217..0000000
+++ /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 (file)
index 6571241..0000000
+++ /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) => (
-      <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />
-    ));
-  };
-
-  return (
-    <Header>
-      <IconButton
-        src={BackArrowIC}
-        className="room-header__back-btn"
-        tooltip="Return to navigation"
-        onClick={() => openNavigation()}
-      />
-      <button
-        ref={roomHeaderBtnRef}
-        className="room-header__btn"
-        onClick={() => toggleRoomSettings()}
-        type="button"
-        onMouseUp={(e) => blurOnBubbling(e, '.room-header__btn')}
-      >
-        <Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
-        <TitleWrapper>
-          <Text variant="h2" weight="medium" primary>
-            {twemojify(roomName)}
-          </Text>
-        </TitleWrapper>
-        <RawIcon src={ChevronBottomIC} />
-      </button>
-      {mx.isRoomEncrypted(roomId) === false && (
-        <IconButton
-          onClick={() => toggleRoomSettings(tabText.SEARCH)}
-          tooltip="Search"
-          src={SearchIC}
-        />
-      )}
-      <IconButton
-        className="room-header__drawer-btn"
-        onClick={() => {
-          setPeopleDrawer((t) => !t);
-        }}
-        tooltip="People"
-        src={UserIC}
-      />
-      <IconButton
-        className="room-header__members-btn"
-        onClick={() => toggleRoomSettings(tabText.MEMBERS)}
-        tooltip="Members"
-        src={UserIC}
-      />
-      <IconButton onClick={openRoomOptions} tooltip="Options" src={VerticalMenuIC} />
-    </Header>
-  );
-}
-RoomViewHeader.propTypes = {
-  roomId: PropTypes.string.isRequired,
-};
-
-export default RoomViewHeader;
diff --git a/src/app/organisms/room/RoomViewHeader.scss b/src/app/organisms/room/RoomViewHeader.scss
deleted file mode 100644 (file)
index fc19c06..0000000
+++ /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 (file)
index 3fb780a..0000000
+++ /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 (
-        <Text className="room-input__alert">
-          {
-            tombstoneEvent
-              ? tombstoneEvent.getContent()?.body ?? 'This room has been replaced and is no longer active.'
-              : 'You do not have permission to post to this room'
-          }
-        </Text>
-      );
-    }
-    return (
-      <>
-        <div className={`room-input__option-container${attachment === null ? '' : ' room-attachment__option'}`}>
-          <input onChange={uploadFileChange} style={{ display: 'none' }} ref={uploadInputRef} type="file" />
-          <IconButton onClick={handleUploadClick} tooltip={attachment === null ? 'Upload' : 'Cancel'} src={CirclePlusIC} />
-        </div>
-        <div ref={inputBaseRef} className="room-input__input-container">
-          {roomTimeline.isEncrypted() && <RawIcon size="extra-small" src={ShieldIC} />}
-          <ScrollView autoHide>
-            <Text className="room-input__textarea-wrapper">
-              <TextareaAutosize
-                dir="auto"
-                id="message-textarea"
-                ref={textAreaRef}
-                onChange={handleMsgTyping}
-                onPaste={handlePaste}
-                onKeyDown={handleKeyDown}
-                placeholder="Send a message..."
-              />
-            </Text>
-          </ScrollView>
-        </div>
-        <div ref={rightOptionsRef} className="room-input__option-container">
-          <IconButton
-            onClick={(e) => {
-              openReusableContextMenu(
-                'top',
-                (() => {
-                  const cords = getEventCords(e);
-                  cords.y -= 20;
-                  return cords;
-                })(),
-                (closeMenu) => (
-                  <StickerBoard
-                    roomId={roomId}
-                    onSelect={(data) => {
-                      handleSendSticker(data);
-                      closeMenu();
-                    }}
-                  />
-                ),
-              );
-            }}
-            tooltip="Sticker"
-            src={StickerIC}
-          />
-          <IconButton
-            onClick={(e) => {
-              const cords = getEventCords(e);
-              cords.x += (document.dir === 'rtl' ? -80 : 80);
-              cords.y -= 250;
-              openEmojiBoard(cords, addEmoji);
-            }}
-            tooltip="Emoji"
-            src={EmojiIC}
-          />
-          <IconButton onClick={sendMessage} tooltip="Send" src={SendIC} />
-        </div>
-      </>
-    );
-  }
-
-  function attachFile() {
-    const fileType = attachment.type.slice(0, attachment.type.indexOf('/'));
-    return (
-      <div className="room-attachment">
-        <div className={`room-attachment__preview${fileType !== 'image' ? ' room-attachment__icon' : ''}`}>
-          {fileType === 'image' && <img alt={attachment.name} src={URL.createObjectURL(attachment)} />}
-          {fileType === 'video' && <RawIcon src={VLCIC} />}
-          {fileType === 'audio' && <RawIcon src={VolumeFullIC} />}
-          {fileType !== 'image' && fileType !== 'video' && fileType !== 'audio' && <RawIcon src={FileIC} />}
-        </div>
-        <div className="room-attachment__info">
-          <Text variant="b1">{attachment.name}</Text>
-          <Text variant="b3"><span ref={uploadProgressRef}>{`size: ${bytesToSize(attachment.size)}`}</span></Text>
-        </div>
-      </div>
-    );
-  }
-
-  function attachReply() {
-    return (
-      <div className="room-reply">
-        <IconButton
-          onClick={() => {
-            roomsInput.cancelReplyTo(roomId);
-            setReplyTo(null);
-          }}
-          src={CrossIC}
-          tooltip="Cancel reply"
-          size="extra-small"
-        />
-        <MessageReply
-          userId={replyTo.userId}
-          onKeyDown={handleKeyDown}
-          name={getUsername(replyTo.userId)}
-          color={colorMXID(replyTo.userId)}
-          body={replyTo.body}
-        />
-      </div>
-    );
-  }
-
-  return (
-    <>
-      { replyTo !== null && attachReply()}
-      { attachment !== null && attachFile() }
-      <form className="room-input" onSubmit={(e) => { e.preventDefault(); }}>
-        {
-          renderInputs()
-        }
-      </form>
-    </>
-  );
-}
-RoomViewInput.propTypes = {
-  roomId: PropTypes.string.isRequired,
-  roomTimeline: PropTypes.shape({}).isRequired,
-  viewEvent: PropTypes.shape({}).isRequired,
-};
-
-export default RoomViewInput;
diff --git a/src/app/organisms/room/RoomViewInput.scss b/src/app/organisms/room/RoomViewInput.scss
deleted file mode 100644 (file)
index 9fb7c4d..0000000
+++ /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 (file)
index ccdc9a9..0000000
+++ /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 (file)
index 463f9d9..0000000
+++ /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(
-    <Text variant="s1" weight="medium">Commands</Text>,
-    () => (
-      <div className="commands-dialog">
-        {Object.keys(commands).map((cmdName) => (
-          <SettingTile
-            key={cmdName}
-            title={cmdName}
-            content={<Text variant="b3">{commands[cmdName].description}</Text>}
-          />
-        ))}
-      </div>
-    ),
-  );
-}
-
-export default commands;
diff --git a/src/app/organisms/room/commands.scss b/src/app/organisms/room/commands.scss
deleted file mode 100644 (file)
index 6283937..0000000
+++ /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 (file)
index 28974a8..0000000
+++ /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 (
-        <>
-          <b>{twemojify(user)}</b>
-          {' joined the room'}
-        </>
-      );
-    },
-    leave(user, reason) {
-      const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
-      return (
-        <>
-          <b>{twemojify(user)}</b>
-          {' left the room'}
-          {twemojify(reasonMsg)}
-        </>
-      );
-    },
-    invite(inviter, user) {
-      return (
-        <>
-          <b>{twemojify(inviter)}</b>
-          {' invited '}
-          <b>{twemojify(user)}</b>
-        </>
-      );
-    },
-    cancelInvite(inviter, user) {
-      return (
-        <>
-          <b>{twemojify(inviter)}</b>
-          {' canceled '}
-          <b>{twemojify(user)}</b>
-          {'\'s invite'}
-        </>
-      );
-    },
-    rejectInvite(user) {
-      return (
-        <>
-          <b>{twemojify(user)}</b>
-          {' rejected the invitation'}
-        </>
-      );
-    },
-    kick(actor, user, reason) {
-      const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
-      return (
-        <>
-          <b>{twemojify(actor)}</b>
-          {' kicked '}
-          <b>{twemojify(user)}</b>
-          {twemojify(reasonMsg)}
-        </>
-      );
-    },
-    ban(actor, user, reason) {
-      const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
-      return (
-        <>
-          <b>{twemojify(actor)}</b>
-          {' banned '}
-          <b>{twemojify(user)}</b>
-          {twemojify(reasonMsg)}
-        </>
-      );
-    },
-    unban(actor, user) {
-      return (
-        <>
-          <b>{twemojify(actor)}</b>
-          {' unbanned '}
-          <b>{twemojify(user)}</b>
-        </>
-      );
-    },
-    avatarSets(user) {
-      return (
-        <>
-          <b>{twemojify(user)}</b>
-          {' set a avatar'}
-        </>
-      );
-    },
-    avatarChanged(user) {
-      return (
-        <>
-          <b>{twemojify(user)}</b>
-          {' changed their avatar'}
-        </>
-      );
-    },
-    avatarRemoved(user) {
-      return (
-        <>
-          <b>{twemojify(user)}</b>
-          {' removed their avatar'}
-        </>
-      );
-    },
-    nameSets(user, newName) {
-      return (
-        <>
-          <b>{twemojify(user)}</b>
-          {' set display name to '}
-          <b>{twemojify(newName)}</b>
-        </>
-      );
-    },
-    nameChanged(user, newName) {
-      return (
-        <>
-          <b>{twemojify(user)}</b>
-          {' changed their display name to '}
-          <b>{twemojify(newName)}</b>
-        </>
-      );
-    },
-    nameRemoved(user, lastName) {
-      return (
-        <>
-          <b>{twemojify(user)}</b>
-          {' removed their display name '}
-          <b>{twemojify(lastName)}</b>
-        </>
-      );
-    },
-  };
-}
-
-function getUsersActionJsx(roomId, userIds, actionStr) {
-  const room = initMatrix.matrixClient.getRoom(roomId);
-  const getUserDisplayName = (userId) => {
-    if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
-    return getUsername(userId);
-  };
-  const getUserJSX = (userId) => <b>{twemojify(getUserDisplayName(userId))}</b>;
-  if (!Array.isArray(userIds)) return 'Idle';
-  if (userIds.length === 0) return 'Idle';
-  const MAX_VISIBLE_COUNT = 3;
-
-  const u1Jsx = getUserJSX(userIds[0]);
-  // eslint-disable-next-line react/jsx-one-expression-per-line
-  if (userIds.length === 1) return <>{u1Jsx} is {actionStr}</>;
-
-  const u2Jsx = getUserJSX(userIds[1]);
-  // eslint-disable-next-line react/jsx-one-expression-per-line
-  if (userIds.length === 2) return <>{u1Jsx} and {u2Jsx} are {actionStr}</>;
-
-  const u3Jsx = getUserJSX(userIds[2]);
-  if (userIds.length === 3) {
-    // eslint-disable-next-line react/jsx-one-expression-per-line
-    return <>{u1Jsx}, {u2Jsx} and {u3Jsx} are {actionStr}</>;
-  }
-
-  const othersCount = userIds.length - MAX_VISIBLE_COUNT;
-  // eslint-disable-next-line react/jsx-one-expression-per-line
-  return <>{u1Jsx}, {u2Jsx}, {u3Jsx} and {othersCount} others are {actionStr}</>;
-}
-
-function parseTimelineChange(mEvent) {
-  const tJSXMsgs = getTimelineJSXMessages();
-  const makeReturnObj = (variant, content) => ({
-    variant,
-    content,
-  });
-  const content = mEvent.getContent();
-  const prevContent = mEvent.getPrevContent();
-  const sender = mEvent.getSender();
-  const senderName = getUsername(sender);
-  const userName = getUsername(mEvent.getStateKey());
-
-  switch (content.membership) {
-    case 'invite': return makeReturnObj('invite', tJSXMsgs.invite(senderName, userName));
-    case 'ban': return makeReturnObj('leave', tJSXMsgs.ban(senderName, userName, content.reason));
-    case 'join':
-      if (prevContent.membership === 'join') {
-        if (content.displayname !== prevContent.displayname) {
-          if (typeof content.displayname === 'undefined') return makeReturnObj('avatar', tJSXMsgs.nameRemoved(sender, prevContent.displayname));
-          if (typeof prevContent.displayname === 'undefined') return makeReturnObj('avatar', tJSXMsgs.nameSets(sender, content.displayname));
-          return makeReturnObj('avatar', tJSXMsgs.nameChanged(prevContent.displayname, content.displayname));
-        }
-        if (content.avatar_url !== prevContent.avatar_url) {
-          if (typeof content.avatar_url === 'undefined') return makeReturnObj('avatar', tJSXMsgs.avatarRemoved(content.displayname));
-          if (typeof prevContent.avatar_url === 'undefined') return makeReturnObj('avatar', tJSXMsgs.avatarSets(content.displayname));
-          return makeReturnObj('avatar', tJSXMsgs.avatarChanged(content.displayname));
-        }
-        return null;
-      }
-      return makeReturnObj('join', tJSXMsgs.join(senderName));
-    case 'leave':
-      if (sender === mEvent.getStateKey()) {
-        switch (prevContent.membership) {
-          case 'invite': return makeReturnObj('invite-cancel', tJSXMsgs.rejectInvite(senderName));
-          default: return makeReturnObj('leave', tJSXMsgs.leave(senderName, content.reason));
-        }
-      }
-      switch (prevContent.membership) {
-        case 'invite': return makeReturnObj('invite-cancel', tJSXMsgs.cancelInvite(senderName, userName));
-        case 'ban': return makeReturnObj('other', tJSXMsgs.unban(senderName, userName));
-        // sender is not target and made the target leave,
-        // if not from invite/ban then this is a kick
-        default: return makeReturnObj('leave', tJSXMsgs.kick(senderName, userName, content.reason));
-      }
-    default: return null;
-  }
-}
-
-export {
-  getTimelineJSXMessages,
-  getUsersActionJsx,
-  parseTimelineChange,
-};
index 66b6851149438358024d780252acfba89689dd8a..c9d1d991f72011775f153fed5d58a9519bec75a6 100644 (file)
@@ -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)}
       />
     );
index 563e31522d0b51ed8596854c1bf8c0360609c38e..9d848d5ae3b3e130fb1429baca3bc30abf21ddba 100644 (file)
@@ -3,7 +3,6 @@ import React, { useState } from 'react';
 import './CrossSigning.scss';
 import FileSaver from 'file-saver';
 import { Formik } from 'formik';
-import { twemojify } from '../../../util/twemojify';
 
 import initMatrix from '../../../client/initMatrix';
 import { openReusableDialog } from '../../../client/action/navigation';
@@ -22,15 +21,17 @@ import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
 const failedDialog = () => {
   const renderFailure = (requestClose) => (
     <div className="cross-signing__failure">
-      <Text variant="h1">{twemojify('❌')}</Text>
+      <Text variant="h1"></Text>
       <Text weight="medium">Failed to setup cross signing. Please try again.</Text>
       <Button onClick={requestClose}>Close</Button>
     </div>
   );
 
   openReusableDialog(
-    <Text variant="s1" weight="medium">Setup cross signing</Text>,
-    renderFailure,
+    <Text variant="s1" weight="medium">
+      Setup cross signing
+    </Text>,
+    renderFailure
   );
 };
 
@@ -48,11 +49,11 @@ const securityKeyDialog = (key) => {
   const renderSecurityKey = () => (
     <div className="cross-signing__key">
       <Text weight="medium">Please save this security key somewhere safe.</Text>
-      <Text className="cross-signing__key-text">
-        {key.encodedPrivateKey}
-      </Text>
+      <Text className="cross-signing__key-text">{key.encodedPrivateKey}</Text>
       <div className="cross-signing__key-btn">
-        <Button variant="primary" onClick={() => copyKey(key)}>Copy</Button>
+        <Button variant="primary" onClick={() => copyKey(key)}>
+          Copy
+        </Button>
         <Button onClick={() => downloadKey(key)}>Download</Button>
       </div>
     </div>
@@ -62,8 +63,10 @@ const securityKeyDialog = (key) => {
   downloadKey();
 
   openReusableDialog(
-    <Text variant="s1" weight="medium">Security Key</Text>,
-    () => renderSecurityKey(),
+    <Text variant="s1" weight="medium">
+      Security Key
+    </Text>,
+    () => renderSecurityKey()
   );
 };
 
@@ -112,7 +115,7 @@ function CrossSigningSetup() {
       errors.phrase = 'Phrase must contain 8-127 characters with no space.';
     }
     if (values.confirmPhrase.length > 0 && values.confirmPhrase !== values.phrase) {
-      errors.confirmPhrase = 'Phrase don\'t match.';
+      errors.confirmPhrase = "Phrase don't match.";
     }
     return errors;
   };
@@ -121,10 +124,14 @@ function CrossSigningSetup() {
     <div className="cross-signing__setup">
       <div className="cross-signing__setup-entry">
         <Text>
-          We will generate a <b>Security Key</b>, 
-          which you can use to manage messages backup and session verification.
+          We will generate a <b>Security Key</b>, which you can use to manage messages backup and
+          session verification.
         </Text>
-        {genWithPhrase !== false && <Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>Generate Key</Button>}
+        {genWithPhrase !== false && (
+          <Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>
+            Generate Key
+          </Button>
+        )}
         {genWithPhrase === false && <Spinner size="small" />}
       </div>
       <Text className="cross-signing__setup-divider">OR</Text>
@@ -133,9 +140,7 @@ function CrossSigningSetup() {
         onSubmit={(values) => setup(values.phrase)}
         validate={validator}
       >
-        {({
-          values, errors, handleChange, handleSubmit,
-        }) => (
+        {({ values, errors, handleChange, handleSubmit }) => (
           <form
             className="cross-signing__setup-entry"
             onSubmit={handleSubmit}
@@ -143,8 +148,8 @@ function CrossSigningSetup() {
           >
             <Text>
               Alternatively you can also set a <b>Security Phrase </b>
-              so you don't have to remember long Security Key, 
-              and optionally save the Key as backup.
+              so you don't have to remember long Security Key, and optionally save the Key as
+              backup.
             </Text>
             <Input
               name="phrase"
@@ -155,7 +160,11 @@ function CrossSigningSetup() {
               required
               disabled={genWithPhrase !== undefined}
             />
-            {errors.phrase && <Text variant="b3" className="cross-signing__error">{errors.phrase}</Text>}
+            {errors.phrase && (
+              <Text variant="b3" className="cross-signing__error">
+                {errors.phrase}
+              </Text>
+            )}
             <Input
               name="confirmPhrase"
               value={values.confirmPhrase}
@@ -165,8 +174,16 @@ function CrossSigningSetup() {
               required
               disabled={genWithPhrase !== undefined}
             />
-            {errors.confirmPhrase && <Text variant="b3" className="cross-signing__error">{errors.confirmPhrase}</Text>}
-            {genWithPhrase !== true && <Button variant="primary" type="submit" disabled={genWithPhrase !== undefined}>Set Phrase & Generate Key</Button>}
+            {errors.confirmPhrase && (
+              <Text variant="b3" className="cross-signing__error">
+                {errors.confirmPhrase}
+              </Text>
+            )}
+            {genWithPhrase !== true && (
+              <Button variant="primary" type="submit" disabled={genWithPhrase !== undefined}>
+                Set Phrase & Generate Key
+              </Button>
+            )}
             {genWithPhrase === true && <Spinner size="small" />}
           </form>
         )}
@@ -177,31 +194,36 @@ function CrossSigningSetup() {
 
 const setupDialog = () => {
   openReusableDialog(
-    <Text variant="s1" weight="medium">Setup cross signing</Text>,
-    () => <CrossSigningSetup />,
+    <Text variant="s1" weight="medium">
+      Setup cross signing
+    </Text>,
+    () => <CrossSigningSetup />
   );
 };
 
 function CrossSigningReset() {
   return (
     <div className="cross-signing__reset">
-      <Text variant="h1">{twemojify('✋🧑‍🚒🤚')}</Text>
+      <Text variant="h1">✋🧑‍🚒🤚</Text>
       <Text weight="medium">Resetting cross-signing keys is permanent.</Text>
       <Text>
-        Anyone you have verified with will see security alerts and your message backup will be lost. 
-        You almost certainly do not want to do this, 
-        unless you have lost <b>Security Key</b> or <b>Phrase</b> and 
-        every session you can cross-sign from.
+        Anyone you have verified with will see security alerts and your message backup will be lost.
+        You almost certainly do not want to do this, unless you have lost <b>Security Key</b> or{' '}
+        <b>Phrase</b> and every session you can cross-sign from.
       </Text>
-      <Button variant="danger" onClick={setupDialog}>Reset</Button>
+      <Button variant="danger" onClick={setupDialog}>
+        Reset
+      </Button>
     </div>
   );
 }
 
 const resetDialog = () => {
   openReusableDialog(
-    <Text variant="s1" weight="medium">Reset cross signing</Text>,
-    () => <CrossSigningReset />,
+    <Text variant="s1" weight="medium">
+      Reset cross signing
+    </Text>,
+    () => <CrossSigningReset />
   );
 };
 
@@ -210,12 +232,23 @@ function CrossSignin() {
   return (
     <SettingTile
       title="Cross signing"
-      content={<Text variant="b3">Setup to verify and keep track of all your sessions. Also required to backup encrypted message.</Text>}
-      options={(
-        isCSEnabled
-          ? <Button variant="danger" onClick={resetDialog}>Reset</Button>
-          : <Button variant="primary" onClick={setupDialog}>Setup</Button>
-      )}
+      content={
+        <Text variant="b3">
+          Setup to verify and keep track of all your sessions. Also required to backup encrypted
+          message.
+        </Text>
+      }
+      options={
+        isCSEnabled ? (
+          <Button variant="danger" onClick={resetDialog}>
+            Reset
+          </Button>
+        ) : (
+          <Button variant="primary" onClick={setupDialog}>
+            Setup
+          </Button>
+        )
+      }
     />
   );
 }
index 75f032bc32f52fe8ea2826a2e2f93e974e848318..b4f2125e206707f7a18bbb8840ae158469b5fe56 100644 (file)
@@ -2,7 +2,6 @@
 import React, { useState, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import './KeyBackup.scss';
-import { twemojify } from '../../../util/twemojify';
 
 import initMatrix from '../../../client/initMatrix';
 import { openReusableDialog } from '../../../client/action/navigation';
@@ -34,10 +33,7 @@ function CreateKeyBackupDialog({ keyData }) {
     let info;
 
     try {
-      info = await mx.prepareKeyBackupVersion(
-        null,
-        { secureSecretStorage: true },
-      );
+      info = await mx.prepareKeyBackupVersion(null, { secureSecretStorage: true });
       info = await mx.createKeyBackupVersion(info);
       await mx.scheduleAllGroupSessionsForBackup();
       if (!mountStore.getItem()) return;
@@ -65,7 +61,7 @@ function CreateKeyBackupDialog({ keyData }) {
       )}
       {done === true && (
         <>
-          <Text variant="h1">{twemojify('✅')}</Text>
+          <Text variant="h1"></Text>
           <Text>Successfully created backup</Text>
         </>
       )}
@@ -104,12 +100,9 @@ function RestoreKeyBackupDialog({ keyData }) {
 
     try {
       const backupInfo = await mx.getKeyBackupVersion();
-      const info = await mx.restoreKeyBackupWithSecretStorage(
-        backupInfo,
-        undefined,
-        undefined,
-        { progressCallback },
-      );
+      const info = await mx.restoreKeyBackupWithSecretStorage(backupInfo, undefined, undefined, {
+        progressCallback,
+      });
       if (!mountStore.getItem()) return;
       setStatus({ done: `Successfully restored backup keys (${info.imported}/${info.total}).` });
     } catch (e) {
@@ -138,7 +131,7 @@ function RestoreKeyBackupDialog({ keyData }) {
       )}
       {status.done && (
         <>
-          <Text variant="h1">{twemojify('✅')}</Text>
+          <Text variant="h1"></Text>
           <Text>{status.done}</Text>
         </>
       )}
@@ -176,14 +169,16 @@ function DeleteKeyBackupDialog({ requestClose }) {
 
   return (
     <div className="key-backup__delete">
-      <Text variant="h1">{twemojify('🗑')}</Text>
+      <Text variant="h1">🗑</Text>
       <Text weight="medium">Deleting key backup is permanent.</Text>
       <Text>All encrypted messages keys stored on server will be deleted.</Text>
-      {
-        isDeleting
-          ? <Spinner size="small" />
-          : <Button variant="danger" onClick={deleteBackup}>Delete</Button>
-      }
+      {isDeleting ? (
+        <Spinner size="small" />
+      ) : (
+        <Button variant="danger" onClick={deleteBackup}>
+          Delete
+        </Button>
+      )}
     </div>
   );
 }
@@ -224,9 +219,11 @@ function KeyBackup() {
     if (keyData === null) return;
 
     openReusableDialog(
-      <Text variant="s1" weight="medium">Create Key Backup</Text>,
+      <Text variant="s1" weight="medium">
+        Create Key Backup
+      </Text>,
       () => <CreateKeyBackupDialog keyData={keyData} />,
-      () => fetchKeyBackupVersion(),
+      () => fetchKeyBackupVersion()
     );
   };
 
@@ -235,29 +232,44 @@ function KeyBackup() {
     if (keyData === null) return;
 
     openReusableDialog(
-      <Text variant="s1" weight="medium">Restore Key Backup</Text>,
-      () => <RestoreKeyBackupDialog keyData={keyData} />,
+      <Text variant="s1" weight="medium">
+        Restore Key Backup
+      </Text>,
+      () => <RestoreKeyBackupDialog keyData={keyData} />
     );
   };
 
-  const openDeleteKeyBackup = () => openReusableDialog(
-    <Text variant="s1" weight="medium">Delete Key Backup</Text>,
-    (requestClose) => (
-      <DeleteKeyBackupDialog
-        requestClose={(isDone) => {
-          if (isDone) setKeyBackup(null);
-          requestClose();
-        }}
-      />
-    ),
-  );
+  const openDeleteKeyBackup = () =>
+    openReusableDialog(
+      <Text variant="s1" weight="medium">
+        Delete Key Backup
+      </Text>,
+      (requestClose) => (
+        <DeleteKeyBackupDialog
+          requestClose={(isDone) => {
+            if (isDone) setKeyBackup(null);
+            requestClose();
+          }}
+        />
+      )
+    );
 
   const renderOptions = () => {
     if (keyBackup === undefined) return <Spinner size="small" />;
-    if (keyBackup === null) return <Button variant="primary" onClick={openCreateKeyBackup}>Create Backup</Button>;
+    if (keyBackup === null)
+      return (
+        <Button variant="primary" onClick={openCreateKeyBackup}>
+          Create Backup
+        </Button>
+      );
     return (
       <>
-        <IconButton src={DownloadIC} variant="positive" onClick={openRestoreKeyBackup} tooltip="Restore backup" />
+        <IconButton
+          src={DownloadIC}
+          variant="positive"
+          onClick={openRestoreKeyBackup}
+          tooltip="Restore backup"
+        />
         <IconButton src={BinIC} onClick={openDeleteKeyBackup} tooltip="Delete backup" />
       </>
     );
@@ -266,9 +278,12 @@ function KeyBackup() {
   return (
     <SettingTile
       title="Encrypted messages backup"
-      content={(
+      content={
         <>
-          <Text variant="b3">Online backup your encrypted messages keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.</Text>
+          <Text variant="b3">
+            Online backup your encrypted messages keys with your account data in case you lose
+            access to your sessions. Your keys will be secured with a unique Security Key.
+          </Text>
           {!isCSEnabled && (
             <InfoCard
               style={{ marginTop: 'var(--sp-ultra-tight)' }}
@@ -279,7 +294,7 @@ function KeyBackup() {
             />
           )}
         </>
-      )}
+      }
       options={isCSEnabled ? renderOptions() : null}
     />
   );
diff --git a/src/app/organisms/shortcut-spaces/ShortcutSpaces.jsx b/src/app/organisms/shortcut-spaces/ShortcutSpaces.jsx
deleted file mode 100644 (file)
index 62ec76a..0000000
+++ /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 (
-      <RoomSelector
-        key={spaceId}
-        name={room.name}
-        parentName={parents}
-        roomId={spaceId}
-        imageSrc={null}
-        iconSrc={joinRuleToIconSrc(room.getJoinRule(), true)}
-        isUnread={false}
-        notificationCount={0}
-        isAlert={false}
-        onClick={isShortcut ? deleteShortcut : toggleSelected}
-        options={isShortcut ? (
-          <IconButton
-            src={isShortcut ? PinFilledIC : PinIC}
-            size="small"
-            onClick={deleteShortcut}
-            disabled={process !== null}
-          />
-        ) : (
-          <Checkbox
-            isActive={selected.includes(spaceId)}
-            variant="positive"
-            onToggle={toggleSelected}
-            tabIndex={-1}
-            disabled={process !== null}
-          />
-        )}
-      />
-    );
-  };
-
-  return (
-    <>
-      <Text className="shortcut-spaces__header" variant="b3" weight="bold">Pinned spaces</Text>
-      {spaceShortcut.length === 0 && <Text>No pinned spaces</Text>}
-      {spaceShortcut.map((spaceId) => renderSpace(spaceId, true))}
-      <Text className="shortcut-spaces__header" variant="b3" weight="bold">Unpinned spaces</Text>
-      {spaceWithoutShortcut.length === 0 && <Text>No unpinned spaces</Text>}
-      {spaceWithoutShortcut.map((spaceId) => renderSpace(spaceId, false))}
-      {selected.length !== 0 && (
-        <div className="shortcut-spaces__footer">
-          {process && <Spinner size="small" />}
-          <Text weight="medium">{process || `${selected.length} spaces selected`}</Text>
-          { !process && (
-            <Button onClick={handleAdd} variant="primary">Pin</Button>
-          )}
-        </div>
-      )}
-    </>
-  );
-}
-
-function useVisibilityToggle() {
-  const [isOpen, setIsOpen] = useState(false);
-
-  useEffect(() => {
-    const handleOpen = () => setIsOpen(true);
-    navigation.on(cons.events.navigation.SHORTCUT_SPACES_OPENED, handleOpen);
-    return () => {
-      navigation.removeListener(cons.events.navigation.SHORTCUT_SPACES_OPENED, handleOpen);
-    };
-  }, []);
-
-  const requestClose = () => setIsOpen(false);
-
-  return [isOpen, requestClose];
-}
-
-function ShortcutSpaces() {
-  const [isOpen, requestClose] = useVisibilityToggle();
-
-  return (
-    <Dialog
-      isOpen={isOpen}
-      className="shortcut-spaces"
-      title={(
-        <Text variant="s1" weight="medium" primary>
-          Pin spaces
-        </Text>
-      )}
-      contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
-      onRequestClose={requestClose}
-    >
-      {
-        isOpen
-          ? <ShortcutSpacesContent />
-          : <div />
-      }
-    </Dialog>
-  );
-}
-
-export default ShortcutSpaces;
diff --git a/src/app/organisms/shortcut-spaces/ShortcutSpaces.scss b/src/app/organisms/shortcut-spaces/ShortcutSpaces.scss
deleted file mode 100644 (file)
index 686c8cc..0000000
+++ /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 (file)
index 60f00ad..0000000
+++ /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 (
-    <div className="space-manage-breadcrumb__wrapper">
-      <ScrollView horizontal vertical={false} invisible>
-        <div className="space-manage-breadcrumb">
-          {
-            path.map((item, index) => (
-              <React.Fragment key={item.roomId}>
-                {index > 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
-                <Button onClick={() => onSelect(item.roomId, item.name)}>
-                  <Text variant="b2">{twemojify(item.name)}</Text>
-                </Button>
-              </React.Fragment>
-            ))
-          }
-        </div>
-      </ScrollView>
-    </div>
-  );
-}
-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 = (
-    <Avatar
-      text={name}
-      bgColor={colorMXID(roomId)}
-      imageSrc={isDM ? imageSrc : null}
-      iconColor="var(--ic-surface-low)"
-      iconSrc={
-        isDM
-          ? null
-          : joinRuleToIconSrc((roomInfo.join_rules || roomInfo.join_rule), isSpace)
-      }
-      size="extra-small"
-    />
-  );
-  const roomNameJSX = (
-    <Text>
-      {twemojify(name)}
-      <Text variant="b3" span>{` • ${roomInfo.num_joined_members} members`}</Text>
-    </Text>
-  );
-
-  const expandBtnJsx = (
-    <IconButton
-      variant={isExpand ? 'primary' : 'surface'}
-      size="extra-small"
-      src={InfoIC}
-      tooltip="Topic"
-      tooltipPlacement="top"
-      onClick={() => setIsExpand(!isExpand)}
-    />
-  );
-
-  return (
-    <div
-      className={`space-manage-item${isSpace ? '--space' : ''}`}
-    >
-      <div>
-        {canManage && <Checkbox isActive={isSelected} onToggle={() => onSelect(roomId)} variant="positive" />}
-        <button
-          className="space-manage-item__btn"
-          onClick={isSpace ? () => onSpaceClick(roomId, name) : null}
-          type="button"
-        >
-          {roomAvatarJSX}
-          {roomNameJSX}
-          {isSuggested && <Text variant="b2">Suggested</Text>}
-        </button>
-        {roomInfo.topic && expandBtnJsx}
-        {
-          isJoined
-            ? <Button onClick={handleOpen}>Open</Button>
-            : <Button variant="primary" onClick={handleJoin} disabled={isJoining}>{isJoining ? 'Joining...' : 'Join'}</Button>
-        }
-      </div>
-      {isExpand && roomInfo.topic && <Text variant="b2">{twemojify(roomInfo.topic, undefined, true)}</Text>}
-    </div>
-  );
-}
-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 (
-    <div className="space-manage__footer">
-      {process && <Spinner size="small" />}
-      <Text weight="medium">{process || `${selected.length} item selected`}</Text>
-      { !process && (
-        <>
-          <Button onClick={handleRemove} variant="danger">Remove</Button>
-          <Button
-            onClick={() => handleToggleSuggested(!allSuggested)}
-            variant={allSuggested ? 'surface' : 'primary'}
-          >
-            {allSuggested ? 'Mark as not suggested' : 'Mark as suggested'}
-          </Button>
-        </>
-      )}
-    </div>
-  );
-}
-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 (
-    <div className="space-manage__content">
-      {spacePath.length > 1 && (
-        <SpaceManageBreadcrumb path={spacePath} onSelect={addPathItem} />
-      )}
-      <Text variant="b3" weight="bold">Rooms and spaces</Text>
-      <div className="space-manage__content-items">
-        {!isLoading && currentHierarchy?.rooms?.length === 1 && (
-          <Text>
-            Either the space contains private rooms or you need to join space to view it's rooms.
-          </Text>
-        )}
-        {currentHierarchy && (currentHierarchy.rooms?.map((roomInfo) => (
-          roomInfo.room_id === currentPath.roomId
-            ? null
-            : (
-              <SpaceManageItem
-                key={roomInfo.room_id}
-                isSelected={selected.includes(roomInfo.room_id)}
-                roomHierarchy={currentHierarchy}
-                parentId={currentPath.roomId}
-                roomInfo={roomInfo}
-                onSpaceClick={addPathItem}
-                requestClose={requestClose}
-                onSelect={handleSelected}
-              />
-            )
-        )))}
-        {!currentHierarchy && <Text>loading...</Text>}
-      </div>
-      {currentHierarchy?.canLoadMore && !isLoading && (
-        <Button onClick={loadRoomHierarchy}>Load more</Button>
-      )}
-      {isLoading && (
-        <div className="space-manage__content-loading">
-          <Spinner size="small" />
-          <Text>Loading rooms...</Text>
-        </div>
-      )}
-      {selected.length > 0 && (
-        <SpaceManageFooter parentId={currentPath.roomId} selected={selected} />
-      )}
-    </div>
-  );
-}
-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 (
-    <PopupWindow
-      isOpen={roomId !== null}
-      className="space-manage"
-      title={(
-        <Text variant="s1" weight="medium" primary>
-          {roomId && twemojify(room.name)}
-          <span style={{ color: 'var(--tc-surface-low)' }}> — manage rooms</span>
-        </Text>
-      )}
-      contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
-      onRequestClose={requestClose}
-    >
-      {
-        roomId
-          ? <SpaceManageContent roomId={roomId} requestClose={requestClose} />
-          : <div />
-      }
-    </PopupWindow>
-  );
-}
-
-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 (file)
index b72c92d..0000000
+++ /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
index 46fe7b3f73588d638cc1742965d44e754a1fc36c..ff6c18632750e19c3b5c68327d2ec3fda68347e9 100644 (file)
@@ -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={
         <Text variant="s1" weight="medium" primary>
-          {isOpen && twemojify(room.name)}
+          {isOpen && room.name}
           <span style={{ color: 'var(--tc-surface-low)' }}> — space settings</span>
         </Text>
       }
diff --git a/src/app/organisms/sticker-board/StickerBoard.jsx b/src/app/organisms/sticker-board/StickerBoard.jsx
deleted file mode 100644 (file)
index 91e2591..0000000
+++ /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) => (
-    <div className="sticker-board__pack" key={pack.id}>
-      <Text className="sticker-board__pack-header" variant="b2" weight="bold">{pack.displayName ?? 'Unknown'}</Text>
-      <div className="sticker-board__pack-items">
-        {pack.getStickers().map((sticker) => (
-          <img
-            key={sticker.shortcode}
-            className="sticker-board__sticker"
-            src={mx.mxcUrlToHttp(sticker.mxc)}
-            alt={sticker.shortcode}
-            title={sticker.body ?? sticker.shortcode}
-            data-mx-sticker={sticker.mxc}
-            loading="lazy"
-          />
-        ))}
-      </div>
-    </div>
-  );
-
-  return (
-    <div className="sticker-board">
-      {packs.length > 0 && (
-        <ScrollView invisible>
-          <div className="sticker-board__sidebar">
-            {packs.map((pack, index) => {
-              const src = mx.mxcUrlToHttp(pack.avatarUrl ?? pack.getStickers()[0].mxc);
-              return (
-                <IconButton
-                  key={pack.id}
-                  onClick={() => openGroup(index)}
-                  src={src}
-                  tooltip={pack.displayName || 'Unknown'}
-                  tooltipPlacement="left"
-                  isImage
-                />
-              );
-            })}
-          </div>
-        </ScrollView>
-      )}
-      <div className="sticker-board__container">
-        <ScrollView autoHide ref={scrollRef}>
-          <div
-            onClick={handleOnSelect}
-            className="sticker-board__content"
-          >
-            {
-              packs.length > 0
-                ? packs.map(renderPack)
-                : (
-                  <div className="sticker-board__empty">
-                    <Text>There is no sticker pack.</Text>
-                  </div>
-                )
-            }
-          </div>
-        </ScrollView>
-      </div>
-      <div />
-    </div>
-  );
-}
-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 (file)
index b4e5513..0000000
+++ /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 (file)
index 9bd3334..0000000
+++ /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 (
-    <div className="view-source__card">
-      <MenuHeader>{title}</MenuHeader>
-      <ScrollView horizontal vertical={false} autoHide>
-        <pre className="text text-b1">
-          <code className="language-json">
-            {JSON.stringify(json, null, 2)}
-          </code>
-        </pre>
-      </ScrollView>
-    </div>
-  );
-}
-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 = () => (
-    <div className="view-source">
-      {event.isEncrypted() && <ViewSourceBlock title="Decrypted source" json={event.getEffectiveEvent()} />}
-      <ViewSourceBlock title="Original source" json={event.event} />
-    </div>
-  );
-
-  return (
-    <PopupWindow
-      isOpen={isOpen}
-      title="View source"
-      onAfterClose={handleAfterClose}
-      onRequestClose={() => setIsOpen(false)}
-      contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
-    >
-      {event ? renderViewSource() : <div />}
-    </PopupWindow>
-  );
-}
-
-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 (file)
index 9ceab8b..0000000
+++ /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 (file)
index 6d135be..0000000
+++ /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 (
-    <div className="app-welcome flex--center">
-      <div>
-        <img className="app-welcome__logo noselect" src={CinnySvg} alt="Cinny logo" />
-        <Text className="app-welcome__heading" variant="h1" weight="medium" primary>Welcome to Cinny</Text>
-        <Text className="app-welcome__subheading" variant="s1">Yet another matrix client</Text>
-      </div>
-    </div>
-  );
-}
-
-export default Welcome;
diff --git a/src/app/organisms/welcome/Welcome.scss b/src/app/organisms/welcome/Welcome.scss
deleted file mode 100644 (file)
index e55bb8e..0000000
+++ /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
index ffa20d4ed236c6dd3fa3236314909debdce5fad2..7d0f4fdeb1cc302c50a13f875bc446f81e090431 100644 (file)
@@ -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)
           <ClientRoot>
             <ClientInitStorageAtom>
               <ClientBindAtoms>
-                <ClientLayout
-                  nav={
-                    <MobileFriendlyClientNav>
-                      <SidebarNav />
-                    </MobileFriendlyClientNav>
-                  }
-                >
-                  <Outlet />
-                </ClientLayout>
+                <ClientNonUIFeatures>
+                  <ClientLayout
+                    nav={
+                      <MobileFriendlyClientNav>
+                        <SidebarNav />
+                      </MobileFriendlyClientNav>
+                    }
+                  >
+                    <Outlet />
+                  </ClientLayout>
+                </ClientNonUIFeatures>
               </ClientBindAtoms>
             </ClientInitStorageAtom>
           </ClientRoot>
diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx
new file mode 100644 (file)
index 0000000..27d1ae4
--- /dev/null
@@ -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<HTMLAudioElement>(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
+    <audio ref={audioRef} style={{ display: 'none' }}>
+      <source src="../../../../public/sound/invite.ogg" type="audio/ogg" />
+    </audio>
+  );
+}
+
+function MessageNotifications() {
+  const audioRef = useRef<HTMLAudioElement>(null);
+  const notifRef = useRef<Notification>();
+  const unreadCacheRef = useRef<Map<string, UnreadInfo>>(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
+    <audio ref={audioRef} style={{ display: 'none' }}>
+      <source src="../../../../public/sound/notification.ogg" type="audio/ogg" />
+    </audio>
+  );
+}
+
+type ClientNonUIFeaturesProps = {
+  children: ReactNode;
+};
+
+export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) {
+  return (
+    <>
+      <FaviconUpdater />
+      <InviteNotifications />
+      <MessageNotifications />
+      {children}
+    </>
+  );
+}
index 1bb7855b6e464fc740c048a02f4a4a9e3968f733..6a1dbcb16cd66bbc469e0b591d68610abe91f475 100644 (file)
@@ -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);
index 1ab08f01ccceda06277d9b5c058c8e758fee863f..8b9d1847c151e5665950c9d96a910615e23ae958 100644 (file)
@@ -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 (
+            <Box grow="Yes" direction="Column">
+              <Text size="T400" priority="300">
+                <code className={customHtmlCss.Code}>{evt.type}</code>
+                {' event'}
+              </Text>
+            </Box>
+          );
+        }
+
+        return (
+          <EncryptedContent mEvent={mEvent}>
+            {() => {
+              if (mEvent.isRedacted()) return <RedactedContent />;
+              if (mEvent.getType() === MessageEvent.Sticker)
+                return (
+                  <MSticker
+                    content={mEvent.getContent()}
+                    renderImageContent={(props) => (
+                      <ImageContent
+                        {...props}
+                        autoPlay={mediaAutoLoad}
+                        renderImage={(p) => <Image {...p} loading="lazy" />}
+                        renderViewer={(p) => <ImageViewer {...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 (
+                  <RenderMessageContent
+                    displayName={displayName}
+                    msgType={mEvent.getContent().msgtype ?? ''}
+                    ts={mEvent.getTs()}
+                    edited={!!editedEvent}
+                    getContent={getContent}
+                    mediaAutoLoad={mediaAutoLoad}
+                    urlPreview={urlPreview}
+                    htmlReactParserOptions={htmlReactParserOptions}
+                  />
+                );
+              }
+              if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
+                return (
+                  <Text>
+                    <MessageNotDecryptedContent />
+                  </Text>
+                );
+              return (
+                <Text>
+                  <MessageUnsupportedContent />
+                </Text>
+              );
+            }}
+          </EncryptedContent>
+        );
+      },
       [MessageEvent.Sticker]: (event, displayName, getContent) => {
         if (event.unsigned?.redacted_because) {
           return <RedactedContent reason={event.unsigned?.redacted_because.content.reason} />;
@@ -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
   );
index 4ac391fe92636f356737884f078e82ab3bc3a190..5a009405eee79dcefaeef45440b4d7a3606eeb4c 100644 (file)
@@ -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,
index 88d4687cdcb4364452bf563f89f92cfc5145b041..55bf8f62837c61f3c89c16421ca3336326f6cf48 100644 (file)
@@ -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 (file)
index 7c21173..0000000
+++ /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 (
-    <>
-      <div className="homeserver-form">
-        <Input
-          name="homeserver"
-          onChange={handleHsInput}
-          value={hs?.selected}
-          forwardRef={hsRef}
-          label="Homeserver"
-          disabled={hs === null || !hs.allowCustom}
-        />
-        <ContextMenu
-          placement="right"
-          content={(hideMenu) => (
-            <>
-              <MenuHeader>Homeserver list</MenuHeader>
-              {
-                hs?.list.map((hsName) => (
-                  <MenuItem
-                    key={hsName}
-                    onClick={() => {
-                      hideMenu();
-                      hsRef.current.value = hsName;
-                      setHs({ ...hs, selected: hsName });
-                    }}
-                  >
-                    {hsName}
-                  </MenuItem>
-                ))
-              }
-            </>
-          )}
-          render={(toggleMenu) => <IconButton onClick={toggleMenu} src={ChevronBottomIC} />}
-        />
-      </div>
-      {process.error !== undefined && <Text className="homeserver-form__error" variant="b3">{process.error}</Text>}
-      {process.isLoading && (
-        <div className="homeserver-form__status flex--center">
-          <Spinner size="small" />
-          <Text variant="b2">{process.message}</Text>
-        </div>
-      )}
-    </>
-  );
-}
-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 (
-    <>
-      <div className="auth-form__heading">
-        <Text variant="h2" weight="medium">Login</Text>
-        {isPassword && (
-          <ContextMenu
-            placement="right"
-            content={(hideMenu) => (
-              loginTypes.map((type, index) => (
-                <MenuItem
-                  key={type}
-                  onClick={() => {
-                    hideMenu();
-                    setTypeIndex(index);
-                  }}
-                >
-                  {type}
-                </MenuItem>
-              ))
-            )}
-            render={(toggleMenu) => (
-              <Button onClick={toggleMenu} iconSrc={ChevronBottomIC}>
-                {loginTypes[typeIndex]}
-              </Button>
-            )}
-          />
-        )}
-      </div>
-      {isPassword && (
-        <Formik
-          initialValues={initialValues}
-          onSubmit={submitter}
-          validate={validator}
-        >
-          {({
-            values, errors, handleChange, handleSubmit, isSubmitting,
-          }) => (
-            <>
-              {isSubmitting && <LoadingScreen message="Login in progress..." />}
-              <form className="auth-form" onSubmit={handleSubmit}>
-                {typeIndex === 0 && <Input values={values.username} name="username" onChange={handleChange} label="Username" type="username" required />}
-                {errors.username && <Text className="auth-form__error" variant="b3">{errors.username}</Text>}
-                {typeIndex === 1 && <Input values={values.email} name="email" onChange={handleChange} label="Email" type="email" required />}
-                {errors.email && <Text className="auth-form__error" variant="b3">{errors.email}</Text>}
-                <div className="auth-form__pass-eye-wrapper">
-                  <Input values={values.password} name="password" onChange={handleChange} label="Password" type={passVisible ? 'text' : 'password'} required />
-                  <IconButton onClick={() => setPassVisible(!passVisible)} src={passVisible ? EyeIC : EyeBlindIC} size="extra-small" />
-                </div>
-                {errors.password && <Text className="auth-form__error" variant="b3">{errors.password}</Text>}
-                {errors.other && <Text className="auth-form__error" variant="b3">{errors.other}</Text>}
-                <div className="auth-form__btns">
-                  <Button variant="primary" type="submit" disabled={isSubmitting}>Login</Button>
-                </div>
-              </form>
-            </>
-          )}
-        </Formik>
-      )}
-      {ssoProviders && isPassword && <Text className="sso__divider">OR</Text>}
-      {ssoProviders && (
-        <SSOButtons
-          type="sso"
-          identityProviders={ssoProviders.identity_providers}
-          baseUrl={baseUrl}
-        />
-      )}
-    </>
-  );
-}
-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' && <LoadingScreen message={process.message} />}
-      {process.type === 'm.login.recaptcha' && <Recaptcha message="Please check the box below to proceed." sitekey={process.sitekey} onChange={handleRecaptcha} />}
-      {process.type === 'm.login.terms' && <Terms url={process.url} onSubmit={handleTerms} />}
-      {process.type === 'm.login.email.identity' && <EmailVerify email={process.email} onContinue={handleEmailVerify} />}
-      <div className="auth-form__heading">
-        {!isDisabled && <Text variant="h2" weight="medium">Register</Text>}
-        {isDisabled && <Text className="auth-form__error">{registerInfo.error}</Text>}
-      </div>
-      {!isDisabled && (
-        <Formik
-          initialValues={initialValues}
-          onSubmit={submitter}
-          validate={validator}
-        >
-          {({
-            values, errors, handleChange, handleSubmit, isSubmitting,
-          }) => (
-            <>
-              {process.type === undefined && isSubmitting && <LoadingScreen message="Registration in progress..." />}
-              <form className="auth-form" ref={formRef} onSubmit={handleSubmit}>
-                <Input values={values.username} name="username" onChange={handleChange} label="Username" type="username" required />
-                {errors.username && <Text className="auth-form__error" variant="b3">{errors.username}</Text>}
-                <div className="auth-form__pass-eye-wrapper">
-                  <Input values={values.password} name="password" onChange={handleChange} label="Password" type={passVisible ? 'text' : 'password'} required />
-                  <IconButton onClick={() => setPassVisible(!passVisible)} src={passVisible ? EyeIC : EyeBlindIC} size="extra-small" />
-                </div>
-                {errors.password && <Text className="auth-form__error" variant="b3">{errors.password}</Text>}
-                <div className="auth-form__pass-eye-wrapper">
-                  <Input values={values.confirmPassword} name="confirmPassword" onChange={handleChange} label="Confirm password" type={cPassVisible ? 'text' : 'password'} required />
-                  <IconButton onClick={() => setCPassVisible(!cPassVisible)} src={cPassVisible ? EyeIC : EyeBlindIC} size="extra-small" />
-                </div>
-                {errors.confirmPassword && <Text className="auth-form__error" variant="b3">{errors.confirmPassword}</Text>}
-                {isEmail && <Input values={values.email} name="email" onChange={handleChange} label={`Email${isEmailRequired ? '' : ' (optional)'}`} type="email" required={isEmailRequired} />}
-                {errors.email && <Text className="auth-form__error" variant="b3">{errors.email}</Text>}
-                {errors.other && <Text className="auth-form__error" variant="b3">{errors.other}</Text>}
-                <div className="auth-form__btns">
-                  <Button variant="primary" type="submit" disabled={isSubmitting}>Register</Button>
-                </div>
-              </form>
-            </>
-          )}
-        </Formik>
-      )}
-      {isDisabled && ssoProviders && (
-        <SSOButtons
-          type="sso"
-          identityProviders={ssoProviders.identity_providers}
-          baseUrl={baseUrl}
-        />
-      )}
-    </>
-  );
-}
-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 (
-    <>
-      <Homeserver onChange={handleHsChange} />
-      { hsConfig !== null && (
-        type === 'login'
-          ? <Login loginFlow={hsConfig.login.flows} baseUrl={hsConfig.baseUrl} />
-          : (
-            <Register
-              registerInfo={hsConfig.register}
-              loginFlow={hsConfig.login.flows}
-              baseUrl={hsConfig.baseUrl}
-            />
-          )
-      )}
-      { hsConfig !== null && (
-        <Text variant="b2" className="auth-card__switch flex--center">
-          {`${(type === 'login' ? 'Don\'t have' : 'Already have')} an account?`}
-          <button
-            type="button"
-            style={{ color: 'var(--tc-link)', cursor: 'pointer', margin: '0 var(--sp-ultra-tight)' }}
-            onClick={() => setType((type === 'login') ? 'register' : 'login')}
-          >
-            { type === 'login' ? ' Register' : ' Login' }
-          </button>
-        </Text>
-      )}
-    </>
-  );
-}
-
-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 (
-    <ScrollView invisible>
-      <div className="auth__base">
-        <div className="auth__wrapper">
-          {loginToken && <LoadingScreen message="Redirecting..." />}
-          {!loginToken && (
-            <div className="auth-card">
-              <Header>
-                <Avatar size="extra-small" imageSrc={CinnySvg} />
-                <TitleWrapper>
-                  <Text variant="h2" weight="medium">Cinny</Text>
-                </TitleWrapper>
-              </Header>
-              <div className="auth-card__content">
-                <AuthCard />
-              </div>
-            </div>
-          )}
-        </div>
-
-        <div className="auth-footer">
-          <Text variant="b2">
-            <a href="https://cinny.in" target="_blank" rel="noreferrer">About</a>
-          </Text>
-          <Text variant="b2">
-            <a href="https://github.com/ajbura/cinny/releases" target="_blank" rel="noreferrer">{`v${cons.version}`}</a>
-          </Text>
-          <Text variant="b2">
-            <a href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">Twitter</a>
-          </Text>
-          <Text variant="b2">
-            <a href="https://matrix.org" target="_blank" rel="noreferrer">Powered by Matrix</a>
-          </Text>
-        </div>
-      </div>
-    </ScrollView>
-  );
-}
-
-function LoadingScreen({ message }) {
-  return (
-    <ProcessWrapper>
-      <Spinner />
-      <div style={{ marginTop: 'var(--sp-normal)' }}>
-        <Text variant="b1">{message}</Text>
-      </div>
-    </ProcessWrapper>
-  );
-}
-LoadingScreen.propTypes = {
-  message: PropTypes.string.isRequired,
-};
-
-function Recaptcha({ message, sitekey, onChange }) {
-  return (
-    <ProcessWrapper>
-      <div style={{ marginBottom: 'var(--sp-normal)' }}>
-        <Text variant="s1" weight="medium">{message}</Text>
-      </div>
-      <ReCAPTCHA sitekey={sitekey} onChange={onChange} />
-    </ProcessWrapper>
-  );
-}
-Recaptcha.propTypes = {
-  message: PropTypes.string.isRequired,
-  sitekey: PropTypes.string.isRequired,
-  onChange: PropTypes.func.isRequired,
-};
-
-function Terms({ url, onSubmit }) {
-  return (
-    <ProcessWrapper>
-      <form onSubmit={(e) => { e.preventDefault(); onSubmit(); }}>
-        <div style={{ margin: 'var(--sp-normal)', maxWidth: '450px' }}>
-          <Text variant="h2" weight="medium">Agree with terms</Text>
-          <div style={{ marginBottom: 'var(--sp-normal)' }} />
-          <Text variant="b1">In order to complete registration, you need to agree to the terms and conditions.</Text>
-          <div style={{ display: 'flex', alignItems: 'center', margin: 'var(--sp-normal) 0' }}>
-            <input style={{ marginRight: '8px' }} id="termsCheckbox" type="checkbox" required />
-            <Text variant="b1">
-              {'I accept '}
-              <a style={{ cursor: 'pointer' }} href={url} rel="noreferrer" target="_blank">Terms and Conditions</a>
-            </Text>
-          </div>
-          <Button id="termsBtn" type="submit" variant="primary">Submit</Button>
-        </div>
-      </form>
-    </ProcessWrapper>
-  );
-}
-Terms.propTypes = {
-  url: PropTypes.string.isRequired,
-  onSubmit: PropTypes.func.isRequired,
-};
-
-function EmailVerify({ email, onContinue }) {
-  return (
-    <ProcessWrapper>
-      <div style={{ margin: 'var(--sp-normal)', maxWidth: '450px' }}>
-        <Text variant="h2" weight="medium">Verify email</Text>
-        <div style={{ margin: 'var(--sp-normal) 0' }}>
-          <Text variant="b1">
-            {'Please check your email '}
-            <b>{`(${email})`}</b>
-            {' and validate before continuing further.'}
-          </Text>
-        </div>
-        <Button variant="primary" onClick={onContinue}>Continue</Button>
-      </div>
-    </ProcessWrapper>
-  );
-}
-EmailVerify.propTypes = {
-  email: PropTypes.string.isRequired,
-};
-
-function ProcessWrapper({ children }) {
-  return (
-    <div className="process-wrapper">
-      {children}
-    </div>
-  );
-}
-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 (file)
index 956a270..0000000
+++ /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 (file)
index f6ef2b9..0000000
+++ /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 (
-    <div className="client-container">
-      {/* <div className="navigation__wrapper" ref={navWrapperRef}>
-        <Navigation />
-      </div> */}
-      <div className={`room__wrapper ${classNameHidden}`} ref={roomWrapperRef}>
-        <ClientContent />
-      </div>
-      <Windows />
-      <Dialogs />
-      <ReusableContextMenu />
-      <SystemEmojiFeature />
-    </div>
-  );
-}
-
-export default Client;
diff --git a/src/app/templates/client/Client.scss b/src/app/templates/client/Client.scss
deleted file mode 100644 (file)
index bad5fc9..0000000
+++ /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 (file)
index cebe012..0000000
+++ /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 <Welcome />;
-  }
-
-  return <RoomBaseView room={room} eventId={eventId} />;
-}
index 7840fe494bd0c55f670e50d6dfb1f71fc94fc9f6..744241546ca0c15829583696632ca093486b60fb 100644 (file)
@@ -1,8 +1,16 @@
-export type DisposeCallback<Q extends unknown[] = [], R = void> = (...args: Q) => R;
-export type DisposableContext<P extends unknown[] = [], Q extends unknown[] = [], R = void> = (
-  ...args: P
-) => DisposeCallback<Q, R>;
+export type DisposeCallback<DisposeArgs extends unknown[] = [], DisposeReturn = void> = (
+  ...args: DisposeArgs
+) => DisposeReturn;
+export type DisposableContext<
+  DisposableArgs extends unknown[] = [],
+  DisposeArgs extends unknown[] = [],
+  DisposeReturn = void
+> = (...args: DisposableArgs) => DisposeCallback<DisposeArgs, DisposeReturn>;
 
-export const disposable = <P extends unknown[], Q extends unknown[] = [], R = void>(
-  context: DisposableContext<P, Q, R>
+export const disposable = <
+  DisposableArgs extends unknown[],
+  DisposeArgs extends unknown[] = [],
+  DisposeReturn = void
+>(
+  context: DisposableContext<DisposableArgs, DisposeArgs, DisposeReturn>
 ) => context;
index f39fe623a3dd3a13b5725cd44d191f34cf6cbcd8..1aea6754c682fdeb1ab8c03967ed73d6bc7456cf 100644 (file)
@@ -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 (file)
index 1fb49fb..0000000
+++ /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 (file)
index f04306b..0000000
+++ /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 (file)
index 0000000..dbe9baa
--- /dev/null
@@ -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);
+}
index e48e839b33da0833cfe60715aa1f66d9a583a49c..1967a463bc21f2ea1de92a100e9da1726896f8b2 100644 (file)
@@ -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,
index a869632a84c46281b23e17bb199491ac3a731f8d..579c7c3cb7d02909ac769dfe4f6758d99170d73f 100644 (file)
@@ -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());
index 996c26802242b1b2c003f82231aa467d3248d13b..c2d11438884fc007eed376747048cc5c3671afd4 100644 (file)
@@ -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 (file)
index 41c62d4..0000000
+++ /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,
-};
index 076d3794100832ce4aa345eec17961943239ff4c..856fcadc49e4a27b937d77600c1604648e4d6312 100644 (file)
@@ -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 (file)
index 6592d67..0000000
+++ /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 };
index 835982f81be091e548b505ed0fd4368b41520f30..0352ff36379553a17b9f252a423acb25599bedea 100644 (file)
@@ -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 (file)
index 6fc811a..0000000
+++ /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 (file)
index 09fa240..0000000
+++ /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 (file)
index fc137ae..0000000
+++ /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 (file)
index 57d91c1..0000000
+++ /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 (file)
index f3ffb1f..0000000
+++ /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 (file)
index d1e0aed..0000000
+++ /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('</mx-reply>'));
-        if (fReplyHead) content.formatted_body = `${fReplyHead}</mx-reply>${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 = `<a href="https://matrix.to/#/${encodeURIComponent(roomId)}/${encodeURIComponent(reply.eventId)}">In reply to</a>`;
-      const userLink = `<a href="https://matrix.to/#/${encodeURIComponent(reply.userId)}">${sanitizeText(reply.userId)}</a>`;
-      const fallback = `<mx-reply><blockquote>${replyToLink}${userLink}<br />${reply.formattedBody || sanitizeText(reply.body)}</blockquote></mx-reply>`;
-      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;
index 62c0cacc94b26c855f61724242c3fac1aec70dfa..523e871a14f92e7c8d824609517cfb56298231dc 100644 (file)
@@ -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',
index ddac4dda860ae00330472e7f95f2460d3f29ea9e..5f28f232f15567964352735c90a7b80216f72a00 100644 (file)
@@ -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 (file)
index 73c8f9e..0000000
+++ /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;
index 4d303aae4b07f200a23bc8b790c43b2122daa827..95600d29e64b68bf434f8109f9afbb9995f10e73 100644 (file)
@@ -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 (file)
index c6c1a49..0000000
+++ /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}</${tagName}>`;
-}
-
-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(/<br>/g, '\n').replace(/<\/p><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);
-}
index a776fb2be124effa7b6bf1dc1efdb89b4c27917e..74e56ec76006ae91274175551acdf31ef792c692 100644 (file)
@@ -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 (file)
index bf7efbc..0000000
+++ /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 (file)
index 3723a11..0000000
+++ /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 = {
-    '&': '&amp;',
-    '<': '&lt;',
-    '>': '&gt;',
-    '"': '&quot;',
-    "'": '&#39;',
-  };
-  return body.replace(/[&<>'"]/g, (tag) => tagsToReplace[tag] || tag);
-}
diff --git a/src/util/twemojify.jsx b/src/util/twemojify.jsx
deleted file mode 100644 (file)
index ad203a9..0000000
+++ /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);
-}