diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index 441da0de..450e4e29 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -14,7 +14,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4.2.0 - name: Setup node - uses: actions/setup-node@v4.3.0 + uses: actions/setup-node@v4.4.0 with: node-version: 20.12.2 cache: 'npm' diff --git a/.github/workflows/deploy-pull-request.yml b/.github/workflows/deploy-pull-request.yml index 9c0bea78..b330c3c1 100644 --- a/.github/workflows/deploy-pull-request.yml +++ b/.github/workflows/deploy-pull-request.yml @@ -15,7 +15,7 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - name: Download pr number - uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295 + uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 with: workflow: ${{ github.event.workflow.id }} run_id: ${{ github.event.workflow_run.id }} @@ -24,7 +24,7 @@ jobs: id: pr run: echo "id=$(> $GITHUB_OUTPUT - name: Download artifact - uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295 + uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 with: workflow: ${{ github.event.workflow.id }} run_id: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/docker-pr.yml b/.github/workflows/docker-pr.yml index 4e88c78d..398785ab 100644 --- a/.github/workflows/docker-pr.yml +++ b/.github/workflows/docker-pr.yml @@ -13,7 +13,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4.2.0 - name: Build Docker image - uses: docker/build-push-action@v6.15.0 + uses: docker/build-push-action@v6.18.0 with: context: . push: false diff --git a/.github/workflows/netlify-dev.yml b/.github/workflows/netlify-dev.yml index 34308c21..66cd5ad5 100644 --- a/.github/workflows/netlify-dev.yml +++ b/.github/workflows/netlify-dev.yml @@ -13,7 +13,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4.2.0 - name: Setup node - uses: actions/setup-node@v4.3.0 + uses: actions/setup-node@v4.4.0 with: node-version: 20.12.2 cache: 'npm' diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml index 44205ff2..24edda96 100644 --- a/.github/workflows/prod-deploy.yml +++ b/.github/workflows/prod-deploy.yml @@ -12,7 +12,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4.2.0 - name: Setup node - uses: actions/setup-node@v4.3.0 + uses: actions/setup-node@v4.4.0 with: node-version: 20.12.2 cache: 'npm' @@ -52,7 +52,7 @@ jobs: gpg --export | xxd -p echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz - name: Upload tagged release - uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda + uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 with: files: | cinny-${{ steps.vars.outputs.tag }}.tar.gz @@ -72,25 +72,25 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.10.0 - name: Login to Docker Hub - uses: docker/login-action@v3.4.0 + uses: docker/login-action@v3.5.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Login to the Container registry - uses: docker/login-action@v3.4.0 + uses: docker/login-action@v3.5.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5.7.0 + uses: docker/metadata-action@v5.8.0 with: images: | ${{ secrets.DOCKER_USERNAME }}/cinny ghcr.io/${{ github.repository }} - name: Build and push Docker image - uses: docker/build-push-action@v6.15.0 + uses: docker/build-push-action@v6.18.0 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/Dockerfile b/Dockerfile index abb65ee5..718fed72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN npm run build ## App -FROM nginx:1.27.4-alpine +FROM nginx:1.29.0-alpine COPY --from=builder /src/dist /app COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf diff --git a/index.html b/index.html index 9196cf3d..2eefeee2 100644 --- a/index.html +++ b/index.html @@ -90,6 +90,7 @@ window.global ||= window;
+
diff --git a/package-lock.json b/package-lock.json index 306de799..4aacff72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cinny", - "version": "4.8.0", + "version": "4.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cinny", - "version": "4.8.0", + "version": "4.10.0", "license": "AGPL-3.0-only", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "1.1.6", @@ -16,7 +16,6 @@ "@tanstack/react-query": "5.24.1", "@tanstack/react-query-devtools": "5.24.1", "@tanstack/react-virtual": "3.2.0", - "@tippyjs/react": "4.2.6", "@vanilla-extract/css": "1.9.3", "@vanilla-extract/recipes": "0.3.0", "@vanilla-extract/vite-plugin": "3.7.1", @@ -32,10 +31,8 @@ "emojibase": "15.3.1", "emojibase-data": "15.3.2", "file-saver": "2.0.5", - "flux": "4.0.3", "focus-trap-react": "10.0.2", - "folds": "2.1.0", - "formik": "2.4.6", + "folds": "2.3.0", "html-dom-parser": "4.0.0", "html-react-parser": "4.2.0", "i18next": "23.12.2", @@ -50,17 +47,14 @@ "millify": "6.1.0", "pdfjs-dist": "4.2.67", "prismjs": "1.30.0", - "prop-types": "15.8.1", "react": "18.2.0", "react-aria": "3.29.1", - "react-autosize-textarea": "7.1.0", "react-blurhash": "0.2.0", "react-colorful": "5.6.1", "react-dom": "18.2.0", "react-error-boundary": "4.0.13", "react-google-recaptcha": "2.1.0", "react-i18next": "15.0.0", - "react-modal": "3.16.1", "react-range": "1.8.14", "react-router-dom": "6.20.0", "sanitize-html": "2.12.1", @@ -68,7 +62,6 @@ "slate-dom": "0.112.2", "slate-history": "0.110.3", "slate-react": "0.112.1", - "tippy.js": "6.3.7", "ua-parser-js": "1.0.35" }, "devDependencies": { @@ -97,9 +90,8 @@ "eslint-plugin-react": "7.31.11", "eslint-plugin-react-hooks": "4.6.0", "prettier": "2.8.1", - "sass": "1.56.2", "typescript": "4.9.4", - "vite": "5.4.15", + "vite": "5.4.19", "vite-plugin-pwa": "0.20.5", "vite-plugin-static-copy": "1.0.4", "vite-plugin-top-level-await": "1.4.4" @@ -2313,15 +2305,6 @@ "node": ">= 8" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@react-aria/breadcrumbs": { "version": "3.5.20", "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.20.tgz", @@ -4524,18 +4507,6 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@tippyjs/react": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", - "integrity": "sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==", - "dependencies": { - "tippy.js": "^6.3.1" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4601,15 +4572,6 @@ "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==", "dev": true }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", - "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==", - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "node_modules/@types/is-hotkey": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.10.tgz", @@ -4643,12 +4605,14 @@ "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==" + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "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": "*", @@ -4697,7 +4661,8 @@ "node_modules/@types/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==" + "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==", + "dev": true }, "node_modules/@types/semver": { "version": "7.5.8", @@ -5320,11 +5285,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" - }, "node_modules/ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -5346,11 +5306,6 @@ "node": ">= 4.0.0" } }, - "node_modules/autosize": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.4.tgz", - "integrity": "sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ==" - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5830,11 +5785,6 @@ "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", "license": "MIT" }, - "node_modules/computed-style": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/computed-style/-/computed-style-0.1.4.tgz", - "integrity": "sha512-WpAmaKbMNmS3OProfHIdJiNleNJdgUrJfbKArXua28QF7+0CoZjlLn0lp6vlc+dl5r2/X9GQiQRQQU4BzSa69w==" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5896,14 +5846,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7019,11 +6961,6 @@ "node": ">=0.8.x" } }, - "node_modules/exenv": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", - "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7095,33 +7032,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fbemitter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz", - "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==", - "dependencies": { - "fbjs": "^3.0.0" - } - }, - "node_modules/fbjs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", - "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", - "dependencies": { - "cross-fetch": "^3.1.5", - "fbjs-css-vars": "^1.0.0", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^1.0.35" - } - }, - "node_modules/fbjs-css-vars": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", - "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==" - }, "node_modules/fdir": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", @@ -7230,18 +7140,6 @@ "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "dev": true }, - "node_modules/flux": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/flux/-/flux-4.0.3.tgz", - "integrity": "sha512-yKAbrp7JhZhj6uiT1FTuVMlIAT1J4jqEyBpFApi1kxpGZCvacMVc/t1pMQyotqHhAgvoE3bNvAykhCo2CLjnYw==", - "dependencies": { - "fbemitter": "^3.0.0", - "fbjs": "^3.0.1" - }, - "peerDependencies": { - "react": "^15.0.2 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/focus-trap": { "version": "7.6.4", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", @@ -7265,15 +7163,16 @@ } }, "node_modules/folds": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/folds/-/folds-2.1.0.tgz", - "integrity": "sha512-KwAG8bH3jsyZ9FKPMg+6ABV2YOcpp4nL0cCelsalnaPeRThkc5fgG1Xj5mhmdffYKjEXpEbERi5qmGbepgJryg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/folds/-/folds-2.3.0.tgz", + "integrity": "sha512-1KoM21jrg5daxvKrmSY0V04wa946KlNT0z6h017Rsnw2fdtNC6J0f34Ce5GF46Tzi00gZ/7SvCDXMzW/7e5s0w==", + "license": "Apache-2.0", "peerDependencies": { - "@vanilla-extract/css": "^1.9.2", - "@vanilla-extract/recipes": "^0.3.0", - "classnames": "^2.3.2", - "react": "^17.0.0", - "react-dom": "^17.0.0" + "@vanilla-extract/css": "1.9.2", + "@vanilla-extract/recipes": "0.3.0", + "classnames": "2.3.2", + "react": "17.0.0", + "react-dom": "17.0.0" } }, "node_modules/for-each": { @@ -7285,38 +7184,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/formik": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz", - "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==", - "funding": [ - { - "type": "individual", - "url": "https://opencollective.com/formik" - } - ], - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.1", - "deepmerge": "^2.1.1", - "hoist-non-react-statics": "^3.3.0", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "react-fast-compare": "^2.0.1", - "tiny-warning": "^1.0.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/formik/node_modules/deepmerge": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", - "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fs-extra": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", @@ -7895,12 +7762,6 @@ "url": "https://opencollective.com/immer" } }, - "node_modules/immutable": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", - "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", - "dev": true - }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -8635,17 +8496,6 @@ "node": ">=10" } }, - "node_modules/line-height": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/line-height/-/line-height-0.3.1.tgz", - "integrity": "sha512-YExecgqPwnp5gplD2+Y8e8A5+jKpr25+DzMbFdI1/1UAr0FJrTFv4VkHLf8/6B590i1wUPJWMKKldkd/bdQ//w==", - "dependencies": { - "computed-style": "~0.1.3" - }, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/linkify-react": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz", @@ -8679,11 +8529,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" - }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -9540,14 +9385,6 @@ "node": ">=6" } }, - "node_modules/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dependencies": { - "asap": "~2.0.3" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -9671,20 +9508,6 @@ "react": ">=16.4.1" } }, - "node_modules/react-autosize-textarea": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/react-autosize-textarea/-/react-autosize-textarea-7.1.0.tgz", - "integrity": "sha512-BHpjCDkuOlllZn3nLazY2F8oYO1tS2jHnWhcjTWQdcKiiMU6gHLNt/fzmqMSyerR0eTdKtfSIqtSeTtghNwS+g==", - "dependencies": { - "autosize": "^4.0.2", - "line-height": "^0.3.1", - "prop-types": "^15.5.6" - }, - "peerDependencies": { - "react": "^0.14.0 || ^15.0.0 || ^16.0.0", - "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0" - } - }, "node_modules/react-blurhash": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/react-blurhash/-/react-blurhash-0.2.0.tgz", @@ -9727,11 +9550,6 @@ "react": ">=16.13.1" } }, - "node_modules/react-fast-compare": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", - "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" - }, "node_modules/react-google-recaptcha": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz", @@ -9770,29 +9588,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" - }, - "node_modules/react-modal": { - "version": "3.16.1", - "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", - "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", - "dependencies": { - "exenv": "^1.2.0", - "prop-types": "^15.7.2", - "react-lifecycles-compat": "^3.0.0", - "warning": "^4.0.3" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", - "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" - } - }, "node_modules/react-property": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz", @@ -10251,23 +10046,6 @@ "postcss": "^8.3.11" } }, - "node_modules/sass": { - "version": "1.56.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz", - "integrity": "sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==", - "dev": true, - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -10367,11 +10145,6 @@ "node": ">= 0.4" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10968,14 +10741,6 @@ "node": ">=12.0.0" } }, - "node_modules/tippy.js": { - "version": "6.3.7", - "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", - "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", - "dependencies": { - "@popperjs/core": "^2.9.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11330,9 +11095,9 @@ } }, "node_modules/vite": { - "version": "5.4.15", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz", - "integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "license": "MIT", "dependencies": { "esbuild": "^0.21.3", @@ -11890,14 +11655,6 @@ "node": ">=0.10.0" } }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 8772462a..5a2356f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cinny", - "version": "4.8.0", + "version": "4.10.0", "description": "Yet another matrix client", "main": "index.js", "type": "module", @@ -27,7 +27,6 @@ "@tanstack/react-query": "5.24.1", "@tanstack/react-query-devtools": "5.24.1", "@tanstack/react-virtual": "3.2.0", - "@tippyjs/react": "4.2.6", "@vanilla-extract/css": "1.9.3", "@vanilla-extract/recipes": "0.3.0", "@vanilla-extract/vite-plugin": "3.7.1", @@ -43,10 +42,8 @@ "emojibase": "15.3.1", "emojibase-data": "15.3.2", "file-saver": "2.0.5", - "flux": "4.0.3", "focus-trap-react": "10.0.2", - "folds": "2.1.0", - "formik": "2.4.6", + "folds": "2.3.0", "html-dom-parser": "4.0.0", "html-react-parser": "4.2.0", "i18next": "23.12.2", @@ -61,17 +58,14 @@ "millify": "6.1.0", "pdfjs-dist": "4.2.67", "prismjs": "1.30.0", - "prop-types": "15.8.1", "react": "18.2.0", "react-aria": "3.29.1", - "react-autosize-textarea": "7.1.0", "react-blurhash": "0.2.0", "react-colorful": "5.6.1", "react-dom": "18.2.0", "react-error-boundary": "4.0.13", "react-google-recaptcha": "2.1.0", "react-i18next": "15.0.0", - "react-modal": "3.16.1", "react-range": "1.8.14", "react-router-dom": "6.20.0", "sanitize-html": "2.12.1", @@ -79,7 +73,6 @@ "slate-dom": "0.112.2", "slate-history": "0.110.3", "slate-react": "0.112.1", - "tippy.js": "6.3.7", "ua-parser-js": "1.0.35" }, "devDependencies": { @@ -108,9 +101,8 @@ "eslint-plugin-react": "7.31.11", "eslint-plugin-react-hooks": "4.6.0", "prettier": "2.8.1", - "sass": "1.56.2", "typescript": "4.9.4", - "vite": "5.4.15", + "vite": "5.4.19", "vite-plugin-pwa": "0.20.5", "vite-plugin-static-copy": "1.0.4", "vite-plugin-top-level-await": "1.4.4" diff --git a/public/res/ic/filled/category.svg b/public/res/ic/filled/category.svg deleted file mode 100644 index 87b2588d..00000000 --- a/public/res/ic/filled/category.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/public/res/ic/filled/pin.svg b/public/res/ic/filled/pin.svg deleted file mode 100644 index 6a701474..00000000 --- a/public/res/ic/filled/pin.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/public/res/ic/filled/star.svg b/public/res/ic/filled/star.svg deleted file mode 100644 index 378c891e..00000000 --- a/public/res/ic/filled/star.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/public/res/ic/outlined/add-pin.svg b/public/res/ic/outlined/add-pin.svg deleted file mode 100644 index 9634bede..00000000 --- a/public/res/ic/outlined/add-pin.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/add-user.svg b/public/res/ic/outlined/add-user.svg deleted file mode 100644 index c3803d80..00000000 --- a/public/res/ic/outlined/add-user.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/public/res/ic/outlined/ball.svg b/public/res/ic/outlined/ball.svg deleted file mode 100644 index d4b89ff5..00000000 --- a/public/res/ic/outlined/ball.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/bell-off.svg b/public/res/ic/outlined/bell-off.svg deleted file mode 100644 index 79ce8a33..00000000 --- a/public/res/ic/outlined/bell-off.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/public/res/ic/outlined/bell-ping.svg b/public/res/ic/outlined/bell-ping.svg deleted file mode 100644 index 3431bea1..00000000 --- a/public/res/ic/outlined/bell-ping.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/public/res/ic/outlined/bell-ring.svg b/public/res/ic/outlined/bell-ring.svg deleted file mode 100644 index 57fc2679..00000000 --- a/public/res/ic/outlined/bell-ring.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - diff --git a/public/res/ic/outlined/bell.svg b/public/res/ic/outlined/bell.svg deleted file mode 100644 index 43d470b5..00000000 --- a/public/res/ic/outlined/bell.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/bin.svg b/public/res/ic/outlined/bin.svg deleted file mode 100644 index 984be625..00000000 --- a/public/res/ic/outlined/bin.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/public/res/ic/outlined/bulb.svg b/public/res/ic/outlined/bulb.svg deleted file mode 100644 index 00e80886..00000000 --- a/public/res/ic/outlined/bulb.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/category.svg b/public/res/ic/outlined/category.svg deleted file mode 100644 index c7c33b38..00000000 --- a/public/res/ic/outlined/category.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - diff --git a/public/res/ic/outlined/check.svg b/public/res/ic/outlined/check.svg deleted file mode 100644 index 72a18327..00000000 --- a/public/res/ic/outlined/check.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/chevron-bottom.svg b/public/res/ic/outlined/chevron-bottom.svg deleted file mode 100644 index 5562b7aa..00000000 --- a/public/res/ic/outlined/chevron-bottom.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/public/res/ic/outlined/chevron-left.svg b/public/res/ic/outlined/chevron-left.svg deleted file mode 100644 index ba9e12cc..00000000 --- a/public/res/ic/outlined/chevron-left.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/public/res/ic/outlined/chevron-right.svg b/public/res/ic/outlined/chevron-right.svg deleted file mode 100644 index 7f6a806e..00000000 --- a/public/res/ic/outlined/chevron-right.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/public/res/ic/outlined/chevron-top.svg b/public/res/ic/outlined/chevron-top.svg deleted file mode 100644 index f5948fe9..00000000 --- a/public/res/ic/outlined/chevron-top.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/public/res/ic/outlined/circle-plus.svg b/public/res/ic/outlined/circle-plus.svg deleted file mode 100644 index 41690a08..00000000 --- a/public/res/ic/outlined/circle-plus.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/cmd.svg b/public/res/ic/outlined/cmd.svg deleted file mode 100644 index 75ae0d98..00000000 --- a/public/res/ic/outlined/cmd.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/public/res/ic/outlined/coin.svg b/public/res/ic/outlined/coin.svg deleted file mode 100644 index 025424e8..00000000 --- a/public/res/ic/outlined/coin.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/cross.svg b/public/res/ic/outlined/cross.svg deleted file mode 100644 index 0acda884..00000000 --- a/public/res/ic/outlined/cross.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/cup.svg b/public/res/ic/outlined/cup.svg deleted file mode 100644 index 8921e2c9..00000000 --- a/public/res/ic/outlined/cup.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/dog.svg b/public/res/ic/outlined/dog.svg deleted file mode 100644 index 3b252956..00000000 --- a/public/res/ic/outlined/dog.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - diff --git a/public/res/ic/outlined/download.svg b/public/res/ic/outlined/download.svg deleted file mode 100644 index 677014f3..00000000 --- a/public/res/ic/outlined/download.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - diff --git a/public/res/ic/outlined/emoji-add.svg b/public/res/ic/outlined/emoji-add.svg deleted file mode 100644 index c4cacef2..00000000 --- a/public/res/ic/outlined/emoji-add.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - diff --git a/public/res/ic/outlined/emoji.svg b/public/res/ic/outlined/emoji.svg deleted file mode 100644 index 0daac879..00000000 --- a/public/res/ic/outlined/emoji.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/public/res/ic/outlined/explore.svg b/public/res/ic/outlined/explore.svg deleted file mode 100644 index 7cc2a479..00000000 --- a/public/res/ic/outlined/explore.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/external.svg b/public/res/ic/outlined/external.svg deleted file mode 100644 index adade1bd..00000000 --- a/public/res/ic/outlined/external.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/res/ic/outlined/eye-blind.svg b/public/res/ic/outlined/eye-blind.svg deleted file mode 100644 index fbc8e2ae..00000000 --- a/public/res/ic/outlined/eye-blind.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/res/ic/outlined/eye.svg b/public/res/ic/outlined/eye.svg deleted file mode 100644 index 1ce868bf..00000000 --- a/public/res/ic/outlined/eye.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/res/ic/outlined/file.svg b/public/res/ic/outlined/file.svg deleted file mode 100644 index d6a2a27a..00000000 --- a/public/res/ic/outlined/file.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/flag.svg b/public/res/ic/outlined/flag.svg deleted file mode 100644 index 8fce98d6..00000000 --- a/public/res/ic/outlined/flag.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/hash-globe.svg b/public/res/ic/outlined/hash-globe.svg deleted file mode 100644 index ce3df083..00000000 --- a/public/res/ic/outlined/hash-globe.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/hash-lock.svg b/public/res/ic/outlined/hash-lock.svg deleted file mode 100644 index ae263ced..00000000 --- a/public/res/ic/outlined/hash-lock.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/hash-plus.svg b/public/res/ic/outlined/hash-plus.svg deleted file mode 100644 index 69737fd5..00000000 --- a/public/res/ic/outlined/hash-plus.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/public/res/ic/outlined/hash-search.svg b/public/res/ic/outlined/hash-search.svg deleted file mode 100644 index f135e898..00000000 --- a/public/res/ic/outlined/hash-search.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/hash-shield.svg b/public/res/ic/outlined/hash-shield.svg deleted file mode 100644 index dfd344b1..00000000 --- a/public/res/ic/outlined/hash-shield.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/hash.svg b/public/res/ic/outlined/hash.svg deleted file mode 100644 index dcb8b964..00000000 --- a/public/res/ic/outlined/hash.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/heart.svg b/public/res/ic/outlined/heart.svg deleted file mode 100644 index c5b940b6..00000000 --- a/public/res/ic/outlined/heart.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/home.svg b/public/res/ic/outlined/home.svg deleted file mode 100644 index 3c7a02df..00000000 --- a/public/res/ic/outlined/home.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/horizontal-menu.svg b/public/res/ic/outlined/horizontal-menu.svg deleted file mode 100644 index a19b3c35..00000000 --- a/public/res/ic/outlined/horizontal-menu.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/public/res/ic/outlined/inbox.svg b/public/res/ic/outlined/inbox.svg deleted file mode 100644 index 65435876..00000000 --- a/public/res/ic/outlined/inbox.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/info.svg b/public/res/ic/outlined/info.svg deleted file mode 100644 index 30a57887..00000000 --- a/public/res/ic/outlined/info.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - diff --git a/public/res/ic/outlined/invite-arrow.svg b/public/res/ic/outlined/invite-arrow.svg deleted file mode 100644 index 370bf8e8..00000000 --- a/public/res/ic/outlined/invite-arrow.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/invite-cancel-arrow.svg b/public/res/ic/outlined/invite-cancel-arrow.svg deleted file mode 100644 index 795a773a..00000000 --- a/public/res/ic/outlined/invite-cancel-arrow.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/invite.svg b/public/res/ic/outlined/invite.svg deleted file mode 100644 index 3896e15e..00000000 --- a/public/res/ic/outlined/invite.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/join-arrow.svg b/public/res/ic/outlined/join-arrow.svg deleted file mode 100644 index 90cfa651..00000000 --- a/public/res/ic/outlined/join-arrow.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/leave-arrow.svg b/public/res/ic/outlined/leave-arrow.svg deleted file mode 100644 index a51ac1d1..00000000 --- a/public/res/ic/outlined/leave-arrow.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/lock.svg b/public/res/ic/outlined/lock.svg deleted file mode 100644 index 77021f0f..00000000 --- a/public/res/ic/outlined/lock.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/markdown.svg b/public/res/ic/outlined/markdown.svg deleted file mode 100644 index 775afbfb..00000000 --- a/public/res/ic/outlined/markdown.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/message-unread.svg b/public/res/ic/outlined/message-unread.svg deleted file mode 100644 index fc5e9ff0..00000000 --- a/public/res/ic/outlined/message-unread.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - diff --git a/public/res/ic/outlined/message.svg b/public/res/ic/outlined/message.svg deleted file mode 100644 index d36e9a30..00000000 --- a/public/res/ic/outlined/message.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/public/res/ic/outlined/pause.svg b/public/res/ic/outlined/pause.svg deleted file mode 100644 index c312613b..00000000 --- a/public/res/ic/outlined/pause.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - diff --git a/public/res/ic/outlined/peace.svg b/public/res/ic/outlined/peace.svg deleted file mode 100644 index 8a7c81a3..00000000 --- a/public/res/ic/outlined/peace.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/pencil.svg b/public/res/ic/outlined/pencil.svg deleted file mode 100644 index 1b8ac24a..00000000 --- a/public/res/ic/outlined/pencil.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/photo.svg b/public/res/ic/outlined/photo.svg deleted file mode 100644 index af01a330..00000000 --- a/public/res/ic/outlined/photo.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/pin.svg b/public/res/ic/outlined/pin.svg deleted file mode 100644 index 211242cd..00000000 --- a/public/res/ic/outlined/pin.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/play.svg b/public/res/ic/outlined/play.svg deleted file mode 100644 index 87b3a8f6..00000000 --- a/public/res/ic/outlined/play.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/plus.svg b/public/res/ic/outlined/plus.svg deleted file mode 100644 index ce37594e..00000000 --- a/public/res/ic/outlined/plus.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/power.svg b/public/res/ic/outlined/power.svg deleted file mode 100644 index 8aeb6db8..00000000 --- a/public/res/ic/outlined/power.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/recent-clock.svg b/public/res/ic/outlined/recent-clock.svg deleted file mode 100644 index 30b10d59..00000000 --- a/public/res/ic/outlined/recent-clock.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - diff --git a/public/res/ic/outlined/reply-arrow.svg b/public/res/ic/outlined/reply-arrow.svg deleted file mode 100644 index 3cda01cd..00000000 --- a/public/res/ic/outlined/reply-arrow.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/search.svg b/public/res/ic/outlined/search.svg deleted file mode 100644 index 75dd6320..00000000 --- a/public/res/ic/outlined/search.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/send.svg b/public/res/ic/outlined/send.svg deleted file mode 100644 index aa487132..00000000 --- a/public/res/ic/outlined/send.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/settings.svg b/public/res/ic/outlined/settings.svg deleted file mode 100644 index ee640b39..00000000 --- a/public/res/ic/outlined/settings.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - diff --git a/public/res/ic/outlined/shield-empty.svg b/public/res/ic/outlined/shield-empty.svg deleted file mode 100644 index 6bc9d304..00000000 --- a/public/res/ic/outlined/shield-empty.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/shield-user.svg b/public/res/ic/outlined/shield-user.svg deleted file mode 100644 index bd5f07c5..00000000 --- a/public/res/ic/outlined/shield-user.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/public/res/ic/outlined/shield.svg b/public/res/ic/outlined/shield.svg deleted file mode 100644 index 9bb46fa1..00000000 --- a/public/res/ic/outlined/shield.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/space-globe.svg b/public/res/ic/outlined/space-globe.svg deleted file mode 100644 index 63d71f1d..00000000 --- a/public/res/ic/outlined/space-globe.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/space-lock.svg b/public/res/ic/outlined/space-lock.svg deleted file mode 100644 index b15705ca..00000000 --- a/public/res/ic/outlined/space-lock.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/space-plus.svg b/public/res/ic/outlined/space-plus.svg deleted file mode 100644 index 4d69a1ef..00000000 --- a/public/res/ic/outlined/space-plus.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/space.svg b/public/res/ic/outlined/space.svg deleted file mode 100644 index a4b54b3e..00000000 --- a/public/res/ic/outlined/space.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/star.svg b/public/res/ic/outlined/star.svg deleted file mode 100644 index 290f159a..00000000 --- a/public/res/ic/outlined/star.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/sticker.svg b/public/res/ic/outlined/sticker.svg deleted file mode 100644 index bc486e5e..00000000 --- a/public/res/ic/outlined/sticker.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/res/ic/outlined/sun.svg b/public/res/ic/outlined/sun.svg deleted file mode 100644 index d8ed06fd..00000000 --- a/public/res/ic/outlined/sun.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/res/ic/outlined/tick-mark.svg b/public/res/ic/outlined/tick-mark.svg deleted file mode 100644 index 8e76ed55..00000000 --- a/public/res/ic/outlined/tick-mark.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/public/res/ic/outlined/user.svg b/public/res/ic/outlined/user.svg deleted file mode 100644 index 6756a1b2..00000000 --- a/public/res/ic/outlined/user.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/public/res/ic/outlined/vertical-menu.svg b/public/res/ic/outlined/vertical-menu.svg deleted file mode 100644 index ec5c544c..00000000 --- a/public/res/ic/outlined/vertical-menu.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/public/res/ic/outlined/vlc.svg b/public/res/ic/outlined/vlc.svg deleted file mode 100644 index 8a2b844f..00000000 --- a/public/res/ic/outlined/vlc.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/public/res/ic/outlined/volume-full.svg b/public/res/ic/outlined/volume-full.svg deleted file mode 100644 index 20419e72..00000000 --- a/public/res/ic/outlined/volume-full.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - diff --git a/public/res/ic/outlined/volume-mute.svg b/public/res/ic/outlined/volume-mute.svg deleted file mode 100644 index beb06771..00000000 --- a/public/res/ic/outlined/volume-mute.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/src/app/atoms/avatar/Avatar.jsx b/src/app/atoms/avatar/Avatar.jsx deleted file mode 100644 index 27bf7c90..00000000 --- a/src/app/atoms/avatar/Avatar.jsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './Avatar.scss'; - -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) => { - let textSize = 's1'; - if (size === 'large') textSize = 'h1'; - if (size === 'small') textSize = 'b1'; - if (size === 'extra-small') textSize = 'b3'; - - return ( -
- {imageSrc !== null ? ( - { - e.target.style.backgroundColor = 'transparent'; - }} - onError={(e) => { - e.target.src = ImageBrokenSVG; - }} - alt="" - /> - ) : ( - - {iconSrc !== null ? ( - - ) : ( - text !== null && ( - - {avatarInitials(text)} - - ) - )} - - )} -
- ); -}); - -Avatar.defaultProps = { - text: null, - bgColor: 'transparent', - iconSrc: null, - iconColor: null, - imageSrc: null, - size: 'normal', -}; - -Avatar.propTypes = { - text: PropTypes.string, - bgColor: PropTypes.string, - iconSrc: PropTypes.string, - iconColor: PropTypes.string, - imageSrc: PropTypes.string, - size: PropTypes.oneOf(['large', 'normal', 'small', 'extra-small']), -}; - -export default Avatar; diff --git a/src/app/atoms/avatar/Avatar.scss b/src/app/atoms/avatar/Avatar.scss deleted file mode 100644 index ea69c9e8..00000000 --- a/src/app/atoms/avatar/Avatar.scss +++ /dev/null @@ -1,56 +0,0 @@ -@use '../../partials/flex'; - -.avatar-container { - display: inline-flex; - width: 42px; - height: 42px; - border-radius: var(--bo-radius); - position: relative; - - &__large { - width: var(--av-large); - height: var(--av-large); - } - &__normal { - width: var(--av-normal); - height: var(--av-normal); - } - - &__small { - width: var(--av-small); - height: var(--av-small); - } - - &__extra-small { - width: var(--av-extra-small); - height: var(--av-extra-small); - } - - > img { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: inherit; - background-color: var(--bg-surface-hover); - } - - .avatar__border { - @extend .cp-fx__row--c-c; - - position: absolute; - top: 0; - left: 0; - - width: 100%; - height: 100%; - border-radius: inherit; - - .text { - color: white; - } - &--active { - @extend .avatar__border; - box-shadow: var(--bs-surface-border); - } - } -} \ No newline at end of file diff --git a/src/app/atoms/avatar/render.js b/src/app/atoms/avatar/render.js deleted file mode 100644 index e8cf1a66..00000000 --- a/src/app/atoms/avatar/render.js +++ /dev/null @@ -1,57 +0,0 @@ -import { avatarInitials, cssVar } from '../../../util/common'; - -// renders the avatar and returns it as an URL -export default async function renderAvatar({ - text, bgColor, imageSrc, size, borderRadius, scale, -}) { - try { - const canvas = document.createElement('canvas'); - canvas.width = size * scale; - canvas.height = size * scale; - - const ctx = canvas.getContext('2d'); - - ctx.scale(scale, scale); - - // rounded corners - ctx.beginPath(); - ctx.moveTo(size, size); - ctx.arcTo(0, size, 0, 0, borderRadius); - ctx.arcTo(0, 0, size, 0, borderRadius); - ctx.arcTo(size, 0, size, size, borderRadius); - ctx.arcTo(size, size, 0, size, borderRadius); - - if (imageSrc) { - // clip corners of image - ctx.closePath(); - ctx.clip(); - - const img = new Image(); - img.crossOrigin = 'anonymous'; - const promise = new Promise((resolve, reject) => { - img.onerror = reject; - img.onload = resolve; - }); - img.src = imageSrc; - await promise; - - ctx.drawImage(img, 0, 0, size, size); - } else { - // colored background - ctx.fillStyle = cssVar(bgColor); - ctx.fill(); - - // centered letter - ctx.fillStyle = '#fff'; - ctx.font = `${cssVar('--fs-s1')} ${cssVar('--font-primary')}`; - ctx.textBaseline = 'middle'; - ctx.textAlign = 'center'; - ctx.fillText(avatarInitials(text), size / 2, size / 2); - } - - return canvas.toDataURL(); - } catch (e) { - console.error(e); - return imageSrc; - } -} diff --git a/src/app/atoms/badge/NotificationBadge.jsx b/src/app/atoms/badge/NotificationBadge.jsx deleted file mode 100644 index 12c1bd44..00000000 --- a/src/app/atoms/badge/NotificationBadge.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './NotificationBadge.scss'; - -import Text from '../text/Text'; - -function NotificationBadge({ alert, content }) { - const notificationClass = alert ? ' notification-badge--alert' : ''; - return ( -
- {content !== null && {content}} -
- ); -} - -NotificationBadge.defaultProps = { - alert: false, - content: null, -}; - -NotificationBadge.propTypes = { - alert: PropTypes.bool, - content: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), -}; - -export default NotificationBadge; diff --git a/src/app/atoms/badge/NotificationBadge.scss b/src/app/atoms/badge/NotificationBadge.scss deleted file mode 100644 index f5cfa73f..00000000 --- a/src/app/atoms/badge/NotificationBadge.scss +++ /dev/null @@ -1,21 +0,0 @@ -.notification-badge { - min-width: 16px; - min-height: 8px; - padding: 0 var(--sp-ultra-tight); - background-color: var(--bg-badge); - border-radius: var(--bo-radius); - - .text { - color: var(--tc-badge); - text-align: center; - } - - &--alert { - background-color: var(--bg-positive); - } - - &:empty { - min-width: 8px; - margin: 0 var(--sp-ultra-tight); - } -} \ No newline at end of file diff --git a/src/app/atoms/button/Button.jsx b/src/app/atoms/button/Button.jsx deleted file mode 100644 index 1c1c950c..00000000 --- a/src/app/atoms/button/Button.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './Button.scss'; - -import Text from '../text/Text'; -import RawIcon from '../system-icons/RawIcon'; -import { blurOnBubbling } from './script'; - -const Button = React.forwardRef(({ - id, className, variant, iconSrc, - type, onClick, children, disabled, -}, ref) => { - const iconClass = (iconSrc === null) ? '' : `btn-${variant}--icon`; - return ( - - ); -}); - -Button.defaultProps = { - id: '', - className: null, - variant: 'surface', - iconSrc: null, - type: 'button', - onClick: null, - disabled: false, -}; - -Button.propTypes = { - id: PropTypes.string, - className: PropTypes.string, - variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']), - iconSrc: PropTypes.string, - type: PropTypes.oneOf(['button', 'submit', 'reset']), - onClick: PropTypes.func, - children: PropTypes.node.isRequired, - disabled: PropTypes.bool, -}; - -export default Button; diff --git a/src/app/atoms/button/Button.scss b/src/app/atoms/button/Button.scss deleted file mode 100644 index e1a01bb0..00000000 --- a/src/app/atoms/button/Button.scss +++ /dev/null @@ -1,81 +0,0 @@ -@use 'state'; -@use '../../partials/dir'; -@use '../../partials/text'; - -.btn-surface, -.btn-primary, -.btn-positive, -.btn-caution, -.btn-danger { - display: inline-flex; - align-items: center; - justify-content: center; - - min-width: 80px; - padding: var(--sp-extra-tight) var(--sp-normal); - background-color: transparent; - border: none; - border-radius: var(--bo-radius); - cursor: pointer; - @include state.disabled; - - & .text { - @extend .cp-txt__ellipsis; - } - - &--icon { - @include dir.side(padding, var(--sp-tight), var(--sp-loose)); - - } - .ic-raw { - @include dir.side(margin, 0, var(--sp-extra-tight)); - flex-shrink: 0; - } -} - -@mixin color($textColor, $iconColor) { - .text { - color: $textColor; - } - .ic-raw { - background-color: $iconColor; - } -} - - -.btn-surface { - box-shadow: var(--bs-surface-border); - @include color(var(--tc-surface-high), var(--ic-surface-normal)); - @include state.hover(var(--bg-surface-hover)); - @include state.focus(var(--bs-surface-outline)); - @include state.active(var(--bg-surface-active)); -} - -.btn-primary { - background-color: var(--bg-primary); - @include color(var(--tc-primary-high), var(--ic-primary-normal)); - @include state.hover(var(--bg-primary-hover)); - @include state.focus(var(--bs-primary-outline)); - @include state.active(var(--bg-primary-active)); -} -.btn-positive { - box-shadow: var(--bs-positive-border); - @include color(var(--tc-positive-high), var(--ic-positive-normal)); - @include state.hover(var(--bg-positive-hover)); - @include state.focus(var(--bs-positive-outline)); - @include state.active(var(--bg-positive-active)); -} -.btn-caution { - box-shadow: var(--bs-caution-border); - @include color(var(--tc-caution-high), var(--ic-caution-normal)); - @include state.hover(var(--bg-caution-hover)); - @include state.focus(var(--bs-caution-outline)); - @include state.active(var(--bg-caution-active)); -} -.btn-danger { - box-shadow: var(--bs-danger-border); - @include color(var(--tc-danger-high), var(--ic-danger-normal)); - @include state.hover(var(--bg-danger-hover)); - @include state.focus(var(--bs-danger-outline)); - @include state.active(var(--bg-danger-active)); -} \ No newline at end of file diff --git a/src/app/atoms/button/Checkbox.jsx b/src/app/atoms/button/Checkbox.jsx deleted file mode 100644 index 7fcea3b5..00000000 --- a/src/app/atoms/button/Checkbox.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './Checkbox.scss'; - -function Checkbox({ - variant, isActive, onToggle, - disabled, tabIndex, -}) { - const className = `checkbox checkbox-${variant}${isActive ? ' checkbox--active' : ''}`; - if (onToggle === null) return ; - return ( - // eslint-disable-next-line jsx-a11y/control-has-associated-label - - ); - if (tooltip === null) return btn; - return ( - {tooltip}} - > - {btn} - - ); -}); - -IconButton.defaultProps = { - variant: 'surface', - size: 'normal', - type: 'button', - tooltip: null, - tooltipPlacement: 'top', - onClick: null, - tabIndex: 0, - disabled: false, - isImage: false, - className: '', -}; - -IconButton.propTypes = { - variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']), - size: PropTypes.oneOf(['normal', 'small', 'extra-small']), - type: PropTypes.oneOf(['button', 'submit', 'reset']), - tooltip: PropTypes.string, - tooltipPlacement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), - src: PropTypes.string.isRequired, - onClick: PropTypes.func, - tabIndex: PropTypes.number, - disabled: PropTypes.bool, - isImage: PropTypes.bool, - className: PropTypes.string, -}; - -export default IconButton; diff --git a/src/app/atoms/button/IconButton.scss b/src/app/atoms/button/IconButton.scss deleted file mode 100644 index aa6480c0..00000000 --- a/src/app/atoms/button/IconButton.scss +++ /dev/null @@ -1,56 +0,0 @@ -@use 'state'; - -.ic-btn { - padding: var(--sp-extra-tight); - border: none; - border-radius: var(--bo-radius); - background-color: transparent; - font-size: 0; - line-height: 0; - cursor: pointer; - @include state.disabled; -} - -@mixin color($color) { - .ic-raw { - background-color: $color; - } -} -@mixin focus($color) { - &:focus { - outline: none; - background-color: $color; - } -} - -.ic-btn-surface { - @include color(var(--ic-surface-normal)); - @include state.hover(var(--bg-surface-hover)); - @include focus(var(--bg-surface-hover)); - @include state.active(var(--bg-surface-active)); -} -.ic-btn-primary { - @include color(var(--ic-primary-normal)); - @include state.hover(var(--bg-primary-hover)); - @include focus(var(--bg-primary-hover)); - @include state.active(var(--bg-primary-active)); - background-color: var(--bg-primary); -} -.ic-btn-positive { - @include color(var(--ic-positive-normal)); - @include state.hover(var(--bg-positive-hover)); - @include focus(var(--bg-positive-hover)); - @include state.active(var(--bg-positive-active)); -} -.ic-btn-caution { - @include color(var(--ic-caution-normal)); - @include state.hover(var(--bg-caution-hover)); - @include focus(var(--bg-caution-hover)); - @include state.active(var(--bg-caution-active)); -} -.ic-btn-danger { - @include color(var(--ic-danger-normal)); - @include state.hover(var(--bg-danger-hover)); - @include focus(var(--bg-danger-hover)); - @include state.active(var(--bg-danger-active)); -} \ No newline at end of file diff --git a/src/app/atoms/button/RadioButton.jsx b/src/app/atoms/button/RadioButton.jsx deleted file mode 100644 index 35b68baf..00000000 --- a/src/app/atoms/button/RadioButton.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './RadioButton.scss'; - -function RadioButton({ isActive, onToggle, disabled }) { - if (onToggle === null) return ; - return ( - // eslint-disable-next-line jsx-a11y/control-has-associated-label - - ); -} - -Chip.propTypes = { - iconSrc: PropTypes.string, - iconColor: PropTypes.string, - text: PropTypes.string, - children: PropTypes.element, - onClick: PropTypes.func, -}; - -Chip.defaultProps = { - iconSrc: null, - iconColor: null, - text: null, - children: null, - onClick: null, -}; - -export default Chip; diff --git a/src/app/atoms/chip/Chip.scss b/src/app/atoms/chip/Chip.scss deleted file mode 100644 index 7396b0dc..00000000 --- a/src/app/atoms/chip/Chip.scss +++ /dev/null @@ -1,31 +0,0 @@ -@use '../../partials/dir'; - -.chip { - padding: var(--sp-ultra-tight) var(--sp-extra-tight); - - display: inline-flex; - flex-direction: row; - align-items: center; - - background: var(--bg-surface-low); - border-radius: var(--bo-radius); - box-shadow: var(--bs-surface-border); - cursor: pointer; - - @media (hover: hover) { - &:hover { - background-color: var(--bg-surface-hover); - } - } - - & > .text { - flex: 1; - color: var(--tc-surface-high); - } - - & > .ic-raw { - width: 16px; - height: 16px; - @include dir.side(margin, 0, var(--sp-ultra-tight)); - } -} \ No newline at end of file diff --git a/src/app/atoms/context-menu/ContextMenu.jsx b/src/app/atoms/context-menu/ContextMenu.jsx deleted file mode 100644 index 7d1acd44..00000000 --- a/src/app/atoms/context-menu/ContextMenu.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import './ContextMenu.scss'; - -import Tippy from '@tippyjs/react'; -import 'tippy.js/animations/scale-extreme.css'; - -import Text from '../text/Text'; -import Button from '../button/Button'; -import ScrollView from '../scroll/ScrollView'; - -function ContextMenu({ - content, placement, maxWidth, render, afterToggle, -}) { - const [isVisible, setVisibility] = useState(false); - const showMenu = () => setVisibility(true); - const hideMenu = () => setVisibility(false); - - useEffect(() => { - if (afterToggle !== null) afterToggle(isVisible); - }, [isVisible]); - - return ( - {typeof content === 'function' ? content(hideMenu) : content}} - placement={placement} - interactive - arrow={false} - maxWidth={maxWidth} - duration={200} - > - {render(isVisible ? hideMenu : showMenu)} - - ); -} - -ContextMenu.defaultProps = { - maxWidth: 'unset', - placement: 'right', - afterToggle: null, -}; - -ContextMenu.propTypes = { - content: PropTypes.oneOfType([ - PropTypes.node, - PropTypes.func, - ]).isRequired, - placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), - maxWidth: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), - render: PropTypes.func.isRequired, - afterToggle: PropTypes.func, -}; - -function MenuHeader({ children }) { - return ( -
- { children } -
- ); -} - -MenuHeader.propTypes = { - children: PropTypes.node.isRequired, -}; - -function MenuItem({ - variant, iconSrc, type, - onClick, children, disabled, -}) { - return ( -
- -
- ); -} - -MenuItem.defaultProps = { - variant: 'surface', - iconSrc: null, - type: 'button', - disabled: false, - onClick: null, -}; - -MenuItem.propTypes = { - variant: PropTypes.oneOf(['surface', 'positive', 'caution', 'danger']), - iconSrc: PropTypes.string, - type: PropTypes.oneOf(['button', 'submit']), - onClick: PropTypes.func, - children: PropTypes.node.isRequired, - disabled: PropTypes.bool, -}; - -function MenuBorder() { - return
; -} - -export { - ContextMenu as default, MenuHeader, MenuItem, MenuBorder, -}; diff --git a/src/app/atoms/context-menu/ContextMenu.scss b/src/app/atoms/context-menu/ContextMenu.scss deleted file mode 100644 index 2df9f0a4..00000000 --- a/src/app/atoms/context-menu/ContextMenu.scss +++ /dev/null @@ -1,81 +0,0 @@ -@use '../../partials/flex'; -@use '../../partials/text'; -@use '../../partials/dir'; - -.context-menu { - background-color: var(--bg-surface); - box-shadow: var(--bs-popup); - border-radius: var(--bo-radius); - overflow: hidden; - - &:focus { - outline: none; - } - & .tippy-content > div > .scrollbar { - max-height: 90vh; - } -} - -.context-menu__click-wrapper { - display: inline-flex; - - &:focus { - outline: none; - } -} - -.context-menu__header { - height: 34px; - padding: 0 var(--sp-normal); - margin-bottom: var(--sp-ultra-tight); - display: flex; - align-items: center; - border-bottom: 1px solid var(--bg-surface-border); - - .text { - @extend .cp-txt__ellipsis; - color: var(--tc-surface-low); - } - - &:not(:first-child) { - margin-top: var(--sp-extra-tight); - border-top: 1px solid var(--bg-surface-border); - } -} - -.context-menu__item { - display: flex; - button[class^="btn"] { - @extend .cp-fx__item-one; - justify-content: flex-start; - border-radius: 0; - box-shadow: none; - white-space: nowrap; - padding: var(--sp-extra-tight) var(--sp-normal); - - & > .ic-raw { - @include dir.side(margin, 0, var(--sp-tight)); - } - - // if item doesn't have icon - .text:first-child { - @include dir.side( - margin, - calc(var(--ic-small) + var(--sp-tight)), - 0 - ); - } - } - .btn-surface:focus { - background-color: var(--bg-surface-hover); - } - .btn-positive:focus { - background-color: var(--bg-positive-hover); - } - .btn-caution:focus { - background-color: var(--bg-caution-hover); - } - .btn-danger:focus { - background-color: var(--bg-danger-hover); - } -} \ No newline at end of file diff --git a/src/app/atoms/context-menu/ReusableContextMenu.jsx b/src/app/atoms/context-menu/ReusableContextMenu.jsx deleted file mode 100644 index 59bdb142..00000000 --- a/src/app/atoms/context-menu/ReusableContextMenu.jsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; - -import cons from '../../../client/state/cons'; -import navigation from '../../../client/state/navigation'; - -import ContextMenu from './ContextMenu'; - -let key = null; -function ReusableContextMenu() { - const [data, setData] = useState(null); - const openerRef = useRef(null); - - const closeMenu = () => { - key = null; - if (data) openerRef.current.click(); - }; - - useEffect(() => { - if (data) { - const { cords } = data; - openerRef.current.style.transform = `translate(${cords.x}px, ${cords.y}px)`; - openerRef.current.style.width = `${cords.width}px`; - openerRef.current.style.height = `${cords.height}px`; - openerRef.current.click(); - } - const handleContextMenuOpen = (placement, cords, render, afterClose) => { - if (key) { - closeMenu(); - return; - } - setData({ - placement, cords, render, afterClose, - }); - }; - navigation.on(cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED, handleContextMenuOpen); - return () => { - navigation.removeListener( - cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED, - handleContextMenuOpen, - ); - }; - }, [data]); - - const handleAfterToggle = (isVisible) => { - if (isVisible) { - key = Math.random(); - return; - } - data?.afterClose?.(); - if (setData) setData(null); - - if (key === null) return; - const copyKey = key; - setTimeout(() => { - if (key === copyKey) key = null; - }, 200); - }; - - return ( - ( - - )} - /> - ); -} - -export default ReusableContextMenu; diff --git a/src/app/atoms/divider/Divider.jsx b/src/app/atoms/divider/Divider.jsx deleted file mode 100644 index 76721241..00000000 --- a/src/app/atoms/divider/Divider.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './Divider.scss'; - -import Text from '../text/Text'; - -function Divider({ text, variant, align }) { - const dividerClass = ` divider--${variant} divider--${align}`; - return ( -
- {text !== null && {text}} -
- ); -} - -Divider.defaultProps = { - text: null, - variant: 'surface', - align: 'center', -}; - -Divider.propTypes = { - text: PropTypes.string, - variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']), - align: PropTypes.oneOf(['left', 'center', 'right']), -}; - -export default Divider; diff --git a/src/app/atoms/divider/Divider.scss b/src/app/atoms/divider/Divider.scss deleted file mode 100644 index 0f013ff0..00000000 --- a/src/app/atoms/divider/Divider.scss +++ /dev/null @@ -1,68 +0,0 @@ -.divider-line { - content: ''; - display: inline-block; - flex: 1; - border-bottom: 1px solid var(--local-divider-color); - opacity: var(--local-divider-opacity); -} - -.divider { - display: flex; - align-items: center; - - &--center::before, - &--right::before { - @extend .divider-line; - } - &--center::after, - &--left::after { - @extend .divider-line; - } - - &__text { - padding: 2px var(--sp-extra-tight); - border-radius: calc(var(--bo-radius) / 2); - } -} - -.divider--surface { - --local-divider-color: var(--bg-divider); - --local-divider-opacity: 1; - - .divider__text { - color: var(--tc-surface-low); - border: 1px solid var(--bg-divider); - } -} -.divider--primary { - --local-divider-color: var(--bg-primary); - --local-divider-opacity: .8; - .divider__text { - color: var(--tc-primary-high); - background-color: var(--bg-primary); - } -} -.divider--positive { - --local-divider-color: var(--bg-positive); - --local-divider-opacity: .8; - .divider__text { - color: var(--bg-surface); - background-color: var(--bg-positive); - } -} -.divider--danger { - --local-divider-color: var(--bg-danger); - --local-divider-opacity: .8; - .divider__text { - color: var(--bg-surface); - background-color: var(--bg-danger); - } -} -.divider--caution { - --local-divider-color: var(--bg-caution); - --local-divider-opacity: .8; - .divider__text { - color: var(--bg-surface); - background-color: var(--bg-caution); - } -} diff --git a/src/app/atoms/header/Header.jsx b/src/app/atoms/header/Header.jsx deleted file mode 100644 index 3c81e423..00000000 --- a/src/app/atoms/header/Header.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './Header.scss'; - -function Header({ children }) { - return ( -
- {children} -
- ); -} - -Header.propTypes = { - children: PropTypes.node.isRequired, -}; - -function TitleWrapper({ children }) { - return ( -
- {children} -
- ); -} - -TitleWrapper.propTypes = { - children: PropTypes.node.isRequired, -}; - -export { Header as default, TitleWrapper }; diff --git a/src/app/atoms/header/Header.scss b/src/app/atoms/header/Header.scss deleted file mode 100644 index 9e45f824..00000000 --- a/src/app/atoms/header/Header.scss +++ /dev/null @@ -1,43 +0,0 @@ -@use '../../partials/text'; -@use '../../partials/dir'; - -.header { - @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight)); - width: 100%; - height: var(--header-height); - border-bottom: 1px solid var(--bg-surface-border); - display: flex; - align-items: center; - - &__title-wrapper { - flex: 1; - min-width: 0; - display: flex; - align-items: center; - margin: 0 var(--sp-tight); - - &:first-child { - @include dir.side(margin, 0, var(--sp-tight)); - } - - & > .text:first-child { - @extend .cp-txt__ellipsis; - min-width: 0; - } - & > .text-b3{ - flex: 1; - min-width: 0; - - margin-top: var(--sp-ultra-tight); - @include dir.side(margin, var(--sp-tight), 0); - @include dir.side(padding, var(--sp-tight), 0); - @include dir.side(border, 1px solid var(--bg-surface-border), none); - - max-height: calc(2 * var(--lh-b3)); - overflow: hidden; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - display: -webkit-box; - } - } -} \ No newline at end of file diff --git a/src/app/atoms/input/Input.jsx b/src/app/atoms/input/Input.jsx deleted file mode 100644 index 96c94967..00000000 --- a/src/app/atoms/input/Input.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './Input.scss'; - -import TextareaAutosize from 'react-autosize-textarea'; - -function Input({ - id, label, name, value, placeholder, - required, type, onChange, forwardRef, - resizable, minHeight, onResize, state, - onKeyDown, disabled, autoFocus, -}) { - return ( -
- { label !== '' && } - { resizable - ? ( - - ) : ( - - )} -
- ); -} - -Input.defaultProps = { - id: null, - name: '', - label: '', - value: '', - placeholder: '', - type: 'text', - required: false, - onChange: null, - forwardRef: null, - resizable: false, - minHeight: 46, - onResize: null, - state: 'normal', - onKeyDown: null, - disabled: false, - autoFocus: false, -}; - -Input.propTypes = { - id: PropTypes.string, - name: PropTypes.string, - label: PropTypes.string, - value: PropTypes.string, - placeholder: PropTypes.string, - required: PropTypes.bool, - type: PropTypes.string, - onChange: PropTypes.func, - forwardRef: PropTypes.shape({}), - resizable: PropTypes.bool, - minHeight: PropTypes.number, - onResize: PropTypes.func, - state: PropTypes.oneOf(['normal', 'success', 'error']), - onKeyDown: PropTypes.func, - disabled: PropTypes.bool, - autoFocus: PropTypes.bool, -}; - -export default Input; diff --git a/src/app/atoms/input/Input.scss b/src/app/atoms/input/Input.scss deleted file mode 100644 index 40fe43ec..00000000 --- a/src/app/atoms/input/Input.scss +++ /dev/null @@ -1,52 +0,0 @@ -@use '../../atoms/scroll/scrollbar'; - -.input { - display: block; - width: 100%; - min-width: 0px; - margin: 0; - padding: var(--sp-tight) var(--sp-normal); - background-color: var(--bg-surface-low); - color: var(--tc-surface-normal); - box-shadow: none; - border-radius: var(--bo-radius); - border: 1px solid var(--bg-surface-border); - font-size: var(--fs-b2); - letter-spacing: var(--ls-b2); - line-height: var(--lh-b2); - - :disabled { - opacity: 0.4; - cursor: no-drop; - } - - &__label { - display: inline-block; - margin-bottom: var(--sp-ultra-tight); - color: var(--tc-surface-low); - } - - &--resizable { - resize: vertical !important; - overflow-y: auto !important; - @include scrollbar.scroll; - @include scrollbar.scroll__v; - @include scrollbar.scroll--auto-hide; - } - &--success { - border: 1px solid var(--bg-positive); - box-shadow: none !important; - } - &--error { - border: 1px solid var(--bg-danger); - box-shadow: none !important; - } - - &:focus { - outline: none; - box-shadow: var(--bs-primary-border); - } - &::placeholder { - color: var(--tc-surface-low) - } -} \ No newline at end of file diff --git a/src/app/atoms/modal/RawModal.jsx b/src/app/atoms/modal/RawModal.jsx deleted file mode 100644 index 450be0e0..00000000 --- a/src/app/atoms/modal/RawModal.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; -import './RawModal.scss'; - -import Modal from 'react-modal'; - -import navigation from '../../../client/state/navigation'; - -Modal.setAppElement('#root'); - -function RawModal({ - className, overlayClassName, - isOpen, size, onAfterOpen, onAfterClose, - onRequestClose, closeFromOutside, children, -}) { - let modalClass = (className !== null) ? `${className} ` : ''; - switch (size) { - case 'large': - modalClass += 'raw-modal__large '; - break; - case 'medium': - modalClass += 'raw-modal__medium '; - break; - case 'small': - default: - modalClass += 'raw-modal__small '; - } - - useEffect(() => { - navigation.setIsRawModalVisible(isOpen); - }, [isOpen]); - - const modalOverlayClass = (overlayClassName !== null) ? `${overlayClassName} ` : ''; - return ( - - {children} - - ); -} - -RawModal.defaultProps = { - className: null, - overlayClassName: null, - size: 'small', - onAfterOpen: null, - onAfterClose: null, - onRequestClose: null, - closeFromOutside: true, -}; - -RawModal.propTypes = { - className: PropTypes.string, - overlayClassName: PropTypes.string, - isOpen: PropTypes.bool.isRequired, - size: PropTypes.oneOf(['large', 'medium', 'small']), - onAfterOpen: PropTypes.func, - onAfterClose: PropTypes.func, - onRequestClose: PropTypes.func, - closeFromOutside: PropTypes.bool, - children: PropTypes.node.isRequired, -}; - -export default RawModal; diff --git a/src/app/atoms/modal/RawModal.scss b/src/app/atoms/modal/RawModal.scss deleted file mode 100644 index d612a4bc..00000000 --- a/src/app/atoms/modal/RawModal.scss +++ /dev/null @@ -1,66 +0,0 @@ -.raw-modal { - --small-modal-width: 525px; - --medium-modal-width: 712px; - --large-modal-width: 1024px; - - position: relative; - width: 100%; - max-height: 100%; - border-radius: var(--bo-radius); - box-shadow: var(--bs-popup); - outline: none; - overflow: hidden; - - &__small { - max-width: var(--small-modal-width); - } - &__medium { - max-width: var(--medium-modal-width); - } - &__large { - max-width: var(--large-modal-width); - } - - &__overlay { - position: fixed; - top: 0; - left: 0; - z-index: 999; - - display: flex; - justify-content: center; - align-items: center; - - padding: var(--sp-normal); - width: 100%; - height: 100%; - background-color: var(--bg-overlay); - } -} - -.ReactModal__Overlay { - animation: raw-modal--overlay 150ms; -} - -.ReactModal__Content { - animation: raw-modal--content 150ms; -} - -@keyframes raw-modal--content { - 0% { - transform: translateY(100px); - opacity: .5; - } - 100% { - transform: translateY(0); - opacity: 1; - } -} -@keyframes raw-modal--overlay { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} diff --git a/src/app/atoms/scroll/ScrollView.jsx b/src/app/atoms/scroll/ScrollView.jsx deleted file mode 100644 index 26c0c83a..00000000 --- a/src/app/atoms/scroll/ScrollView.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './ScrollView.scss'; - -const ScrollView = React.forwardRef(({ - horizontal, vertical, autoHide, invisible, onScroll, children, -}, ref) => { - let scrollbarClasses = ''; - if (horizontal) scrollbarClasses += ' scrollbar__h'; - if (vertical) scrollbarClasses += ' scrollbar__v'; - if (autoHide) scrollbarClasses += ' scrollbar--auto-hide'; - if (invisible) scrollbarClasses += ' scrollbar--invisible'; - return ( -
- {children} -
- ); -}); - -ScrollView.defaultProps = { - horizontal: false, - vertical: true, - autoHide: false, - invisible: false, - onScroll: null, -}; - -ScrollView.propTypes = { - horizontal: PropTypes.bool, - vertical: PropTypes.bool, - autoHide: PropTypes.bool, - invisible: PropTypes.bool, - onScroll: PropTypes.func, - children: PropTypes.node.isRequired, -}; - -export default ScrollView; diff --git a/src/app/atoms/scroll/ScrollView.scss b/src/app/atoms/scroll/ScrollView.scss deleted file mode 100644 index 251037e1..00000000 --- a/src/app/atoms/scroll/ScrollView.scss +++ /dev/null @@ -1,31 +0,0 @@ -@use '../../partials/dir'; -@use '_scrollbar'; - -@mixin paddingForSafari($padding) { - @media not all and (min-resolution:.001dpcm) { - @include dir.side(padding, 0, $padding); - } -} - -.scrollbar { - width: 100%; - height: 100%; - @include scrollbar.scroll; - @include paddingForSafari(var(--sp-extra-tight)); - - &__h { - @include scrollbar.scroll__h; - } - - &__v { - @include scrollbar.scroll__v; - } - - &--auto-hide { - @include scrollbar.scroll--auto-hide; - } - &--invisible { - @include scrollbar.scroll--invisible; - @include paddingForSafari(0); - } -} \ No newline at end of file diff --git a/src/app/atoms/scroll/_scrollbar.scss b/src/app/atoms/scroll/_scrollbar.scss deleted file mode 100644 index 78ec75ad..00000000 --- a/src/app/atoms/scroll/_scrollbar.scss +++ /dev/null @@ -1,65 +0,0 @@ -.firefox-scrollbar { - scrollbar-width: thin; - scrollbar-color: var(--bg-surface-hover) transparent; - &--transparent { - scrollbar-color: transparent transparent; - } -} -.webkit-scrollbar { - &::-webkit-scrollbar { - width: 8px; - height: 8px; - } -} -.webkit-scrollbar-track { - &::-webkit-scrollbar-track { - background-color: transparent; - } -} -.webkit-scrollbar-thumb { - &::-webkit-scrollbar-thumb { - background-color: var(--bg-surface-hover); - } - &::-webkit-scrollbar-thumb:hover { - background-color: var(--bg-surface-active); - } - &--transparent { - &::-webkit-scrollbar-thumb { - background-color: transparent; - } - } -} - -@mixin scroll { - overflow: hidden; - // Below code stop scroll when x-scrollable content come in timeline - // overscroll-behavior: none; - @extend .firefox-scrollbar; - @extend .webkit-scrollbar; - @extend .webkit-scrollbar-track; - @extend .webkit-scrollbar-thumb; -} - -@mixin scroll__h { - overflow-x: scroll; -} -@mixin scroll__v { - overflow-y: scroll; -} -@mixin scroll--auto-hide { - @extend .firefox-scrollbar--transparent; - @extend .webkit-scrollbar-thumb--transparent; - - &:hover { - @extend .firefox-scrollbar; - @extend .webkit-scrollbar-thumb; - } -} -@mixin scroll--invisible { - -ms-overflow-style: none; - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } -} \ No newline at end of file diff --git a/src/app/atoms/segmented-controls/SegmentedControls.jsx b/src/app/atoms/segmented-controls/SegmentedControls.jsx deleted file mode 100644 index 1d54dd06..00000000 --- a/src/app/atoms/segmented-controls/SegmentedControls.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import './SegmentedControls.scss'; - -import { blurOnBubbling } from '../button/script'; - -import Text from '../text/Text'; -import RawIcon from '../system-icons/RawIcon'; - -function SegmentedControls({ - selected, segments, onSelect, -}) { - const [select, setSelect] = useState(selected); - - function selectSegment(segmentIndex) { - setSelect(segmentIndex); - onSelect(segmentIndex); - } - - useEffect(() => { - setSelect(selected); - }, [selected]); - - return ( -
- { - segments.map((segment, index) => ( - - )) - } -
- ); -} - -SegmentedControls.propTypes = { - selected: PropTypes.number.isRequired, - segments: PropTypes.arrayOf(PropTypes.shape({ - iconSrc: PropTypes.string, - text: PropTypes.string, - })).isRequired, - onSelect: PropTypes.func.isRequired, -}; - -export default SegmentedControls; diff --git a/src/app/atoms/segmented-controls/SegmentedControls.scss b/src/app/atoms/segmented-controls/SegmentedControls.scss deleted file mode 100644 index fb1fd987..00000000 --- a/src/app/atoms/segmented-controls/SegmentedControls.scss +++ /dev/null @@ -1,57 +0,0 @@ -@use '../button/state'; -@use '../../partials/dir'; - -.segmented-controls { - background-color: var(--bg-surface-low); - border-radius: var(--bo-radius); - border: 1px solid var(--bg-surface-border); - - display: inline-flex; - overflow: hidden; -} - -.segment-btn { - padding: var(--sp-extra-tight) 0; - cursor: pointer; - @include state.hover(var(--bg-surface-hover)); - @include state.active(var(--bg-surface-active)); - - &__base { - padding: 0 var(--sp-normal); - display: flex; - align-items: center; - justify-content: center; - @include dir.side(border, 1px solid var(--bg-surface-border), none); - - & .text:nth-child(2) { - margin: 0 var(--sp-extra-tight); - } - } - &:first-child &__base { - border: none; - } - - &--active { - background-color: var(--bg-surface); - border: 1px solid var(--bg-surface-border); - border-width: 0 1px 0 1px; - - & .segment-btn__base, - & + .segment-btn .segment-btn__base { - border: none; - } - &:first-child{ - border-left: none; - } - &:last-child { - border-right: none; - } - [dir=rtl] & { - border-left: 1px solid var(--bg-surface-border); - border-right: 1px solid var(--bg-surface-border); - - &:first-child { border-right: none;} - &:last-child { border-left: none;} - } - } -} \ No newline at end of file diff --git a/src/app/atoms/spinner/Spinner.jsx b/src/app/atoms/spinner/Spinner.jsx deleted file mode 100644 index 61c9747e..00000000 --- a/src/app/atoms/spinner/Spinner.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './Spinner.scss'; - -function Spinner({ size }) { - return ( -
- ); -} - -Spinner.defaultProps = { - size: 'normal', -}; - -Spinner.propTypes = { - size: PropTypes.oneOf(['normal', 'small']), -}; - -export default Spinner; diff --git a/src/app/atoms/spinner/Spinner.scss b/src/app/atoms/spinner/Spinner.scss deleted file mode 100644 index 73fbf676..00000000 --- a/src/app/atoms/spinner/Spinner.scss +++ /dev/null @@ -1,22 +0,0 @@ -.donut-spinner { - display: inline-block; - border: 4px solid var(--bg-surface-border); - border-left-color: var(--tc-surface-normal); - border-radius: 50%; - animation: donut-spin 1.2s cubic-bezier(0.73, 0.32, 0.67, 0.86) infinite; - - &--normal { - width: 40px; - height: 40px; - } - &--small { - width: 28px; - height: 28px; - } -} - -@keyframes donut-spin { - to { - transform: rotate(1turn); - } -} \ No newline at end of file diff --git a/src/app/atoms/system-icons/RawIcon.jsx b/src/app/atoms/system-icons/RawIcon.jsx deleted file mode 100644 index a6697f4f..00000000 --- a/src/app/atoms/system-icons/RawIcon.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './RawIcon.scss'; - -function RawIcon({ color, size, src, isImage }) { - const style = {}; - if (color !== null) style.backgroundColor = color; - if (isImage) { - style.backgroundColor = 'transparent'; - style.backgroundImage = `url("${src}")`; - } else { - style.WebkitMaskImage = `url("${src}")`; - style.maskImage = `url("${src}")`; - } - - return ; -} - -RawIcon.defaultProps = { - color: null, - size: 'normal', - isImage: false, -}; - -RawIcon.propTypes = { - color: PropTypes.string, - size: PropTypes.oneOf(['large', 'normal', 'small', 'extra-small']), - src: PropTypes.string.isRequired, - isImage: PropTypes.bool, -}; - -export default RawIcon; diff --git a/src/app/atoms/system-icons/RawIcon.scss b/src/app/atoms/system-icons/RawIcon.scss deleted file mode 100644 index 56fc9b3c..00000000 --- a/src/app/atoms/system-icons/RawIcon.scss +++ /dev/null @@ -1,28 +0,0 @@ -@mixin icSize($size) { - width: $size; - height: $size; -} - -.ic-raw { - display: inline-block; - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-size: cover; - mask-size: cover; - background-color: var(--ic-surface-normal); - - background-size: cover; - background-repeat: no-repeat; -} -.ic-raw-large { - @include icSize(var(--ic-large)); -} -.ic-raw-normal { - @include icSize(var(--ic-normal)); -} -.ic-raw-small { - @include icSize(var(--ic-small)); -} -.ic-raw-extra-small { - @include icSize(var(--ic-extra-small)); -} \ No newline at end of file diff --git a/src/app/atoms/tabs/Tabs.jsx b/src/app/atoms/tabs/Tabs.jsx deleted file mode 100644 index bcdc8ef7..00000000 --- a/src/app/atoms/tabs/Tabs.jsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import './Tabs.scss'; - -import Button from '../button/Button'; -import ScrollView from '../scroll/ScrollView'; - -function TabItem({ - selected, iconSrc, - onClick, children, disabled, -}) { - const isSelected = selected ? 'tab-item--selected' : ''; - - return ( - - ); -} - -TabItem.defaultProps = { - selected: false, - iconSrc: null, - onClick: null, - disabled: false, -}; - -TabItem.propTypes = { - selected: PropTypes.bool, - iconSrc: PropTypes.string, - onClick: PropTypes.func, - children: PropTypes.node.isRequired, - disabled: PropTypes.bool, -}; - -function Tabs({ items, defaultSelected, onSelect }) { - const [selectedItem, setSelectedItem] = useState(items[defaultSelected]); - - const handleTabSelection = (item, index, target) => { - if (selectedItem === item) return; - target.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); - setSelectedItem(item); - onSelect(item, index); - }; - - return ( -
- -
- {items.map((item, index) => ( - handleTabSelection(item, index, e.currentTarget)} - > - {item.text} - - ))} -
-
-
- ); -} - -Tabs.defaultProps = { - defaultSelected: 0, -}; - -Tabs.propTypes = { - items: PropTypes.arrayOf( - PropTypes.shape({ - iconSrc: PropTypes.string, - text: PropTypes.string, - disabled: PropTypes.bool, - }), - ).isRequired, - defaultSelected: PropTypes.number, - onSelect: PropTypes.func.isRequired, -}; - -export default Tabs; diff --git a/src/app/atoms/tabs/Tabs.scss b/src/app/atoms/tabs/Tabs.scss deleted file mode 100644 index 39ddddec..00000000 --- a/src/app/atoms/tabs/Tabs.scss +++ /dev/null @@ -1,45 +0,0 @@ -@use '../../partials/dir'; - -.tabs { - height: var(--header-height); - box-shadow: inset 0 -1px 0 var(--bg-surface-border); - - &__content { - min-width: 100%; - height: 100%; - display: flex; - } -} - -.tab-item { - flex-shrink: 0; - - @include dir.side(padding, var(--sp-normal), 24px); - border-radius: 0; - height: 100%; - box-shadow: none; - border-radius: var(--bo-radius) var(--bo-radius) 0 0; - - &:focus, - &:active { - background-color: var(--bg-surface-active); - box-shadow: none; - } - - &--selected { - --bs-tab-selected: inset 0 -2px 0 var(--tc-surface-high); - box-shadow: var(--bs-tab-selected); - - & .ic-raw { - background-color: var(--ic-surface-high); - } - & .text { - font-weight: var(--fw-medium); - } - &:focus, - &:active { - background-color: var(--bg-surface-active); - box-shadow: var(--bs-tab-selected); - } - } -} \ No newline at end of file diff --git a/src/app/atoms/text/Text.jsx b/src/app/atoms/text/Text.jsx deleted file mode 100644 index 3f507ee3..00000000 --- a/src/app/atoms/text/Text.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './Text.scss'; - -function Text({ - className, style, variant, weight, - primary, span, children, -}) { - const classes = []; - if (className) classes.push(className); - - classes.push(`text text-${variant} text-${weight}`); - if (primary) classes.push('font-primary'); - - const textClass = classes.join(' '); - if (span) return { children }; - if (variant === 'h1') return

{ children }

; - if (variant === 'h2') return

{ children }

; - if (variant === 's1') return

{ children }

; - return

{ children }

; -} - -Text.defaultProps = { - className: null, - style: null, - variant: 'b1', - weight: 'normal', - primary: false, - span: false, -}; - -Text.propTypes = { - className: PropTypes.string, - style: PropTypes.shape({}), - variant: PropTypes.oneOf(['h1', 'h2', 's1', 'b1', 'b2', 'b3']), - weight: PropTypes.oneOf(['light', 'normal', 'medium', 'bold']), - primary: PropTypes.bool, - span: PropTypes.bool, - children: PropTypes.node.isRequired, -}; - -export default Text; diff --git a/src/app/atoms/text/Text.scss b/src/app/atoms/text/Text.scss deleted file mode 100644 index 256bf6ea..00000000 --- a/src/app/atoms/text/Text.scss +++ /dev/null @@ -1,61 +0,0 @@ -@mixin font($type) { - font-size: var(--fs-#{$type}); - letter-spacing: var(--ls-#{$type}); - line-height: var(--lh-#{$type}); - - & img.emoji, - & img[data-mx-emoticon] { - height: calc(var(--lh-#{$type}) - .25rem); - } -} - -.text { - margin: 0; - padding: 0; - color: var(--tc-surface-high); - - & img.emoji, - & img[data-mx-emoticon] { - margin: 0 !important; - margin-right: 2px !important; - padding: 0 !important; - position: relative; - top: -.1rem; - vertical-align: middle; - } -} - -.text-light { - font-weight: var(--fw-light); -} -.text-normal { - font-weight: var(--fw-normal); -} -.text-medium { - font-weight: var(--fw-medium); -} -.text-bold { - font-weight: var(--fw-bold); -} - -.text-h1 { - @include font(h1); -} -.text-h2 { - @include font(h2); -} -.text-s1 { - @include font(s1); -} -.text-b1 { - @include font(b1); - color: var(--tc-surface-normal); -} -.text-b2 { - @include font(b2); - color: var(--tc-surface-normal); -} -.text-b3 { - @include font(b3); - color: var(--tc-surface-low); -} \ No newline at end of file diff --git a/src/app/atoms/time/Time.jsx b/src/app/atoms/time/Time.jsx deleted file mode 100644 index 750b958f..00000000 --- a/src/app/atoms/time/Time.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import dateFormat from 'dateformat'; -import { isInSameDay } from '../../../util/common'; - -function Time({ timestamp, fullTime }) { - const date = new Date(timestamp); - - const formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT'); - let formattedDate = formattedFullTime; - - if (!fullTime) { - const compareDate = new Date(); - const isToday = isInSameDay(date, compareDate); - compareDate.setDate(compareDate.getDate() - 1); - const isYesterday = isInSameDay(date, compareDate); - - formattedDate = dateFormat(date, isToday || isYesterday ? 'hh:MM TT' : 'dd/mm/yyyy'); - if (isYesterday) { - formattedDate = `Yesterday, ${formattedDate}`; - } - } - - return ( - - ); -} - -Time.defaultProps = { - fullTime: false, -}; - -Time.propTypes = { - timestamp: PropTypes.number.isRequired, - fullTime: PropTypes.bool, -}; - -export default Time; diff --git a/src/app/atoms/tooltip/Tooltip.jsx b/src/app/atoms/tooltip/Tooltip.jsx deleted file mode 100644 index 0f9067f5..00000000 --- a/src/app/atoms/tooltip/Tooltip.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './Tooltip.scss'; -import Tippy from '@tippyjs/react'; - -function Tooltip({ - className, placement, content, delay, children, -}) { - return ( - - {children} - - ); -} - -Tooltip.defaultProps = { - placement: 'top', - className: '', - delay: [200, 0], -}; - -Tooltip.propTypes = { - className: PropTypes.string, - placement: PropTypes.string, - content: PropTypes.node.isRequired, - delay: PropTypes.arrayOf(PropTypes.number), - children: PropTypes.node.isRequired, -}; - -export default Tooltip; diff --git a/src/app/atoms/tooltip/Tooltip.scss b/src/app/atoms/tooltip/Tooltip.scss deleted file mode 100644 index f609aa58..00000000 --- a/src/app/atoms/tooltip/Tooltip.scss +++ /dev/null @@ -1,10 +0,0 @@ -.tooltip { - padding: var(--sp-extra-tight) var(--sp-normal); - background-color: var(--bg-tooltip); - border-radius: var(--bo-radius); - box-shadow: var(--bs-popup); - - .text { - color: var(--tc-tooltip); - } -} \ No newline at end of file diff --git a/src/app/components/CapabilitiesAndMediaConfigLoader.tsx b/src/app/components/CapabilitiesAndMediaConfigLoader.tsx deleted file mode 100644 index 574d0ca7..00000000 --- a/src/app/components/CapabilitiesAndMediaConfigLoader.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ReactNode, useCallback, useEffect } from 'react'; -import { Capabilities } from 'matrix-js-sdk'; -import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; -import { useMatrixClient } from '../hooks/useMatrixClient'; -import { MediaConfig } from '../hooks/useMediaConfig'; -import { promiseFulfilledResult } from '../utils/common'; - -type CapabilitiesAndMediaConfigLoaderProps = { - children: (capabilities?: Capabilities, mediaConfig?: MediaConfig) => ReactNode; -}; -export function CapabilitiesAndMediaConfigLoader({ - children, -}: CapabilitiesAndMediaConfigLoaderProps) { - const mx = useMatrixClient(); - - const [state, load] = useAsyncCallback< - [Capabilities | undefined, MediaConfig | undefined], - unknown, - [] - >( - useCallback(async () => { - const result = await Promise.allSettled([mx.getCapabilities(), mx.getMediaConfig()]); - const capabilities = promiseFulfilledResult(result[0]); - const mediaConfig = promiseFulfilledResult(result[1]); - return [capabilities, mediaConfig]; - }, [mx]) - ); - - useEffect(() => { - load(); - }, [load]); - - const [capabilities, mediaConfig] = - state.status === AsyncStatus.Success ? state.data : [undefined, undefined]; - return children(capabilities, mediaConfig); -} diff --git a/src/app/components/DeviceVerificationSetup.tsx b/src/app/components/DeviceVerificationSetup.tsx index fb09c150..433fa6a1 100644 --- a/src/app/components/DeviceVerificationSetup.tsx +++ b/src/app/components/DeviceVerificationSetup.tsx @@ -20,7 +20,7 @@ import { PasswordInput } from './password-input'; import { ContainerColor } from '../styles/ContainerColor.css'; import { copyToClipboard } from '../utils/dom'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; -import { clearSecretStorageKeys } from '../../client/state/secretStorageKeys'; +import { clearSecretStorageKeys } from '../../client/secretStorageKeys'; import { ActionUIA, ActionUIAFlowsLoader } from './ActionUIA'; import { useMatrixClient } from '../hooks/useMatrixClient'; import { useAlive } from '../hooks/useAlive'; diff --git a/src/app/components/ManualVerification.tsx b/src/app/components/ManualVerification.tsx index 03f3e710..f7cde92b 100644 --- a/src/app/components/ManualVerification.tsx +++ b/src/app/components/ManualVerification.tsx @@ -19,7 +19,7 @@ import { SecretStorageKeyContent } from '../../types/matrix/accountData'; import { SecretStorageRecoveryKey, SecretStorageRecoveryPassphrase } from './SecretStorage'; import { useMatrixClient } from '../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; -import { storePrivateKey } from '../../client/state/secretStorageKeys'; +import { storePrivateKey } from '../../client/secretStorageKeys'; export enum ManualVerificationMethod { RecoveryPassphrase = 'passphrase', diff --git a/src/app/components/ServerConfigsLoader.tsx b/src/app/components/ServerConfigsLoader.tsx new file mode 100644 index 00000000..3c8ce8eb --- /dev/null +++ b/src/app/components/ServerConfigsLoader.tsx @@ -0,0 +1,52 @@ +import { ReactNode, useCallback, useMemo } from 'react'; +import { Capabilities, validateAuthMetadata, ValidatedAuthMetadata } from 'matrix-js-sdk'; +import { AsyncStatus, useAsyncCallbackValue } from '../hooks/useAsyncCallback'; +import { useMatrixClient } from '../hooks/useMatrixClient'; +import { MediaConfig } from '../hooks/useMediaConfig'; +import { promiseFulfilledResult } from '../utils/common'; + +export type ServerConfigs = { + capabilities?: Capabilities; + mediaConfig?: MediaConfig; + authMetadata?: ValidatedAuthMetadata; +}; + +type ServerConfigsLoaderProps = { + children: (configs: ServerConfigs) => ReactNode; +}; +export function ServerConfigsLoader({ children }: ServerConfigsLoaderProps) { + const mx = useMatrixClient(); + const fallbackConfigs = useMemo(() => ({}), []); + + const [configsState] = useAsyncCallbackValue( + useCallback(async () => { + const result = await Promise.allSettled([ + mx.getCapabilities(), + mx.getMediaConfig(), + mx.getAuthMetadata(), + ]); + + const capabilities = promiseFulfilledResult(result[0]); + const mediaConfig = promiseFulfilledResult(result[1]); + const authMetadata = promiseFulfilledResult(result[2]); + let validatedAuthMetadata: ValidatedAuthMetadata | undefined; + + try { + validatedAuthMetadata = validateAuthMetadata(authMetadata); + } catch (e) { + console.error(e); + } + + return { + capabilities, + mediaConfig, + authMetadata: validatedAuthMetadata, + }; + }, [mx]) + ); + + const configs: ServerConfigs = + configsState.status === AsyncStatus.Success ? configsState.data : fallbackConfigs; + + return children(configs); +} diff --git a/src/app/components/UserRoomProfileRenderer.tsx b/src/app/components/UserRoomProfileRenderer.tsx new file mode 100644 index 00000000..ca7aa837 --- /dev/null +++ b/src/app/components/UserRoomProfileRenderer.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Menu, PopOut, toRem } from 'folds'; +import FocusTrap from 'focus-trap-react'; +import { useCloseUserRoomProfile, useUserRoomProfileState } from '../state/hooks/userRoomProfile'; +import { UserRoomProfile } from './user-profile'; +import { UserRoomProfileState } from '../state/userRoomProfile'; +import { useAllJoinedRoomsSet, useGetRoom } from '../hooks/useGetRoom'; +import { stopPropagation } from '../utils/keyboard'; +import { SpaceProvider } from '../hooks/useSpace'; +import { RoomProvider } from '../hooks/useRoom'; + +function UserRoomProfileContextMenu({ state }: { state: UserRoomProfileState }) { + const { roomId, spaceId, userId, cords, position } = state; + const allJoinedRooms = useAllJoinedRoomsSet(); + const getRoom = useGetRoom(allJoinedRooms); + const room = getRoom(roomId); + const space = spaceId ? getRoom(spaceId) : undefined; + + const close = useCloseUserRoomProfile(); + + if (!room) return null; + + return ( + + + + + + + + + + } + /> + ); +} + +export function UserRoomProfileRenderer() { + const state = useUserRoomProfileState(); + + if (!state) return null; + return ; +} diff --git a/src/app/components/create-room/AdditionalCreatorInput.tsx b/src/app/components/create-room/AdditionalCreatorInput.tsx new file mode 100644 index 00000000..84b92231 --- /dev/null +++ b/src/app/components/create-room/AdditionalCreatorInput.tsx @@ -0,0 +1,294 @@ +import { + Box, + Button, + Chip, + config, + Icon, + Icons, + Input, + Line, + Menu, + MenuItem, + PopOut, + RectCords, + Scroll, + Text, + toRem, +} from 'folds'; +import { isKeyHotkey } from 'is-hotkey'; +import FocusTrap from 'focus-trap-react'; +import React, { + ChangeEventHandler, + KeyboardEventHandler, + MouseEventHandler, + useMemo, + useState, +} from 'react'; +import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix'; +import { useDirectUsers } from '../../hooks/useDirectUsers'; +import { SettingTile } from '../setting-tile'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { stopPropagation } from '../../utils/keyboard'; +import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch'; +import { highlightText, makeHighlightRegex } from '../../plugins/react-custom-html-parser'; + +export const useAdditionalCreators = (defaultCreators?: string[]) => { + const mx = useMatrixClient(); + const [additionalCreators, setAdditionalCreators] = useState( + () => defaultCreators?.filter((id) => id !== mx.getSafeUserId()) ?? [] + ); + + const addAdditionalCreator = (userId: string) => { + if (userId === mx.getSafeUserId()) return; + + setAdditionalCreators((creators) => { + const creatorsSet = new Set(creators); + creatorsSet.add(userId); + return Array.from(creatorsSet); + }); + }; + + const removeAdditionalCreator = (userId: string) => { + setAdditionalCreators((creators) => { + const creatorsSet = new Set(creators); + creatorsSet.delete(userId); + return Array.from(creatorsSet); + }); + }; + + return { + additionalCreators, + addAdditionalCreator, + removeAdditionalCreator, + }; +}; + +const SEARCH_OPTIONS: UseAsyncSearchOptions = { + limit: 1000, + matchOptions: { + contain: true, + }, +}; +const getUserIdString = (userId: string) => getMxIdLocalPart(userId) ?? userId; + +type AdditionalCreatorInputProps = { + additionalCreators: string[]; + onSelect: (userId: string) => void; + onRemove: (userId: string) => void; + disabled?: boolean; +}; +export function AdditionalCreatorInput({ + additionalCreators, + onSelect, + onRemove, + disabled, +}: AdditionalCreatorInputProps) { + const mx = useMatrixClient(); + const [menuCords, setMenuCords] = useState(); + const directUsers = useDirectUsers(); + + const [validUserId, setValidUserId] = useState(); + const filteredUsers = useMemo( + () => directUsers.filter((userId) => !additionalCreators.includes(userId)), + [directUsers, additionalCreators] + ); + const [result, search, resetSearch] = useAsyncSearch( + filteredUsers, + getUserIdString, + SEARCH_OPTIONS + ); + const queryHighlighRegex = result?.query ? makeHighlightRegex([result.query]) : undefined; + + const suggestionUsers = result + ? result.items + : filteredUsers.sort((a, b) => (a.toLocaleLowerCase() >= b.toLocaleLowerCase() ? 1 : -1)); + + const handleOpenMenu: MouseEventHandler = (evt) => { + setMenuCords(evt.currentTarget.getBoundingClientRect()); + }; + const handleCloseMenu = () => { + setMenuCords(undefined); + setValidUserId(undefined); + resetSearch(); + }; + + const handleCreatorChange: ChangeEventHandler = (evt) => { + const creatorInput = evt.currentTarget; + const creator = creatorInput.value.trim(); + if (isUserId(creator)) { + setValidUserId(creator); + } else { + setValidUserId(undefined); + const term = + getMxIdLocalPart(creator) ?? (creator.startsWith('@') ? creator.slice(1) : creator); + if (term) { + search(term); + } else { + resetSearch(); + } + } + }; + + const handleSelectUserId = (userId?: string) => { + if (userId && isUserId(userId)) { + onSelect(userId); + handleCloseMenu(); + } + }; + + const handleCreatorKeyDown: KeyboardEventHandler = (evt) => { + if (isKeyHotkey('enter', evt)) { + evt.preventDefault(); + const creator = evt.currentTarget.value.trim(); + handleSelectUserId(isUserId(creator) ? creator : suggestionUsers[0]); + } + }; + + const handleEnterClick = () => { + handleSelectUserId(validUserId); + }; + + return ( + + + + + {mx.getSafeUserId()} + + {additionalCreators.map((creator) => ( + } + onClick={() => onRemove(creator)} + disabled={disabled} + > + {creator} + + ))} + evt.key === 'ArrowDown', + isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', + escapeDeactivates: stopPropagation, + }} + > + + + + + + + + + + + {!validUserId && suggestionUsers.length > 0 ? ( + + + {suggestionUsers.map((userId) => ( + handleSelectUserId(userId)} + after={ + + {getMxIdServer(userId)} + + } + > + + + + {queryHighlighRegex + ? highlightText(queryHighlighRegex, [ + getMxIdLocalPart(userId) ?? userId, + ]) + : getMxIdLocalPart(userId)} + + + + + ))} + + + ) : ( + + + No Suggestions + + + Please provide the user ID and hit Enter. + + + )} + + + + + } + > + + + + + + + + ); +} diff --git a/src/app/components/create-room/CreateRoomAliasInput.tsx b/src/app/components/create-room/CreateRoomAliasInput.tsx new file mode 100644 index 00000000..e84658c0 --- /dev/null +++ b/src/app/components/create-room/CreateRoomAliasInput.tsx @@ -0,0 +1,118 @@ +import React, { + FormEventHandler, + KeyboardEventHandler, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import { MatrixError } from 'matrix-js-sdk'; +import { Box, color, Icon, Icons, Input, Spinner, Text, toRem } from 'folds'; +import { isKeyHotkey } from 'is-hotkey'; +import { getMxIdServer } from '../../utils/matrix'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { replaceSpaceWithDash } from '../../utils/common'; +import { AsyncState, AsyncStatus, useAsync } from '../../hooks/useAsyncCallback'; +import { useDebounce } from '../../hooks/useDebounce'; + +export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) { + const mx = useMatrixClient(); + const aliasInputRef = useRef(null); + const [aliasAvail, setAliasAvail] = useState>({ + status: AsyncStatus.Idle, + }); + + useEffect(() => { + if (aliasAvail.status === AsyncStatus.Success && aliasInputRef.current?.value === '') { + setAliasAvail({ status: AsyncStatus.Idle }); + } + }, [aliasAvail]); + + const checkAliasAvail = useAsync( + useCallback( + async (aliasLocalPart: string) => { + const roomAlias = `#${aliasLocalPart}:${getMxIdServer(mx.getSafeUserId())}`; + try { + const result = await mx.getRoomIdForAlias(roomAlias); + return typeof result.room_id !== 'string'; + } catch (e) { + if (e instanceof MatrixError && e.httpStatus === 404) { + return true; + } + throw e; + } + }, + [mx] + ), + setAliasAvail + ); + const aliasAvailable: boolean | undefined = + aliasAvail.status === AsyncStatus.Success ? aliasAvail.data : undefined; + + const debounceCheckAliasAvail = useDebounce(checkAliasAvail, { wait: 500 }); + + const handleAliasChange: FormEventHandler = (evt) => { + const aliasInput = evt.currentTarget; + const aliasLocalPart = replaceSpaceWithDash(aliasInput.value); + if (aliasLocalPart) { + aliasInput.value = aliasLocalPart; + debounceCheckAliasAvail(aliasLocalPart); + } else { + setAliasAvail({ status: AsyncStatus.Idle }); + } + }; + + const handleAliasKeyDown: KeyboardEventHandler = (evt) => { + if (isKeyHotkey('enter', evt)) { + evt.preventDefault(); + + const aliasInput = evt.currentTarget; + const aliasLocalPart = replaceSpaceWithDash(aliasInput.value); + if (aliasLocalPart) { + checkAliasAvail(aliasLocalPart); + } else { + setAliasAvail({ status: AsyncStatus.Idle }); + } + } + }; + + return ( + + Address (Optional) + + Pick an unique address to make it discoverable. + + + ) : ( + + ) + } + after={ + + :{getMxIdServer(mx.getSafeUserId())} + + } + onKeyDown={handleAliasKeyDown} + name="aliasInput" + size="500" + variant={aliasAvailable === true ? 'Success' : 'SurfaceVariant'} + radii="400" + autoComplete="off" + disabled={disabled} + /> + {aliasAvailable === false && ( + + + + This address is already taken. Please select a different one. + + + )} + + ); +} diff --git a/src/app/components/create-room/CreateRoomKindSelector.tsx b/src/app/components/create-room/CreateRoomKindSelector.tsx new file mode 100644 index 00000000..096954fb --- /dev/null +++ b/src/app/components/create-room/CreateRoomKindSelector.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { Box, Text, Icon, Icons, config, IconSrc } from 'folds'; +import { SequenceCard } from '../sequence-card'; +import { SettingTile } from '../setting-tile'; + +export enum CreateRoomKind { + Private = 'private', + Restricted = 'restricted', + Public = 'public', +} +type CreateRoomKindSelectorProps = { + value?: CreateRoomKind; + onSelect: (value: CreateRoomKind) => void; + canRestrict?: boolean; + disabled?: boolean; + getIcon: (kind: CreateRoomKind) => IconSrc; +}; +export function CreateRoomKindSelector({ + value, + onSelect, + canRestrict, + disabled, + getIcon, +}: CreateRoomKindSelectorProps) { + return ( + + {canRestrict && ( + onSelect(CreateRoomKind.Restricted)} + disabled={disabled} + > + } + after={value === CreateRoomKind.Restricted && } + > + Restricted + + Only member of parent space can join. + + + + )} + onSelect(CreateRoomKind.Private)} + disabled={disabled} + > + } + after={value === CreateRoomKind.Private && } + > + Private + + Only people with invite can join. + + + + onSelect(CreateRoomKind.Public)} + disabled={disabled} + > + } + after={value === CreateRoomKind.Public && } + > + Public + + Anyone with the address can join. + + + + + ); +} diff --git a/src/app/components/create-room/RoomVersionSelector.tsx b/src/app/components/create-room/RoomVersionSelector.tsx new file mode 100644 index 00000000..219ded0c --- /dev/null +++ b/src/app/components/create-room/RoomVersionSelector.tsx @@ -0,0 +1,117 @@ +import React, { MouseEventHandler, useState } from 'react'; +import { + Box, + Button, + Chip, + config, + Icon, + Icons, + Menu, + PopOut, + RectCords, + Text, + toRem, +} from 'folds'; +import FocusTrap from 'focus-trap-react'; +import { SettingTile } from '../setting-tile'; +import { SequenceCard } from '../sequence-card'; +import { stopPropagation } from '../../utils/keyboard'; + +export function RoomVersionSelector({ + versions, + value, + onChange, + disabled, +}: { + versions: string[]; + value: string; + onChange: (value: string) => void; + disabled?: boolean; +}) { + const [menuCords, setMenuCords] = useState(); + + const handleMenu: MouseEventHandler = (evt) => { + setMenuCords(evt.currentTarget.getBoundingClientRect()); + }; + + const handleSelect = (version: string) => { + setMenuCords(undefined); + onChange(version); + }; + + return ( + + setMenuCords(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => + evt.key === 'ArrowDown' || evt.key === 'ArrowRight', + isKeyBackward: (evt: KeyboardEvent) => + evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', + escapeDeactivates: stopPropagation, + }} + > + + + Versions + + {versions.map((version) => ( + handleSelect(version)} + type="button" + > + + {version} + + + ))} + + + + + } + > + + + } + /> + + ); +} diff --git a/src/app/components/create-room/index.ts b/src/app/components/create-room/index.ts new file mode 100644 index 00000000..ffd9cb3d --- /dev/null +++ b/src/app/components/create-room/index.ts @@ -0,0 +1,5 @@ +export * from './CreateRoomKindSelector'; +export * from './CreateRoomAliasInput'; +export * from './RoomVersionSelector'; +export * from './utils'; +export * from './AdditionalCreatorInput'; diff --git a/src/app/components/create-room/utils.ts b/src/app/components/create-room/utils.ts new file mode 100644 index 00000000..a0ca7488 --- /dev/null +++ b/src/app/components/create-room/utils.ts @@ -0,0 +1,140 @@ +import { + ICreateRoomOpts, + ICreateRoomStateEvent, + JoinRule, + MatrixClient, + RestrictedAllowType, + Room, +} from 'matrix-js-sdk'; +import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types'; +import { CreateRoomKind } from './CreateRoomKindSelector'; +import { RoomType, StateEvent } from '../../../types/matrix/room'; +import { getViaServers } from '../../plugins/via-servers'; +import { getMxIdServer } from '../../utils/matrix'; + +export const createRoomCreationContent = ( + type: RoomType | undefined, + allowFederation: boolean, + additionalCreators: string[] | undefined +): object => { + const content: Record = {}; + if (typeof type === 'string') { + content.type = type; + } + if (allowFederation === false) { + content['m.federate'] = false; + } + if (Array.isArray(additionalCreators)) { + content.additional_creators = additionalCreators; + } + + return content; +}; + +export const createRoomJoinRulesState = ( + kind: CreateRoomKind, + parent: Room | undefined, + knock: boolean +) => { + let content: RoomJoinRulesEventContent = { + join_rule: knock ? JoinRule.Knock : JoinRule.Invite, + }; + + if (kind === CreateRoomKind.Public) { + content = { + join_rule: JoinRule.Public, + }; + } + + if (kind === CreateRoomKind.Restricted && parent) { + content = { + join_rule: knock ? ('knock_restricted' as JoinRule) : JoinRule.Restricted, + allow: [ + { + type: RestrictedAllowType.RoomMembership, + room_id: parent.roomId, + }, + ], + }; + } + + return { + type: StateEvent.RoomJoinRules, + state_key: '', + content, + }; +}; + +export const createRoomParentState = (parent: Room) => ({ + type: StateEvent.SpaceParent, + state_key: parent.roomId, + content: { + canonical: true, + via: getViaServers(parent), + }, +}); + +export const createRoomEncryptionState = () => ({ + type: 'm.room.encryption', + state_key: '', + content: { + algorithm: 'm.megolm.v1.aes-sha2', + }, +}); + +export type CreateRoomData = { + version: string; + type?: RoomType; + parent?: Room; + kind: CreateRoomKind; + name: string; + topic?: string; + aliasLocalPart?: string; + encryption?: boolean; + knock: boolean; + allowFederation: boolean; + additionalCreators?: string[]; +}; +export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promise => { + const initialState: ICreateRoomStateEvent[] = []; + + if (data.encryption) { + initialState.push(createRoomEncryptionState()); + } + + if (data.parent) { + initialState.push(createRoomParentState(data.parent)); + } + + initialState.push(createRoomJoinRulesState(data.kind, data.parent, data.knock)); + + const options: ICreateRoomOpts = { + room_version: data.version, + name: data.name, + topic: data.topic, + room_alias_name: data.aliasLocalPart, + creation_content: createRoomCreationContent( + data.type, + data.allowFederation, + data.additionalCreators + ), + initial_state: initialState, + }; + + const result = await mx.createRoom(options); + + if (data.parent) { + await mx.sendStateEvent( + data.parent.roomId, + StateEvent.SpaceChild as any, + { + auto_join: false, + suggested: false, + via: [getMxIdServer(mx.getUserId() ?? '') ?? ''], + }, + result.room_id + ); + } + + return result.room_id; +}; diff --git a/src/app/components/editor/Editor.css.ts b/src/app/components/editor/Editor.css.ts index 09a444ec..d128ed07 100644 --- a/src/app/components/editor/Editor.css.ts +++ b/src/app/components/editor/Editor.css.ts @@ -41,21 +41,21 @@ export const EditorTextarea = style([ }, ]); -export const EditorPlaceholder = style([ +export const EditorPlaceholderContainer = style([ DefaultReset, { - position: 'absolute', - zIndex: 1, - width: '100%', opacity: config.opacity.Placeholder, pointerEvents: 'none', userSelect: 'none', + }, +]); - selectors: { - '&:not(:first-child)': { - display: 'none', - }, - }, +export const EditorPlaceholderTextVisual = style([ + DefaultReset, + { + display: 'block', + paddingTop: toRem(13), + paddingLeft: toRem(1), }, ]); diff --git a/src/app/components/editor/Editor.tsx b/src/app/components/editor/Editor.tsx index 044d0837..bd848dd5 100644 --- a/src/app/components/editor/Editor.tsx +++ b/src/app/components/editor/Editor.tsx @@ -106,22 +106,17 @@ export const CustomEditor = forwardRef( [editor, onKeyDown] ); - const renderPlaceholder = useCallback(({ attributes, children }: RenderPlaceholderProps) => { - // drop style attribute as we use our custom placeholder css. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { style, ...props } = attributes; - return ( - - {children} - - ); - }, []); + const renderPlaceholder = useCallback( + ({ attributes, children }: RenderPlaceholderProps) => ( + + {/* Inner component to style the actual text position and appearance */} + + {children} + + + ), + [] + ); return (
diff --git a/src/app/components/editor/Elements.tsx b/src/app/components/editor/Elements.tsx index a7438ecd..675c4542 100644 --- a/src/app/components/editor/Elements.tsx +++ b/src/app/components/editor/Elements.tsx @@ -157,7 +157,7 @@ export function RenderElement({ attributes, element, children }: RenderElementPr } + tooltip={} delay={500} > {(triggerRef) => ( diff --git a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx index cc431f58..b0c64f60 100644 --- a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx @@ -9,7 +9,7 @@ import { getDirectRoomAvatarUrl } from '../../../utils/room'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { AutocompleteQuery } from './autocompleteQuery'; import { AutocompleteMenu } from './AutocompleteMenu'; -import { getMxIdServer, validMxId } from '../../../utils/matrix'; +import { getMxIdServer, isRoomAlias } from '../../../utils/matrix'; import { UseAsyncSearchOptions, useAsyncSearch } from '../../../hooks/useAsyncSearch'; import { onTabPress } from '../../../utils/keyboard'; import { useKeyDown } from '../../../hooks/useKeyDown'; @@ -22,7 +22,7 @@ import { getViaServers } from '../../../plugins/via-servers'; type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void; const roomAliasFromQueryText = (mx: MatrixClient, text: string) => - validMxId(`#${text}`) + isRoomAlias(`#${text}`) ? `#${text}` : `#${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`; diff --git a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx index d6c0f302..7a8012eb 100644 --- a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx @@ -15,7 +15,7 @@ import { import { onTabPress } from '../../../utils/keyboard'; import { createMentionElement, moveCursor, replaceWithElement } from '../utils'; import { useKeyDown } from '../../../hooks/useKeyDown'; -import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix'; +import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../../utils/matrix'; import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room'; import { UserAvatar } from '../../user-avatar'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; @@ -24,7 +24,7 @@ import { Membership } from '../../../../types/matrix/room'; type MentionAutoCompleteHandler = (userId: string, name: string) => void; const userIdFromQueryText = (mx: MatrixClient, text: string) => - validMxId(`@${text}`) + isUserId(`@${text}`) ? `@${text}` : `@${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`; diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 72a60f2b..3db27e2a 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -2,640 +2,346 @@ import React, { ChangeEventHandler, FocusEventHandler, MouseEventHandler, - UIEventHandler, ReactNode, - memo, + RefObject, useCallback, useEffect, useMemo, useRef, } from 'react'; -import { - Badge, - Box, - Chip, - Icon, - IconButton, - Icons, - Input, - Line, - Scroll, - Text, - Tooltip, - TooltipProvider, - as, - config, - toRem, -} from 'folds'; +import { Box, config, Icons, Scroll } from 'folds'; import FocusTrap from 'focus-trap-react'; import { isKeyHotkey } from 'is-hotkey'; -import classNames from 'classnames'; -import { MatrixClient, Room } from 'matrix-js-sdk'; -import { atom, useAtomValue, useSetAtom } from 'jotai'; - -import * as css from './EmojiBoard.css'; -import { EmojiGroupId, IEmoji, IEmojiGroup, emojiGroups, emojis } from '../../plugins/emoji'; -import { IEmojiGroupLabels, useEmojiGroupLabels } from './useEmojiGroupLabels'; -import { IEmojiGroupIcons, useEmojiGroupIcons } from './useEmojiGroupIcons'; +import { Room } from 'matrix-js-sdk'; +import { atom, PrimitiveAtom, useAtom, useSetAtom } from 'jotai'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { IEmoji, emojiGroups, emojis } from '../../plugins/emoji'; +import { useEmojiGroupLabels } from './useEmojiGroupLabels'; +import { useEmojiGroupIcons } from './useEmojiGroupIcons'; import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard'; import { useRelevantImagePacks } from '../../hooks/useImagePacks'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useRecentEmoji } from '../../hooks/useRecentEmoji'; import { isUserId, mxcUrlToHttp } from '../../utils/matrix'; -import { editableActiveElement, isIntersectingScrollView, targetFromEvent } from '../../utils/dom'; +import { editableActiveElement, targetFromEvent } from '../../utils/dom'; import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch'; import { useDebounce } from '../../hooks/useDebounce'; import { useThrottle } from '../../hooks/useThrottle'; import { addRecentEmoji } from '../../plugins/recent-emoji'; -import { mobileOrTablet } from '../../utils/user-agent'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { ImagePack, ImageUsage, PackImageReader } from '../../plugins/custom-emoji'; import { getEmoticonSearchStr } from '../../plugins/utils'; +import { + SearchInput, + EmojiBoardTabs, + SidebarStack, + SidebarDivider, + Sidebar, + NoStickerPacks, + createPreviewDataAtom, + Preview, + PreviewData, + EmojiItem, + StickerItem, + CustomEmojiItem, + ImageGroupIcon, + GroupIcon, + getEmojiItemInfo, + EmojiGroup, + EmojiBoardLayout, +} from './components'; +import { EmojiBoardTab, EmojiType } from './types'; +import { VirtualTile } from '../virtualizer'; const RECENT_GROUP_ID = 'recent_group'; const SEARCH_GROUP_ID = 'search_group'; -export enum EmojiBoardTab { - Emoji = 'Emoji', - Sticker = 'Sticker', -} - -enum EmojiType { - Emoji = 'emoji', - CustomEmoji = 'customEmoji', - Sticker = 'sticker', -} - -export type EmojiItemInfo = { - type: EmojiType; - data: string; - shortcode: string; - label: string; +type EmojiGroupItem = { + id: string; + name: string; + items: Array; +}; +type StickerGroupItem = { + id: string; + name: string; + items: Array; }; -const getDOMGroupId = (id: string): string => `EmojiBoardGroup-${id}`; +const useGroups = ( + tab: EmojiBoardTab, + imagePacks: ImagePack[] +): [EmojiGroupItem[], StickerGroupItem[]] => { + const mx = useMatrixClient(); -const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => { - const type = element.getAttribute('data-emoji-type') as EmojiType | undefined; - const data = element.getAttribute('data-emoji-data'); - const label = element.getAttribute('title'); - const shortcode = element.getAttribute('data-emoji-shortcode'); + const recentEmojis = useRecentEmoji(mx, 21); + const labels = useEmojiGroupLabels(); - if (type && data && shortcode && label) - return { - type, - data, - shortcode, - label, - }; - return undefined; + const emojiGroupItems = useMemo(() => { + const g: EmojiGroupItem[] = []; + if (tab !== EmojiBoardTab.Emoji) return g; + + g.push({ + id: RECENT_GROUP_ID, + name: 'Recent', + items: recentEmojis, + }); + + imagePacks.forEach((pack) => { + let label = pack.meta.name; + if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; + + g.push({ + id: pack.id, + name: label ?? 'Unknown', + items: pack + .getImages(ImageUsage.Emoticon) + .sort((a, b) => a.shortcode.localeCompare(b.shortcode)), + }); + }); + + emojiGroups.forEach((group) => { + g.push({ + id: group.id, + name: labels[group.id], + items: group.emojis, + }); + }); + + return g; + }, [mx, recentEmojis, labels, imagePacks, tab]); + + const stickerGroupItems = useMemo(() => { + const g: StickerGroupItem[] = []; + if (tab !== EmojiBoardTab.Sticker) return g; + + imagePacks.forEach((pack) => { + let label = pack.meta.name; + if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; + + g.push({ + id: pack.id, + name: label ?? 'Unknown', + items: pack + .getImages(ImageUsage.Sticker) + .sort((a, b) => a.shortcode.localeCompare(b.shortcode)), + }); + }); + + return g; + }, [mx, imagePacks, tab]); + + return [emojiGroupItems, stickerGroupItems]; }; -const activeGroupIdAtom = atom(undefined); +const useItemRenderer = (tab: EmojiBoardTab) => { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + + const renderItem = (emoji: IEmoji | PackImageReader, index: number) => { + if ('unicode' in emoji) { + return ; + } + if (tab === EmojiBoardTab.Sticker) { + return ( + + ); + } + return ( + + ); + }; + + return renderItem; +}; + +type EmojiSidebarProps = { + activeGroupAtom: PrimitiveAtom; + packs: ImagePack[]; + onScrollToGroup: (groupId: string) => void; +}; +function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarProps) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + + const [activeGroupId, setActiveGroupId] = useAtom(activeGroupAtom); + const usage = ImageUsage.Emoticon; + const labels = useEmojiGroupLabels(); + const icons = useEmojiGroupIcons(); + + const handleScrollToGroup = (groupId: string) => { + setActiveGroupId(groupId); + onScrollToGroup(groupId); + }; -function Sidebar({ children }: { children: ReactNode }) { return ( - - - - {children} - - - - ); -} + + + + + {packs.length > 0 && ( + + + {packs.map((pack) => { + let label = pack.meta.name; + if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; -const SidebarStack = as<'div'>(({ className, children, ...props }, ref) => ( - - {children} - -)); -function SidebarDivider() { - return ; -} + const url = + mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) || + pack.meta.avatar; -function Header({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} - -function Content({ children }: { children: ReactNode }) { - return {children}; -} - -function Footer({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} - -const EmojiBoardLayout = as< - 'div', - { - header: ReactNode; - sidebar?: ReactNode; - footer?: ReactNode; - children: ReactNode; - } ->(({ className, header, sidebar, footer, children, ...props }, ref) => ( - - - {header} - {children} - {footer} - - - {sidebar} - -)); - -function EmojiBoardTabs({ - tab, - onTabChange, -}: { - tab: EmojiBoardTab; - onTabChange: (tab: EmojiBoardTab) => void; -}) { - return ( - - onTabChange(EmojiBoardTab.Sticker)} - > - - Sticker - - - onTabChange(EmojiBoardTab.Emoji)} - > - - Emoji - - - - ); -} - -export function SidebarBtn({ - active, - label, - id, - onItemClick, - children, -}: { - active?: boolean; - label: string; - id: T; - onItemClick: (id: T) => void; - children: ReactNode; -}) { - return ( - - {label} - - } - > - {(ref) => ( - onItemClick(id)} - size="400" - radii="300" - variant="Surface" - > - {children} - + return ( + + ); + })} + )} - + + + {emojiGroups.map((group) => ( + + ))} + + ); } -export const EmojiGroup = as< - 'div', - { - id: string; - label: string; - children: ReactNode; - } ->(({ className, id, label, children, ...props }, ref) => ( - - - {label} - -
- +type StickerSidebarProps = { + activeGroupAtom: PrimitiveAtom; + packs: ImagePack[]; + onScrollToGroup: (groupId: string) => void; +}; +function StickerSidebar({ activeGroupAtom, packs, onScrollToGroup }: StickerSidebarProps) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + + const [activeGroupId, setActiveGroupId] = useAtom(activeGroupAtom); + const usage = ImageUsage.Sticker; + + const handleScrollToGroup = (groupId: string) => { + setActiveGroupId(groupId); + onScrollToGroup(groupId); + }; + + return ( + + + {packs.map((pack) => { + let label = pack.meta.name; + if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; + + const url = + mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) || pack.meta.avatar; + + return ( + + ); + })} + + + ); +} + +type EmojiGroupHolderProps = { + contentScrollRef: RefObject; + previewAtom: PrimitiveAtom; + children?: ReactNode; + onGroupItemClick: MouseEventHandler; +}; +function EmojiGroupHolder({ + contentScrollRef, + previewAtom, + onGroupItemClick, + children, +}: EmojiGroupHolderProps) { + const setPreviewData = useSetAtom(previewAtom); + + const handleEmojiPreview = useCallback( + (element: HTMLButtonElement) => { + const emojiInfo = getEmojiItemInfo(element); + if (!emojiInfo) return; + + setPreviewData({ + key: emojiInfo.data, + shortcode: emojiInfo.shortcode, + }); + }, + [setPreviewData] + ); + + const throttleEmojiHover = useThrottle(handleEmojiPreview, { + wait: 200, + immediate: true, + }); + + const handleEmojiHover: MouseEventHandler = (evt) => { + const targetEl = targetFromEvent(evt.nativeEvent, 'button') as HTMLButtonElement | undefined; + if (!targetEl) return; + throttleEmojiHover(targetEl); + }; + + const handleEmojiFocus: FocusEventHandler = (evt) => { + const targetEl = evt.target as HTMLButtonElement; + handleEmojiPreview(targetEl); + }; + + return ( + + {children} -
-
-)); - -export function EmojiItem({ - label, - type, - data, - shortcode, - children, -}: { - label: string; - type: EmojiType; - data: string; - shortcode: string; - children: ReactNode; -}) { - return ( - - {children} - +
); } -export function StickerItem({ - label, - type, - data, - shortcode, - children, -}: { - label: string; - type: EmojiType; - data: string; - shortcode: string; - children: ReactNode; -}) { - return ( - - {children} - - ); -} - -function RecentEmojiSidebarStack({ onItemClick }: { onItemClick: (id: string) => void }) { - const activeGroupId = useAtomValue(activeGroupIdAtom); - - return ( - - onItemClick(RECENT_GROUP_ID)} - > - - - - ); -} - -function ImagePackSidebarStack({ - mx, - packs, - usage, - onItemClick, - useAuthentication, -}: { - mx: MatrixClient; - packs: ImagePack[]; - usage: ImageUsage; - onItemClick: (id: string) => void; - useAuthentication?: boolean; -}) { - const activeGroupId = useAtomValue(activeGroupIdAtom); - return ( - - {usage === ImageUsage.Emoticon && } - {packs.map((pack) => { - let label = pack.meta.name; - if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; - return ( - - {label - - ); - })} - - ); -} - -function NativeEmojiSidebarStack({ - groups, - icons, - labels, - onItemClick, -}: { - groups: IEmojiGroup[]; - icons: IEmojiGroupIcons; - labels: IEmojiGroupLabels; - onItemClick: (id: EmojiGroupId) => void; -}) { - const activeGroupId = useAtomValue(activeGroupIdAtom); - return ( - - - {groups.map((group) => ( - - - - ))} - - ); -} - -export function RecentEmojiGroup({ - label, - id, - emojis: recentEmojis, -}: { - label: string; - id: string; - emojis: IEmoji[]; -}) { - return ( - - {recentEmojis.map((emoji) => ( - - {emoji.unicode} - - ))} - - ); -} - -export function SearchEmojiGroup({ - mx, - tab, - label, - id, - emojis: searchResult, - useAuthentication, -}: { - mx: MatrixClient; - tab: EmojiBoardTab; - label: string; - id: string; - emojis: Array; - useAuthentication?: boolean; -}) { - return ( - - {tab === EmojiBoardTab.Emoji - ? searchResult.map((emoji) => - 'unicode' in emoji ? ( - - {emoji.unicode} - - ) : ( - - {emoji.body - - ) - ) - : searchResult.map((emoji) => - 'unicode' in emoji ? null : ( - - {emoji.body - - ) - )} - - ); -} - -export const CustomEmojiGroups = memo( - ({ - mx, - groups, - useAuthentication, - }: { - mx: MatrixClient; - groups: ImagePack[]; - useAuthentication?: boolean; - }) => ( - <> - {groups.map((pack) => ( - - {pack - .getImages(ImageUsage.Emoticon) - .sort((a, b) => a.shortcode.localeCompare(b.shortcode)) - .map((image) => ( - - {image.body - - ))} - - ))} - - ) -); - -export const StickerGroups = memo( - ({ - mx, - groups, - useAuthentication, - }: { - mx: MatrixClient; - groups: ImagePack[]; - useAuthentication?: boolean; - }) => ( - <> - {groups.length === 0 && ( - - - - No Sticker Packs! - - Add stickers from user, room or space settings. - - - - )} - {groups.map((pack) => ( - - {pack - .getImages(ImageUsage.Sticker) - .sort((a, b) => a.shortcode.localeCompare(b.shortcode)) - .map((image) => ( - - {image.body - - ))} - - ))} - - ) -); - -export const NativeEmojiGroups = memo( - ({ groups, labels }: { groups: IEmojiGroup[]; labels: IEmojiGroupLabels }) => ( - <> - {groups.map((emojiGroup) => ( - - {emojiGroup.emojis.map((emoji) => ( - - {emoji.unicode} - - ))} - - ))} - - ) -); +const DefaultEmojiPreview: PreviewData = { key: '🙂', shortcode: 'slight_smile' }; const SEARCH_OPTIONS: UseAsyncSearchOptions = { limit: 1000, @@ -644,6 +350,21 @@ const SEARCH_OPTIONS: UseAsyncSearchOptions = { }, }; +const VIRTUAL_OVER_SCAN = 2; + +type EmojiBoardProps = { + tab?: EmojiBoardTab; + onTabChange?: (tab: EmojiBoardTab) => void; + imagePackRooms: Room[]; + requestClose: () => void; + returnFocusOnDeactivate?: boolean; + onEmojiSelect?: (unicode: string, shortcode: string) => void; + onCustomEmojiSelect?: (mxc: string, shortcode: string) => void; + onStickerSelect?: (mxc: string, shortcode: string, label: string) => void; + allowTextCustomEmoji?: boolean; + addToRecentEmoji?: boolean; +}; + export function EmojiBoard({ tab = EmojiBoardTab.Emoji, onTabChange, @@ -655,33 +376,22 @@ export function EmojiBoard({ onStickerSelect, allowTextCustomEmoji, addToRecentEmoji = true, -}: { - tab?: EmojiBoardTab; - onTabChange?: (tab: EmojiBoardTab) => void; - imagePackRooms: Room[]; - requestClose: () => void; - returnFocusOnDeactivate?: boolean; - onEmojiSelect?: (unicode: string, shortcode: string) => void; - onCustomEmojiSelect?: (mxc: string, shortcode: string) => void; - onStickerSelect?: (mxc: string, shortcode: string, label: string) => void; - allowTextCustomEmoji?: boolean; - addToRecentEmoji?: boolean; -}) { +}: EmojiBoardProps) { + const mx = useMatrixClient(); + const emojiTab = tab === EmojiBoardTab.Emoji; - const stickerTab = tab === EmojiBoardTab.Sticker; const usage = emojiTab ? ImageUsage.Emoticon : ImageUsage.Sticker; + const previewAtom = useMemo( + () => createPreviewDataAtom(emojiTab ? DefaultEmojiPreview : undefined), + [emojiTab] + ); + const activeGroupIdAtom = useMemo(() => atom(undefined), []); const setActiveGroupId = useSetAtom(activeGroupIdAtom); - const mx = useMatrixClient(); - const useAuthentication = useMediaAuthentication(); - const emojiGroupLabels = useEmojiGroupLabels(); - const emojiGroupIcons = useEmojiGroupIcons(); const imagePacks = useRelevantImagePacks(usage, imagePackRooms); - const recentEmojis = useRecentEmoji(mx, 21); - - const contentScrollRef = useRef(null); - const emojiPreviewRef = useRef(null); - const emojiPreviewTextRef = useRef(null); + const [emojiGroupItems, stickerGroupItems] = useGroups(tab, imagePacks); + const groups = emojiTab ? emojiGroupItems : stickerGroupItems; + const renderItem = useItemRenderer(tab); const searchList = useMemo(() => { let list: Array = []; @@ -710,94 +420,73 @@ export function EmojiBoard({ { wait: 200 } ); - const syncActiveGroupId = useCallback(() => { - const targetEl = contentScrollRef.current; - if (!targetEl) return; - const groupEls = Array.from(targetEl.querySelectorAll('div[data-group-id]')) as HTMLElement[]; - const groupEl = groupEls.find((el) => isIntersectingScrollView(targetEl, el)); - const groupId = groupEl?.getAttribute('data-group-id') ?? undefined; - setActiveGroupId(groupId); - }, [setActiveGroupId]); - - const handleOnScroll: UIEventHandler = useThrottle(syncActiveGroupId, { - wait: 500, + const contentScrollRef = useRef(null); + const virtualBaseRef = useRef(null); + const virtualizer = useVirtualizer({ + count: groups.length, + getScrollElement: () => contentScrollRef.current, + estimateSize: () => 40, + overscan: VIRTUAL_OVER_SCAN, }); + const vItems = virtualizer.getVirtualItems(); - const handleScrollToGroup = (groupId: string) => { - setActiveGroupId(groupId); - const groupElement = document.getElementById(getDOMGroupId(groupId)); - groupElement?.scrollIntoView(); - }; - - const handleEmojiClick: MouseEventHandler = (evt) => { + const handleGroupItemClick: MouseEventHandler = (evt) => { const targetEl = targetFromEvent(evt.nativeEvent, 'button'); - if (!targetEl) return; - const emojiInfo = getEmojiItemInfo(targetEl); + const emojiInfo = targetEl && getEmojiItemInfo(targetEl); if (!emojiInfo) return; + if (emojiInfo.type === EmojiType.Emoji) { onEmojiSelect?.(emojiInfo.data, emojiInfo.shortcode); - if (!evt.altKey && !evt.shiftKey) { - if (addToRecentEmoji) { - addRecentEmoji(mx, emojiInfo.data); - } - requestClose(); + if (!evt.altKey && !evt.shiftKey && addToRecentEmoji) { + addRecentEmoji(mx, emojiInfo.data); } } if (emojiInfo.type === EmojiType.CustomEmoji) { onCustomEmojiSelect?.(emojiInfo.data, emojiInfo.shortcode); - if (!evt.altKey && !evt.shiftKey) requestClose(); } if (emojiInfo.type === EmojiType.Sticker) { onStickerSelect?.(emojiInfo.data, emojiInfo.shortcode, emojiInfo.label); - if (!evt.altKey && !evt.shiftKey) requestClose(); } + if (!evt.altKey && !evt.shiftKey) requestClose(); }; - const handleEmojiPreview = useCallback( - (element: HTMLButtonElement) => { - const emojiInfo = getEmojiItemInfo(element); - if (!emojiInfo || !emojiPreviewTextRef.current) return; - if (emojiInfo.type === EmojiType.Emoji && emojiPreviewRef.current) { - emojiPreviewRef.current.textContent = emojiInfo.data; - } else if (emojiInfo.type === EmojiType.CustomEmoji && emojiPreviewRef.current) { - const img = document.createElement('img'); - img.className = css.CustomEmojiImg; - img.setAttribute( - 'src', - mxcUrlToHttp(mx, emojiInfo.data, useAuthentication) || emojiInfo.data - ); - img.setAttribute('alt', emojiInfo.shortcode); - emojiPreviewRef.current.textContent = ''; - emojiPreviewRef.current.appendChild(img); - } - emojiPreviewTextRef.current.textContent = `:${emojiInfo.shortcode}:`; - }, - [mx, useAuthentication] - ); - - const throttleEmojiHover = useThrottle(handleEmojiPreview, { - wait: 200, - immediate: true, - }); - - const handleEmojiHover: MouseEventHandler = (evt) => { - const targetEl = targetFromEvent(evt.nativeEvent, 'button') as HTMLButtonElement | undefined; - if (!targetEl) return; - throttleEmojiHover(targetEl); + const handleTextCustomEmojiSelect = (textEmoji: string) => { + onCustomEmojiSelect?.(textEmoji, textEmoji); + requestClose(); }; - const handleEmojiFocus: FocusEventHandler = (evt) => { - const targetEl = evt.target as HTMLButtonElement; - handleEmojiPreview(targetEl); + const handleScrollToGroup = (groupId: string) => { + const groupIndex = groups.findIndex((group) => group.id === groupId); + virtualizer.scrollToIndex(groupIndex, { align: 'start' }); }; - // Reset scroll top on search and tab change + // sync active sidebar tab with scroll useEffect(() => { - syncActiveGroupId(); - contentScrollRef.current?.scrollTo({ - top: 0, - }); - }, [result, emojiTab, syncActiveGroupId]); + const scrollElement = contentScrollRef.current; + if (scrollElement) { + const scrollTop = scrollElement.offsetTop + scrollElement.scrollTop; + const offsetTop = virtualBaseRef.current?.offsetTop ?? 0; + const inViewVItem = vItems.find((vItem) => scrollTop < offsetTop + vItem.end); + + const group = inViewVItem ? groups[inViewVItem?.index] : undefined; + setActiveGroupId(group?.id); + } + }, [vItems, groups, setActiveGroupId, result?.query]); + + // reset scroll position on search + useEffect(() => { + const scrollElement = contentScrollRef.current; + if (scrollElement) { + scrollElement.scrollTo({ top: 0 }); + } + }, [result?.query]); + + // reset scroll position on tab change + useEffect(() => { + if (groups.length > 0) { + virtualizer.scrollToIndex(0, { align: 'start' }); + } + }, [tab, virtualizer, groups]); return ( - - {onTabChange && } - } - outlined - onClick={() => { - const searchInput = document.querySelector( - '[data-emoji-board-search="true"]' - ); - const textReaction = searchInput?.value.trim(); - if (!textReaction) return; - onCustomEmojiSelect?.(textReaction, textReaction); - requestClose(); - }} - > - React - - ) : ( - - ) - } - onChange={handleOnChange} - autoFocus={!mobileOrTablet()} - /> - - + + {onTabChange && } + + } sidebar={ - - {emojiTab && recentEmojis.length > 0 && ( - - )} - {imagePacks.length > 0 && ( - - )} - {emojiTab && ( - - )} - - } - footer={ emojiTab ? ( -
- - 😃 - - - :smiley: - -
+ ) : ( - imagePacks.length > 0 && ( -
- - :smiley: - -
- ) + ) } > - - + - + {searchedItems.map(renderItem)} + + )} +
- {searchedItems && ( - - )} - {emojiTab && recentEmojis.length > 0 && ( - - )} - {emojiTab && ( - - )} - {stickerTab && ( - - )} - {emojiTab && } - - - + {vItems.map((vItem) => { + const group = groups[vItem.index]; + + return ( + + + {group.items.map(renderItem)} + + + ); + })} +
+ {tab === EmojiBoardTab.Sticker && groups.length === 0 && } +
+ +
); diff --git a/src/app/components/emoji-board/components/Group.tsx b/src/app/components/emoji-board/components/Group.tsx new file mode 100644 index 00000000..cf19c6e0 --- /dev/null +++ b/src/app/components/emoji-board/components/Group.tsx @@ -0,0 +1,34 @@ +import { as, Box, Text } from 'folds'; +import React, { ReactNode } from 'react'; +import classNames from 'classnames'; +import * as css from './styles.css'; + +export const getDOMGroupId = (id: string): string => `EmojiBoardGroup-${id}`; + +export const EmojiGroup = as< + 'div', + { + id: string; + label: string; + children: ReactNode; + } +>(({ className, id, label, children, ...props }, ref) => ( + + + {label} + +
+ + {children} + +
+
+)); diff --git a/src/app/components/emoji-board/components/Item.tsx b/src/app/components/emoji-board/components/Item.tsx new file mode 100644 index 00000000..c3fd3c3a --- /dev/null +++ b/src/app/components/emoji-board/components/Item.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { Box } from 'folds'; +import { MatrixClient } from 'matrix-js-sdk'; +import { EmojiItemInfo, EmojiType } from '../types'; +import * as css from './styles.css'; +import { PackImageReader } from '../../../plugins/custom-emoji'; +import { IEmoji } from '../../../plugins/emoji'; +import { mxcUrlToHttp } from '../../../utils/matrix'; + +export const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => { + const label = element.getAttribute('title'); + const type = element.getAttribute('data-emoji-type') as EmojiType | undefined; + const data = element.getAttribute('data-emoji-data'); + const shortcode = element.getAttribute('data-emoji-shortcode'); + + if (type && data && shortcode && label) + return { + type, + data, + shortcode, + label, + }; + return undefined; +}; + +type EmojiItemProps = { + emoji: IEmoji; +}; +export function EmojiItem({ emoji }: EmojiItemProps) { + return ( + + {emoji.unicode} + + ); +} + +type CustomEmojiItemProps = { + mx: MatrixClient; + useAuthentication?: boolean; + image: PackImageReader; +}; +export function CustomEmojiItem({ mx, useAuthentication, image }: CustomEmojiItemProps) { + return ( + + {image.body + + ); +} + +type StickerItemProps = { + mx: MatrixClient; + useAuthentication?: boolean; + image: PackImageReader; +}; + +export function StickerItem({ mx, useAuthentication, image }: StickerItemProps) { + return ( + + {image.body + + ); +} diff --git a/src/app/components/emoji-board/components/Layout.tsx b/src/app/components/emoji-board/components/Layout.tsx new file mode 100644 index 00000000..392d4a31 --- /dev/null +++ b/src/app/components/emoji-board/components/Layout.tsx @@ -0,0 +1,30 @@ +import { as, Box, Line } from 'folds'; +import React, { ReactNode } from 'react'; +import classNames from 'classnames'; +import * as css from './styles.css'; + +export const EmojiBoardLayout = as< + 'div', + { + header: ReactNode; + sidebar?: ReactNode; + children: ReactNode; + } +>(({ className, header, sidebar, children, ...props }, ref) => ( + + + + {header} + + {children} + + + {sidebar} + +)); diff --git a/src/app/components/emoji-board/components/NoStickerPacks.tsx b/src/app/components/emoji-board/components/NoStickerPacks.tsx new file mode 100644 index 00000000..6703362c --- /dev/null +++ b/src/app/components/emoji-board/components/NoStickerPacks.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Box, toRem, config, Icons, Icon, Text } from 'folds'; + +export function NoStickerPacks() { + return ( + + + + No Sticker Packs! + + Add stickers from user, room or space settings. + + + + ); +} diff --git a/src/app/components/emoji-board/components/Preview.tsx b/src/app/components/emoji-board/components/Preview.tsx new file mode 100644 index 00000000..3f5f8d3a --- /dev/null +++ b/src/app/components/emoji-board/components/Preview.tsx @@ -0,0 +1,53 @@ +import { Box, Text } from 'folds'; +import React from 'react'; +import { Atom, atom, useAtomValue } from 'jotai'; +import * as css from './styles.css'; +import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; +import { mxcUrlToHttp } from '../../../utils/matrix'; + +export type PreviewData = { + key: string; + shortcode: string; +}; + +export const createPreviewDataAtom = (initial?: PreviewData) => + atom(initial); + +type PreviewProps = { + previewAtom: Atom; +}; +export function Preview({ previewAtom }: PreviewProps) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + + const { key, shortcode } = useAtomValue(previewAtom) ?? {}; + + if (!shortcode) return null; + + return ( + + {key && ( + + {key.startsWith('mxc://') ? ( + {shortcode} + ) : ( + key + )} + + )} + + :{shortcode}: + + + ); +} diff --git a/src/app/components/emoji-board/components/SearchInput.tsx b/src/app/components/emoji-board/components/SearchInput.tsx new file mode 100644 index 00000000..6de4d977 --- /dev/null +++ b/src/app/components/emoji-board/components/SearchInput.tsx @@ -0,0 +1,51 @@ +import React, { ChangeEventHandler, useRef } from 'react'; +import { Input, Chip, Icon, Icons, Text } from 'folds'; +import { mobileOrTablet } from '../../../utils/user-agent'; + +type SearchInputProps = { + query?: string; + onChange: ChangeEventHandler; + allowTextCustomEmoji?: boolean; + onTextCustomEmojiSelect?: (text: string) => void; +}; +export function SearchInput({ + query, + onChange, + allowTextCustomEmoji, + onTextCustomEmojiSelect, +}: SearchInputProps) { + const inputRef = useRef(null); + + const handleReact = () => { + const textEmoji = inputRef.current?.value.trim(); + if (!textEmoji) return; + onTextCustomEmojiSelect?.(textEmoji); + }; + + return ( + } + outlined + onClick={handleReact} + > + React + + ) : ( + + ) + } + onChange={onChange} + autoFocus={!mobileOrTablet()} + /> + ); +} diff --git a/src/app/components/emoji-board/components/Sidebar.tsx b/src/app/components/emoji-board/components/Sidebar.tsx new file mode 100644 index 00000000..de22b483 --- /dev/null +++ b/src/app/components/emoji-board/components/Sidebar.tsx @@ -0,0 +1,130 @@ +import React, { ReactNode } from 'react'; +import { + Box, + Scroll, + Line, + as, + TooltipProvider, + Tooltip, + Text, + IconButton, + Icon, + IconSrc, + Icons, +} from 'folds'; +import classNames from 'classnames'; +import * as css from './styles.css'; + +export function Sidebar({ children }: { children: ReactNode }) { + return ( + + + + {children} + + + + ); +} + +export const SidebarStack = as<'div'>(({ className, children, ...props }, ref) => ( + + {children} + +)); +export function SidebarDivider() { + return ; +} + +function SidebarBtn({ + active, + label, + id, + onClick, + children, +}: { + active?: boolean; + label: string; + id: T; + onClick: (id: T) => void; + children: ReactNode; +}) { + return ( + + {label} + + } + > + {(ref) => ( + onClick(id)} + size="400" + radii="300" + variant="Surface" + > + {children} + + )} + + ); +} + +type GroupIconProps = { + active: boolean; + id: T; + label: string; + icon: IconSrc; + onClick: (id: T) => void; +}; +export function GroupIcon({ + active, + id, + label, + icon, + onClick, +}: GroupIconProps) { + return ( + + + + ); +} + +type ImageGroupIconProps = { + active: boolean; + id: T; + label: string; + url?: string; + onClick: (id: T) => void; +}; +export function ImageGroupIcon({ + active, + id, + label, + url, + onClick, +}: ImageGroupIconProps) { + return ( + + {url ? ( + {label} + ) : ( + + )} + + ); +} diff --git a/src/app/components/emoji-board/components/Tabs.tsx b/src/app/components/emoji-board/components/Tabs.tsx new file mode 100644 index 00000000..d433354f --- /dev/null +++ b/src/app/components/emoji-board/components/Tabs.tsx @@ -0,0 +1,44 @@ +import React, { CSSProperties } from 'react'; +import { Badge, Box, Text } from 'folds'; +import { EmojiBoardTab } from '../types'; + +const styles: CSSProperties = { + cursor: 'pointer', +}; + +export function EmojiBoardTabs({ + tab, + onTabChange, +}: { + tab: EmojiBoardTab; + onTabChange: (tab: EmojiBoardTab) => void; +}) { + return ( + + onTabChange(EmojiBoardTab.Sticker)} + > + + Sticker + + + onTabChange(EmojiBoardTab.Emoji)} + > + + Emoji + + + + ); +} diff --git a/src/app/components/emoji-board/components/index.tsx b/src/app/components/emoji-board/components/index.tsx new file mode 100644 index 00000000..55506668 --- /dev/null +++ b/src/app/components/emoji-board/components/index.tsx @@ -0,0 +1,8 @@ +export * from './SearchInput'; +export * from './Tabs'; +export * from './Sidebar'; +export * from './NoStickerPacks'; +export * from './Preview'; +export * from './Item'; +export * from './Group'; +export * from './Layout'; diff --git a/src/app/components/emoji-board/EmojiBoard.css.tsx b/src/app/components/emoji-board/components/styles.css.ts similarity index 83% rename from src/app/components/emoji-board/EmojiBoard.css.tsx rename to src/app/components/emoji-board/components/styles.css.ts index ba4ca4e0..c86a08d8 100644 --- a/src/app/components/emoji-board/EmojiBoard.css.tsx +++ b/src/app/components/emoji-board/components/styles.css.ts @@ -1,5 +1,9 @@ import { style } from '@vanilla-extract/css'; -import { DefaultReset, FocusOutline, color, config, toRem } from 'folds'; +import { toRem, color, config, DefaultReset, FocusOutline } from 'folds'; + +/** + * Layout + */ export const Base = style({ maxWidth: toRem(432), @@ -13,6 +17,15 @@ export const Base = style({ overflow: 'hidden', }); +export const Header = style({ + padding: config.space.S300, + paddingBottom: 0, +}); + +/** + * Sidebar + */ + export const Sidebar = style({ width: toRem(54), backgroundColor: color.Surface.Container, @@ -29,26 +42,21 @@ export const SidebarStack = style({ backgroundColor: color.Surface.Container, }); -export const NativeEmojiSidebarStack = style({ - position: 'sticky', - bottom: '-67%', - zIndex: 1, -}); - export const SidebarDivider = style({ width: toRem(18), }); -export const Header = style({ - padding: config.space.S300, - paddingBottom: 0, +export const SidebarBtnImg = style({ + width: toRem(24), + height: toRem(24), + objectFit: 'contain', }); -export const EmojiBoardTab = style({ - cursor: 'pointer', -}); +/** + * Preview + */ -export const Footer = style({ +export const Preview = style({ padding: config.space.S200, margin: config.space.S300, marginTop: 0, @@ -59,7 +67,30 @@ export const Footer = style({ color: color.SurfaceVariant.OnContainer, }); +export const PreviewEmoji = style([ + DefaultReset, + { + width: toRem(32), + height: toRem(32), + fontSize: toRem(32), + lineHeight: toRem(32), + }, +]); +export const PreviewImg = style([ + DefaultReset, + { + width: toRem(32), + height: toRem(32), + objectFit: 'contain', + }, +]); + +/** + * Group + */ + export const EmojiGroup = style({ + position: 'relative', padding: `${config.space.S300} 0`, }); @@ -82,15 +113,9 @@ export const EmojiGroupContent = style([ }, ]); -export const EmojiPreview = style([ - DefaultReset, - { - width: toRem(32), - height: toRem(32), - fontSize: toRem(32), - lineHeight: toRem(32), - }, -]); +/** + * Item + */ export const EmojiItem = style([ DefaultReset, diff --git a/src/app/components/emoji-board/index.ts b/src/app/components/emoji-board/index.ts index 430cec07..7b1cce3b 100644 --- a/src/app/components/emoji-board/index.ts +++ b/src/app/components/emoji-board/index.ts @@ -1 +1,2 @@ export * from './EmojiBoard'; +export * from './types'; diff --git a/src/app/components/emoji-board/types.ts b/src/app/components/emoji-board/types.ts new file mode 100644 index 00000000..de94cc56 --- /dev/null +++ b/src/app/components/emoji-board/types.ts @@ -0,0 +1,17 @@ +export enum EmojiBoardTab { + Emoji = 'Emoji', + Sticker = 'Sticker', +} + +export enum EmojiType { + Emoji = 'emoji', + CustomEmoji = 'customEmoji', + Sticker = 'sticker', +} + +export type EmojiItemInfo = { + type: EmojiType; + data: string; + shortcode: string; + label: string; +}; diff --git a/src/app/components/event-readers/EventReaders.tsx b/src/app/components/event-readers/EventReaders.tsx index de1416b6..c7900237 100644 --- a/src/app/components/event-readers/EventReaders.tsx +++ b/src/app/components/event-readers/EventReaders.tsx @@ -19,9 +19,11 @@ import { getMemberDisplayName } from '../../utils/room'; import { getMxIdLocalPart } from '../../utils/matrix'; import * as css from './EventReaders.css'; import { useMatrixClient } from '../../hooks/useMatrixClient'; -import { openProfileViewer } from '../../../client/action/navigation'; import { UserAvatar } from '../user-avatar'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; +import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile'; +import { useSpaceOptionally } from '../../hooks/useSpace'; +import { getMouseEventCords } from '../../utils/dom'; export type EventReadersProps = { room: Room; @@ -33,6 +35,8 @@ export const EventReaders = as<'div', EventReadersProps>( const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const latestEventReaders = useRoomEventReaders(room, eventId); + const openProfile = useOpenUserRoomProfile(); + const space = useSpaceOptionally(); const getName = (userId: string) => getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId; @@ -57,19 +61,32 @@ export const EventReaders = as<'div', EventReadersProps>( {latestEventReaders.map((readerId) => { const name = getName(readerId); - const avatarMxcUrl = room - .getMember(readerId) - ?.getMxcAvatarUrl(); - const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp(avatarMxcUrl, 100, 100, 'crop', undefined, false, useAuthentication) : undefined; + const avatarMxcUrl = room.getMember(readerId)?.getMxcAvatarUrl(); + const avatarUrl = avatarMxcUrl + ? mx.mxcUrlToHttp( + avatarMxcUrl, + 100, + 100, + 'crop', + undefined, + false, + useAuthentication + ) + : undefined; return ( { - requestClose(); - openProfileViewer(readerId, room.roomId); + onClick={(event) => { + openProfile( + room.roomId, + space?.roomId, + readerId, + getMouseEventCords(event.nativeEvent), + 'Bottom' + ); }} before={ diff --git a/src/app/components/image-pack-view/RoomImagePack.tsx b/src/app/components/image-pack-view/RoomImagePack.tsx index 9dd45c1f..92b4ff21 100644 --- a/src/app/components/image-pack-view/RoomImagePack.tsx +++ b/src/app/components/image-pack-view/RoomImagePack.tsx @@ -1,12 +1,14 @@ import React, { useCallback, useMemo } from 'react'; import { Room } from 'matrix-js-sdk'; -import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; +import { usePowerLevels } from '../../hooks/usePowerLevels'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { ImagePackContent } from './ImagePackContent'; import { ImagePack, PackContent } from '../../plugins/custom-emoji'; import { StateEvent } from '../../../types/matrix/room'; import { useRoomImagePack } from '../../hooks/useImagePacks'; import { randomStr } from '../../utils/common'; +import { useRoomPermissions } from '../../hooks/useRoomPermissions'; +import { useRoomCreators } from '../../hooks/useRoomCreators'; type RoomImagePackProps = { room: Room; @@ -17,9 +19,10 @@ export function RoomImagePack({ room, stateKey }: RoomImagePackProps) { const mx = useMatrixClient(); const userId = mx.getUserId()!; const powerLevels = usePowerLevels(room); + const creators = useRoomCreators(room); - const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); - const canEditImagePack = canSendStateEvent(StateEvent.PoniesRoomEmotes, getPowerLevel(userId)); + const permissions = useRoomPermissions(creators, powerLevels); + const canEditImagePack = permissions.stateEvent(StateEvent.PoniesRoomEmotes, userId); const fallbackPack = useMemo(() => { const fakePackId = randomStr(4); diff --git a/src/app/components/invite-user-prompt/InviteUserPrompt.tsx b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx new file mode 100644 index 00000000..ddac0576 --- /dev/null +++ b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx @@ -0,0 +1,291 @@ +import React, { + ChangeEventHandler, + FormEventHandler, + KeyboardEventHandler, + useCallback, + useMemo, + useRef, + useState, +} from 'react'; +import { + Overlay, + OverlayBackdrop, + OverlayCenter, + Box, + Header, + config, + Text, + IconButton, + Icon, + Icons, + Input, + Button, + Spinner, + color, + TextArea, + Dialog, + Menu, + toRem, + Scroll, + MenuItem, +} from 'folds'; +import { Room } from 'matrix-js-sdk'; +import { isKeyHotkey } from 'is-hotkey'; +import FocusTrap from 'focus-trap-react'; +import { stopPropagation } from '../../utils/keyboard'; +import { useDirectUsers } from '../../hooks/useDirectUsers'; +import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix'; +import { Membership } from '../../../types/matrix/room'; +import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch'; +import { highlightText, makeHighlightRegex } from '../../plugins/react-custom-html-parser'; +import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { BreakWord } from '../../styles/Text.css'; +import { useAlive } from '../../hooks/useAlive'; + +const SEARCH_OPTIONS: UseAsyncSearchOptions = { + limit: 1000, + matchOptions: { + contain: true, + }, +}; +const getUserIdString = (userId: string) => getMxIdLocalPart(userId) ?? userId; + +type InviteUserProps = { + room: Room; + requestClose: () => void; +}; +export function InviteUserPrompt({ room, requestClose }: InviteUserProps) { + const mx = useMatrixClient(); + const alive = useAlive(); + + const inputRef = useRef(null); + const directUsers = useDirectUsers(); + const [validUserId, setValidUserId] = useState(); + + const filteredUsers = useMemo( + () => + directUsers.filter((userId) => { + const membership = room.getMember(userId)?.membership; + return membership !== Membership.Join; + }), + [directUsers, room] + ); + const [result, search, resetSearch] = useAsyncSearch( + filteredUsers, + getUserIdString, + SEARCH_OPTIONS + ); + const queryHighlighRegex = result?.query + ? makeHighlightRegex(result.query.split(' ')) + : undefined; + + const [inviteState, invite] = useAsyncCallback( + useCallback( + async (userId, reason) => { + await mx.invite(room.roomId, userId, reason); + }, + [mx, room] + ) + ); + + const inviting = inviteState.status === AsyncStatus.Loading; + + const handleReset = () => { + if (inputRef.current) inputRef.current.value = ''; + setValidUserId(undefined); + resetSearch(); + }; + + const handleSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + const target = evt.target as HTMLFormElement | undefined; + + if (inviting || !validUserId) return; + + const reasonInput = target?.reasonInput as HTMLTextAreaElement | undefined; + const reason = reasonInput?.value.trim(); + + invite(validUserId, reason || undefined).then(() => { + if (alive()) { + handleReset(); + if (reasonInput) reasonInput.value = ''; + } + }); + }; + + const handleSearchChange: ChangeEventHandler = (evt) => { + const value = evt.currentTarget.value.trim(); + if (isUserId(value)) { + setValidUserId(value); + } else { + setValidUserId(undefined); + const term = getMxIdLocalPart(value) ?? (value.startsWith('@') ? value.slice(1) : value); + if (term) { + search(term); + } else { + resetSearch(); + } + } + }; + + const handleUserId = (userId: string) => { + if (inputRef.current) { + inputRef.current.value = userId; + setValidUserId(userId); + resetSearch(); + inputRef.current.focus(); + } + }; + + const handleKeyDown: KeyboardEventHandler = (evt) => { + if (isKeyHotkey('escape', evt)) { + resetSearch(); + return; + } + if (isKeyHotkey('tab', evt) && result && result.items.length > 0) { + evt.preventDefault(); + const userId = result.items[0]; + handleUserId(userId); + } + }; + + return ( + }> + + inputRef.current, + clickOutsideDeactivates: true, + onDeactivate: requestClose, + escapeDeactivates: stopPropagation, + }} + > + + +
+ + + Invite + + + + + + + +
+ + + User ID +
+ + {result && result.items.length > 0 && ( + isKeyHotkey('arrowdown', evt), + isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), + escapeDeactivates: stopPropagation, + }} + > + + + +
+ {result.items.map((userId) => { + const username = `${getMxIdLocalPart(userId)}`; + const userServer = getMxIdServer(userId); + + return ( + handleUserId(userId)} + after={ + + {userServer} + + } + disabled={inviting} + > + + + + {queryHighlighRegex + ? highlightText(queryHighlighRegex, [ + username ?? userId, + ]) + : username} + + + + + ); + })} +
+
+
+
+
+ )} +
+
+ + Reason (Optional) +