diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index 450e4e29..441da0de 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.4.0 + uses: actions/setup-node@v4.3.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 b330c3c1..9c0bea78 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@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 + uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295 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@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 + uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295 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 398785ab..4e88c78d 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.18.0 + uses: docker/build-push-action@v6.15.0 with: context: . push: false diff --git a/.github/workflows/netlify-dev.yml b/.github/workflows/netlify-dev.yml index 66cd5ad5..34308c21 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.4.0 + uses: actions/setup-node@v4.3.0 with: node-version: 20.12.2 cache: 'npm' diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml index 24edda96..44205ff2 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.4.0 + uses: actions/setup-node@v4.3.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@72f2c25fcb47643c292f7107632f7a47c1df5cd8 + uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda 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.5.0 + uses: docker/login-action@v3.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Login to the Container registry - uses: docker/login-action@v3.5.0 + uses: docker/login-action@v3.4.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.8.0 + uses: docker/metadata-action@v5.7.0 with: images: | ${{ secrets.DOCKER_USERNAME }}/cinny ghcr.io/${{ github.repository }} - name: Build and push Docker image - uses: docker/build-push-action@v6.18.0 + uses: docker/build-push-action@v6.15.0 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/Dockerfile b/Dockerfile index 718fed72..abb65ee5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN npm run build ## App -FROM nginx:1.29.0-alpine +FROM nginx:1.27.4-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 2eefeee2..9196cf3d 100644 --- a/index.html +++ b/index.html @@ -90,7 +90,6 @@ window.global ||= window;
-
diff --git a/package-lock.json b/package-lock.json index 4aacff72..f85dd74d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cinny", - "version": "4.10.0", + "version": "4.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cinny", - "version": "4.10.0", + "version": "4.6.0", "license": "AGPL-3.0-only", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "1.1.6", @@ -16,11 +16,11 @@ "@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", "await-to-js": "3.0.0", - "badwords-list": "2.0.1-4", "blurhash": "2.0.4", "browser-encrypt-attachment": "0.3.0", "chroma-js": "3.1.2", @@ -31,8 +31,10 @@ "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.3.0", + "folds": "2.1.0", + "formik": "2.4.6", "html-dom-parser": "4.0.0", "html-react-parser": "4.2.0", "i18next": "23.12.2", @@ -43,18 +45,21 @@ "jotai": "2.6.0", "linkify-react": "4.1.3", "linkifyjs": "4.1.3", - "matrix-js-sdk": "37.5.0", + "matrix-js-sdk": "35.0.0", "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", @@ -62,6 +67,7 @@ "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": { @@ -90,8 +96,9 @@ "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.19", + "vite": "5.4.15", "vite-plugin-pwa": "0.20.5", "vite-plugin-static-copy": "1.0.4", "vite-plugin-top-level-await": "1.4.4" @@ -2256,19 +2263,17 @@ } }, "node_modules/@matrix-org/matrix-sdk-crypto-wasm": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-14.1.0.tgz", - "integrity": "sha512-vcSxHJIr6lP0Fgo8jl0sTHg+OZxZn+skGjiyB62erfgw/R2QqJl0ZVSY8SRcbk9LtHo/ZGld1tnaOyjL2e3cLQ==", - "license": "Apache-2.0", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-11.1.0.tgz", + "integrity": "sha512-JPuO9RCVDklDjbFzMvZfQb7PuiFkLY72bniRSu81lRzkkrcbZtmKqBFMm9H4f2FSz+tHVkDnmsvn12I4sdJJ5A==", "engines": { - "node": ">= 18" + "node": ">= 10" } }, "node_modules/@matrix-org/olm": { "version": "3.2.15", "resolved": "https://registry.npmjs.org/@matrix-org/olm/-/olm-3.2.15.tgz", - "integrity": "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q==", - "license": "Apache-2.0" + "integrity": "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q==" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -2305,6 +2310,15 @@ "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", @@ -4507,6 +4521,18 @@ "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", @@ -4563,8 +4589,7 @@ "node_modules/@types/events": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", - "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", - "license": "MIT" + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==" }, "node_modules/@types/file-saver": { "version": "2.0.5", @@ -4572,6 +4597,15 @@ "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", @@ -4605,14 +4639,12 @@ "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==", - "dev": true + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==" }, "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": "*", @@ -4646,8 +4678,7 @@ "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "license": "MIT" + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, "node_modules/@types/sanitize-html": { "version": "2.9.0", @@ -4661,8 +4692,7 @@ "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==", - "dev": true + "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==" }, "node_modules/@types/semver": { "version": "7.5.8", @@ -5058,8 +5088,7 @@ "node_modules/another-json": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz", - "integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==", - "license": "Apache-2.0" + "integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==" }, "node_modules/ansi-regex": { "version": "5.0.1", @@ -5285,6 +5314,11 @@ "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", @@ -5306,6 +5340,11 @@ "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", @@ -5392,12 +5431,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/badwords-list": { - "version": "2.0.1-4", - "resolved": "https://registry.npmjs.org/badwords-list/-/badwords-list-2.0.1-4.tgz", - "integrity": "sha512-FxfZUp7B9yCnesNtFQS9v6PvZdxTYa14Q60JR6vhjdQdWI4naTjJIyx22JzoER8ooeT8SAAKoHLjKfCV7XgYUQ==", - "license": "MIT" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5405,10 +5438,9 @@ "devOptional": true }, "node_modules/base-x": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", - "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", - "license": "MIT" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz", + "integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -5514,7 +5546,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", - "license": "MIT", "dependencies": { "base-x": "^5.0.0" } @@ -5785,6 +5816,11 @@ "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", @@ -5812,7 +5848,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -5846,6 +5881,14 @@ "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", @@ -6956,11 +6999,15 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", "engines": { "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", @@ -7032,6 +7079,33 @@ "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", @@ -7140,6 +7214,18 @@ "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", @@ -7163,16 +7249,15 @@ } }, "node_modules/folds": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/folds/-/folds-2.3.0.tgz", - "integrity": "sha512-1KoM21jrg5daxvKrmSY0V04wa946KlNT0z6h017Rsnw2fdtNC6J0f34Ce5GF46Tzi00gZ/7SvCDXMzW/7e5s0w==", - "license": "Apache-2.0", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/folds/-/folds-2.1.0.tgz", + "integrity": "sha512-KwAG8bH3jsyZ9FKPMg+6ABV2YOcpp4nL0cCelsalnaPeRThkc5fgG1Xj5mhmdffYKjEXpEbERi5qmGbepgJryg==", "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": { @@ -7184,6 +7269,38 @@ "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", @@ -7762,6 +7879,12 @@ "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", @@ -8434,7 +8557,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "license": "MIT", "engines": { "node": ">=18" } @@ -8496,6 +8618,17 @@ "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", @@ -8529,6 +8662,11 @@ "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", @@ -8551,7 +8689,6 @@ "version": "1.9.2", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", - "license": "MIT", "engines": { "node": ">= 0.6.0" }, @@ -8627,23 +8764,21 @@ "node_modules/matrix-events-sdk": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", - "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==", - "license": "Apache-2.0" + "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==" }, "node_modules/matrix-js-sdk": { - "version": "37.5.0", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-37.5.0.tgz", - "integrity": "sha512-5tyuAi5hnKud1UkVq8Z2/3c22hWGELBZzErJPZkE6Hju2uGUfGtrIx6uj6puv0ZjvsUU3X6Qgm8vdReKO1PGig==", - "license": "Apache-2.0", + "version": "35.0.0", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-35.0.0.tgz", + "integrity": "sha512-X8hIsd/8x1SC9vRr8DiNKQxmdrfRujtvEWPz8mY4FxVDJG8HEGDHvqUmaSy2jrtnOUn4oHzGQVLFO3DnhsSf8w==", "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^14.0.1", + "@matrix-org/matrix-sdk-crypto-wasm": "^11.0.0", "@matrix-org/olm": "3.2.15", "another-json": "^0.2.0", "bs58": "^6.0.0", "content-type": "^1.0.4", "jwt-decode": "^4.0.0", - "loglevel": "^1.9.2", + "loglevel": "^1.7.1", "matrix-events-sdk": "0.0.1", "matrix-widget-api": "^1.10.0", "oidc-client-ts": "^3.0.1", @@ -8657,23 +8792,21 @@ } }, "node_modules/matrix-js-sdk/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "license": "MIT", "bin": { "uuid": "dist/esm/bin/uuid" } }, "node_modules/matrix-widget-api": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.13.1.tgz", - "integrity": "sha512-mkOHUVzaN018TCbObfGOSaMW2GoUxOfcxNNlTVx5/HeMk3OSQPQM0C9oEME5Liiv/dBUoSrEB64V8wF7e/gb1w==", - "license": "Apache-2.0", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.12.0.tgz", + "integrity": "sha512-6JRd9fJGGvuBRhcTg9wX+Skn/Q1wox3jdp5yYQKJ6pPw4urW9bkTR90APBKVDB1vorJKT44jml+lCzkDMRBjww==", "dependencies": { "@types/events": "^3.0.0", "events": "^3.2.0" @@ -9065,10 +9198,9 @@ } }, "node_modules/oidc-client-ts": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.2.1.tgz", - "integrity": "sha512-hS5AJ5s/x4bXhHvNJT1v+GGvzHUwdRWqNQQbSrp10L1IRmzfRGKQ3VWN3dstJb+oF3WtAyKezwD2+dTEIyBiAA==", - "license": "Apache-2.0", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.1.0.tgz", + "integrity": "sha512-IDopEXjiwjkmJLYZo6BTlvwOtnlSniWZkKZoXforC/oLZHC9wkIxd25Kwtmo5yKFMMVcsp3JY6bhcNJqdYk8+g==", "dependencies": { "jwt-decode": "^4.0.0" }, @@ -9156,7 +9288,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "license": "MIT", "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" @@ -9385,6 +9516,14 @@ "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", @@ -9508,6 +9647,20 @@ "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", @@ -9550,6 +9703,11 @@ "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", @@ -9588,6 +9746,29 @@ "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", @@ -9870,7 +10051,6 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", "engines": { "node": ">= 4" } @@ -10046,6 +10226,23 @@ "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", @@ -10067,7 +10264,6 @@ "version": "2.15.0", "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz", "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==", - "license": "MIT", "bin": { "sdp-verify": "checker.js" } @@ -10145,6 +10341,11 @@ "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", @@ -10741,6 +10942,14 @@ "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", @@ -10963,8 +11172,7 @@ "node_modules/unhomoglyph": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", - "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==", - "license": "MIT" + "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -11095,9 +11303,9 @@ } }, "node_modules/vite": { - "version": "5.4.19", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", - "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "version": "5.4.15", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz", + "integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==", "license": "MIT", "dependencies": { "esbuild": "^0.21.3", @@ -11655,6 +11863,14 @@ "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 5a2356f4..b489dc2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cinny", - "version": "4.10.0", + "version": "4.6.0", "description": "Yet another matrix client", "main": "index.js", "type": "module", @@ -27,11 +27,11 @@ "@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", "await-to-js": "3.0.0", - "badwords-list": "2.0.1-4", "blurhash": "2.0.4", "browser-encrypt-attachment": "0.3.0", "chroma-js": "3.1.2", @@ -42,8 +42,10 @@ "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.3.0", + "folds": "2.1.0", + "formik": "2.4.6", "html-dom-parser": "4.0.0", "html-react-parser": "4.2.0", "i18next": "23.12.2", @@ -54,18 +56,21 @@ "jotai": "2.6.0", "linkify-react": "4.1.3", "linkifyjs": "4.1.3", - "matrix-js-sdk": "37.5.0", + "matrix-js-sdk": "35.0.0", "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", @@ -73,6 +78,7 @@ "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": { @@ -101,8 +107,9 @@ "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.19", + "vite": "5.4.15", "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 new file mode 100644 index 00000000..87b2588d --- /dev/null +++ b/public/res/ic/filled/category.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/public/res/ic/filled/pin.svg b/public/res/ic/filled/pin.svg new file mode 100644 index 00000000..6a701474 --- /dev/null +++ b/public/res/ic/filled/pin.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/filled/star.svg b/public/res/ic/filled/star.svg new file mode 100644 index 00000000..378c891e --- /dev/null +++ b/public/res/ic/filled/star.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/public/res/ic/outlined/add-pin.svg b/public/res/ic/outlined/add-pin.svg new file mode 100644 index 00000000..9634bede --- /dev/null +++ b/public/res/ic/outlined/add-pin.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/add-user.svg b/public/res/ic/outlined/add-user.svg new file mode 100644 index 00000000..c3803d80 --- /dev/null +++ b/public/res/ic/outlined/add-user.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/ball.svg b/public/res/ic/outlined/ball.svg new file mode 100644 index 00000000..d4b89ff5 --- /dev/null +++ b/public/res/ic/outlined/ball.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/public/res/ic/outlined/bell-off.svg b/public/res/ic/outlined/bell-off.svg new file mode 100644 index 00000000..79ce8a33 --- /dev/null +++ b/public/res/ic/outlined/bell-off.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/bell-ping.svg b/public/res/ic/outlined/bell-ping.svg new file mode 100644 index 00000000..3431bea1 --- /dev/null +++ b/public/res/ic/outlined/bell-ping.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/bell-ring.svg b/public/res/ic/outlined/bell-ring.svg new file mode 100644 index 00000000..57fc2679 --- /dev/null +++ b/public/res/ic/outlined/bell-ring.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/bell.svg b/public/res/ic/outlined/bell.svg new file mode 100644 index 00000000..43d470b5 --- /dev/null +++ b/public/res/ic/outlined/bell.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/bin.svg b/public/res/ic/outlined/bin.svg new file mode 100644 index 00000000..984be625 --- /dev/null +++ b/public/res/ic/outlined/bin.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/public/res/ic/outlined/bulb.svg b/public/res/ic/outlined/bulb.svg new file mode 100644 index 00000000..00e80886 --- /dev/null +++ b/public/res/ic/outlined/bulb.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/public/res/ic/outlined/category.svg b/public/res/ic/outlined/category.svg new file mode 100644 index 00000000..c7c33b38 --- /dev/null +++ b/public/res/ic/outlined/category.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/check.svg b/public/res/ic/outlined/check.svg new file mode 100644 index 00000000..72a18327 --- /dev/null +++ b/public/res/ic/outlined/check.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/chevron-bottom.svg b/public/res/ic/outlined/chevron-bottom.svg new file mode 100644 index 00000000..5562b7aa --- /dev/null +++ b/public/res/ic/outlined/chevron-bottom.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/public/res/ic/outlined/chevron-left.svg b/public/res/ic/outlined/chevron-left.svg new file mode 100644 index 00000000..ba9e12cc --- /dev/null +++ b/public/res/ic/outlined/chevron-left.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/public/res/ic/outlined/chevron-right.svg b/public/res/ic/outlined/chevron-right.svg new file mode 100644 index 00000000..7f6a806e --- /dev/null +++ b/public/res/ic/outlined/chevron-right.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/public/res/ic/outlined/chevron-top.svg b/public/res/ic/outlined/chevron-top.svg new file mode 100644 index 00000000..f5948fe9 --- /dev/null +++ b/public/res/ic/outlined/chevron-top.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/public/res/ic/outlined/circle-plus.svg b/public/res/ic/outlined/circle-plus.svg new file mode 100644 index 00000000..41690a08 --- /dev/null +++ b/public/res/ic/outlined/circle-plus.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/cmd.svg b/public/res/ic/outlined/cmd.svg new file mode 100644 index 00000000..75ae0d98 --- /dev/null +++ b/public/res/ic/outlined/cmd.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/coin.svg b/public/res/ic/outlined/coin.svg new file mode 100644 index 00000000..025424e8 --- /dev/null +++ b/public/res/ic/outlined/coin.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/cross.svg b/public/res/ic/outlined/cross.svg new file mode 100644 index 00000000..0acda884 --- /dev/null +++ b/public/res/ic/outlined/cross.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/cup.svg b/public/res/ic/outlined/cup.svg new file mode 100644 index 00000000..8921e2c9 --- /dev/null +++ b/public/res/ic/outlined/cup.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/public/res/ic/outlined/dog.svg b/public/res/ic/outlined/dog.svg new file mode 100644 index 00000000..3b252956 --- /dev/null +++ b/public/res/ic/outlined/dog.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/download.svg b/public/res/ic/outlined/download.svg new file mode 100644 index 00000000..677014f3 --- /dev/null +++ b/public/res/ic/outlined/download.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/emoji-add.svg b/public/res/ic/outlined/emoji-add.svg new file mode 100644 index 00000000..c4cacef2 --- /dev/null +++ b/public/res/ic/outlined/emoji-add.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/public/res/ic/outlined/emoji.svg b/public/res/ic/outlined/emoji.svg new file mode 100644 index 00000000..0daac879 --- /dev/null +++ b/public/res/ic/outlined/emoji.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/explore.svg b/public/res/ic/outlined/explore.svg new file mode 100644 index 00000000..7cc2a479 --- /dev/null +++ b/public/res/ic/outlined/explore.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/external.svg b/public/res/ic/outlined/external.svg new file mode 100644 index 00000000..adade1bd --- /dev/null +++ b/public/res/ic/outlined/external.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/res/ic/outlined/eye-blind.svg b/public/res/ic/outlined/eye-blind.svg new file mode 100644 index 00000000..fbc8e2ae --- /dev/null +++ b/public/res/ic/outlined/eye-blind.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/res/ic/outlined/eye.svg b/public/res/ic/outlined/eye.svg new file mode 100644 index 00000000..1ce868bf --- /dev/null +++ b/public/res/ic/outlined/eye.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/res/ic/outlined/file.svg b/public/res/ic/outlined/file.svg new file mode 100644 index 00000000..d6a2a27a --- /dev/null +++ b/public/res/ic/outlined/file.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/flag.svg b/public/res/ic/outlined/flag.svg new file mode 100644 index 00000000..8fce98d6 --- /dev/null +++ b/public/res/ic/outlined/flag.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/hash-globe.svg b/public/res/ic/outlined/hash-globe.svg new file mode 100644 index 00000000..ce3df083 --- /dev/null +++ b/public/res/ic/outlined/hash-globe.svg @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/hash-lock.svg b/public/res/ic/outlined/hash-lock.svg new file mode 100644 index 00000000..ae263ced --- /dev/null +++ b/public/res/ic/outlined/hash-lock.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/hash-plus.svg b/public/res/ic/outlined/hash-plus.svg new file mode 100644 index 00000000..69737fd5 --- /dev/null +++ b/public/res/ic/outlined/hash-plus.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/hash-search.svg b/public/res/ic/outlined/hash-search.svg new file mode 100644 index 00000000..f135e898 --- /dev/null +++ b/public/res/ic/outlined/hash-search.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/hash-shield.svg b/public/res/ic/outlined/hash-shield.svg new file mode 100644 index 00000000..dfd344b1 --- /dev/null +++ b/public/res/ic/outlined/hash-shield.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/hash.svg b/public/res/ic/outlined/hash.svg new file mode 100644 index 00000000..dcb8b964 --- /dev/null +++ b/public/res/ic/outlined/hash.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/heart.svg b/public/res/ic/outlined/heart.svg new file mode 100644 index 00000000..c5b940b6 --- /dev/null +++ b/public/res/ic/outlined/heart.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/public/res/ic/outlined/home.svg b/public/res/ic/outlined/home.svg new file mode 100644 index 00000000..3c7a02df --- /dev/null +++ b/public/res/ic/outlined/home.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/horizontal-menu.svg b/public/res/ic/outlined/horizontal-menu.svg new file mode 100644 index 00000000..a19b3c35 --- /dev/null +++ b/public/res/ic/outlined/horizontal-menu.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/inbox.svg b/public/res/ic/outlined/inbox.svg new file mode 100644 index 00000000..65435876 --- /dev/null +++ b/public/res/ic/outlined/inbox.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/public/res/ic/outlined/info.svg b/public/res/ic/outlined/info.svg new file mode 100644 index 00000000..30a57887 --- /dev/null +++ b/public/res/ic/outlined/info.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/public/res/ic/outlined/invite-arrow.svg b/public/res/ic/outlined/invite-arrow.svg new file mode 100644 index 00000000..370bf8e8 --- /dev/null +++ b/public/res/ic/outlined/invite-arrow.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/invite-cancel-arrow.svg b/public/res/ic/outlined/invite-cancel-arrow.svg new file mode 100644 index 00000000..795a773a --- /dev/null +++ b/public/res/ic/outlined/invite-cancel-arrow.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/invite.svg b/public/res/ic/outlined/invite.svg new file mode 100644 index 00000000..3896e15e --- /dev/null +++ b/public/res/ic/outlined/invite.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/join-arrow.svg b/public/res/ic/outlined/join-arrow.svg new file mode 100644 index 00000000..90cfa651 --- /dev/null +++ b/public/res/ic/outlined/join-arrow.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/leave-arrow.svg b/public/res/ic/outlined/leave-arrow.svg new file mode 100644 index 00000000..a51ac1d1 --- /dev/null +++ b/public/res/ic/outlined/leave-arrow.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/lock.svg b/public/res/ic/outlined/lock.svg new file mode 100644 index 00000000..77021f0f --- /dev/null +++ b/public/res/ic/outlined/lock.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/markdown.svg b/public/res/ic/outlined/markdown.svg new file mode 100644 index 00000000..775afbfb --- /dev/null +++ b/public/res/ic/outlined/markdown.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/message-unread.svg b/public/res/ic/outlined/message-unread.svg new file mode 100644 index 00000000..fc5e9ff0 --- /dev/null +++ b/public/res/ic/outlined/message-unread.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/public/res/ic/outlined/message.svg b/public/res/ic/outlined/message.svg new file mode 100644 index 00000000..d36e9a30 --- /dev/null +++ b/public/res/ic/outlined/message.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/pause.svg b/public/res/ic/outlined/pause.svg new file mode 100644 index 00000000..c312613b --- /dev/null +++ b/public/res/ic/outlined/pause.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/public/res/ic/outlined/peace.svg b/public/res/ic/outlined/peace.svg new file mode 100644 index 00000000..8a7c81a3 --- /dev/null +++ b/public/res/ic/outlined/peace.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/public/res/ic/outlined/pencil.svg b/public/res/ic/outlined/pencil.svg new file mode 100644 index 00000000..1b8ac24a --- /dev/null +++ b/public/res/ic/outlined/pencil.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/photo.svg b/public/res/ic/outlined/photo.svg new file mode 100644 index 00000000..af01a330 --- /dev/null +++ b/public/res/ic/outlined/photo.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/pin.svg b/public/res/ic/outlined/pin.svg new file mode 100644 index 00000000..211242cd --- /dev/null +++ b/public/res/ic/outlined/pin.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/play.svg b/public/res/ic/outlined/play.svg new file mode 100644 index 00000000..87b3a8f6 --- /dev/null +++ b/public/res/ic/outlined/play.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/plus.svg b/public/res/ic/outlined/plus.svg new file mode 100644 index 00000000..ce37594e --- /dev/null +++ b/public/res/ic/outlined/plus.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/power.svg b/public/res/ic/outlined/power.svg new file mode 100644 index 00000000..8aeb6db8 --- /dev/null +++ b/public/res/ic/outlined/power.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/recent-clock.svg b/public/res/ic/outlined/recent-clock.svg new file mode 100644 index 00000000..30b10d59 --- /dev/null +++ b/public/res/ic/outlined/recent-clock.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/public/res/ic/outlined/reply-arrow.svg b/public/res/ic/outlined/reply-arrow.svg new file mode 100644 index 00000000..3cda01cd --- /dev/null +++ b/public/res/ic/outlined/reply-arrow.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/search.svg b/public/res/ic/outlined/search.svg new file mode 100644 index 00000000..75dd6320 --- /dev/null +++ b/public/res/ic/outlined/search.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/send.svg b/public/res/ic/outlined/send.svg new file mode 100644 index 00000000..aa487132 --- /dev/null +++ b/public/res/ic/outlined/send.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/settings.svg b/public/res/ic/outlined/settings.svg new file mode 100644 index 00000000..ee640b39 --- /dev/null +++ b/public/res/ic/outlined/settings.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/shield-empty.svg b/public/res/ic/outlined/shield-empty.svg new file mode 100644 index 00000000..6bc9d304 --- /dev/null +++ b/public/res/ic/outlined/shield-empty.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/res/ic/outlined/shield-user.svg b/public/res/ic/outlined/shield-user.svg new file mode 100644 index 00000000..bd5f07c5 --- /dev/null +++ b/public/res/ic/outlined/shield-user.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/shield.svg b/public/res/ic/outlined/shield.svg new file mode 100644 index 00000000..9bb46fa1 --- /dev/null +++ b/public/res/ic/outlined/shield.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/space-globe.svg b/public/res/ic/outlined/space-globe.svg new file mode 100644 index 00000000..63d71f1d --- /dev/null +++ b/public/res/ic/outlined/space-globe.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/space-lock.svg b/public/res/ic/outlined/space-lock.svg new file mode 100644 index 00000000..b15705ca --- /dev/null +++ b/public/res/ic/outlined/space-lock.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/space-plus.svg b/public/res/ic/outlined/space-plus.svg new file mode 100644 index 00000000..4d69a1ef --- /dev/null +++ b/public/res/ic/outlined/space-plus.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/space.svg b/public/res/ic/outlined/space.svg new file mode 100644 index 00000000..a4b54b3e --- /dev/null +++ b/public/res/ic/outlined/space.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/public/res/ic/outlined/star.svg b/public/res/ic/outlined/star.svg new file mode 100644 index 00000000..290f159a --- /dev/null +++ b/public/res/ic/outlined/star.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/sticker.svg b/public/res/ic/outlined/sticker.svg new file mode 100644 index 00000000..bc486e5e --- /dev/null +++ b/public/res/ic/outlined/sticker.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/res/ic/outlined/sun.svg b/public/res/ic/outlined/sun.svg new file mode 100644 index 00000000..d8ed06fd --- /dev/null +++ b/public/res/ic/outlined/sun.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/res/ic/outlined/tick-mark.svg b/public/res/ic/outlined/tick-mark.svg new file mode 100644 index 00000000..8e76ed55 --- /dev/null +++ b/public/res/ic/outlined/tick-mark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/user.svg b/public/res/ic/outlined/user.svg new file mode 100644 index 00000000..6756a1b2 --- /dev/null +++ b/public/res/ic/outlined/user.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/public/res/ic/outlined/vertical-menu.svg b/public/res/ic/outlined/vertical-menu.svg new file mode 100644 index 00000000..ec5c544c --- /dev/null +++ b/public/res/ic/outlined/vertical-menu.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/vlc.svg b/public/res/ic/outlined/vlc.svg new file mode 100644 index 00000000..8a2b844f --- /dev/null +++ b/public/res/ic/outlined/vlc.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/volume-full.svg b/public/res/ic/outlined/volume-full.svg new file mode 100644 index 00000000..20419e72 --- /dev/null +++ b/public/res/ic/outlined/volume-full.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/public/res/ic/outlined/volume-mute.svg b/public/res/ic/outlined/volume-mute.svg new file mode 100644 index 00000000..beb06771 --- /dev/null +++ b/public/res/ic/outlined/volume-mute.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/app/atoms/avatar/Avatar.jsx b/src/app/atoms/avatar/Avatar.jsx new file mode 100644 index 00000000..27bf7c90 --- /dev/null +++ b/src/app/atoms/avatar/Avatar.jsx @@ -0,0 +1,69 @@ +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 new file mode 100644 index 00000000..ea69c9e8 --- /dev/null +++ b/src/app/atoms/avatar/Avatar.scss @@ -0,0 +1,56 @@ +@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 new file mode 100644 index 00000000..e8cf1a66 --- /dev/null +++ b/src/app/atoms/avatar/render.js @@ -0,0 +1,57 @@ +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 new file mode 100644 index 00000000..12c1bd44 --- /dev/null +++ b/src/app/atoms/badge/NotificationBadge.jsx @@ -0,0 +1,29 @@ +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 new file mode 100644 index 00000000..f5cfa73f --- /dev/null +++ b/src/app/atoms/badge/NotificationBadge.scss @@ -0,0 +1,21 @@ +.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 new file mode 100644 index 00000000..1c1c950c --- /dev/null +++ b/src/app/atoms/button/Button.jsx @@ -0,0 +1,53 @@ +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 new file mode 100644 index 00000000..e1a01bb0 --- /dev/null +++ b/src/app/atoms/button/Button.scss @@ -0,0 +1,81 @@ +@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 new file mode 100644 index 00000000..7fcea3b5 --- /dev/null +++ b/src/app/atoms/button/Checkbox.jsx @@ -0,0 +1,39 @@ +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 new file mode 100644 index 00000000..aa6480c0 --- /dev/null +++ b/src/app/atoms/button/IconButton.scss @@ -0,0 +1,56 @@ +@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 new file mode 100644 index 00000000..35b68baf --- /dev/null +++ b/src/app/atoms/button/RadioButton.jsx @@ -0,0 +1,30 @@ +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 new file mode 100644 index 00000000..7396b0dc --- /dev/null +++ b/src/app/atoms/chip/Chip.scss @@ -0,0 +1,31 @@ +@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 new file mode 100644 index 00000000..7d1acd44 --- /dev/null +++ b/src/app/atoms/context-menu/ContextMenu.jsx @@ -0,0 +1,115 @@ +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 new file mode 100644 index 00000000..2df9f0a4 --- /dev/null +++ b/src/app/atoms/context-menu/ContextMenu.scss @@ -0,0 +1,81 @@ +@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 new file mode 100644 index 00000000..59bdb142 --- /dev/null +++ b/src/app/atoms/context-menu/ReusableContextMenu.jsx @@ -0,0 +1,87 @@ +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 new file mode 100644 index 00000000..76721241 --- /dev/null +++ b/src/app/atoms/divider/Divider.jsx @@ -0,0 +1,28 @@ +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 new file mode 100644 index 00000000..0f013ff0 --- /dev/null +++ b/src/app/atoms/divider/Divider.scss @@ -0,0 +1,68 @@ +.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 new file mode 100644 index 00000000..3c81e423 --- /dev/null +++ b/src/app/atoms/header/Header.jsx @@ -0,0 +1,29 @@ +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 new file mode 100644 index 00000000..9e45f824 --- /dev/null +++ b/src/app/atoms/header/Header.scss @@ -0,0 +1,43 @@ +@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 new file mode 100644 index 00000000..96c94967 --- /dev/null +++ b/src/app/atoms/input/Input.jsx @@ -0,0 +1,97 @@ +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 new file mode 100644 index 00000000..40fe43ec --- /dev/null +++ b/src/app/atoms/input/Input.scss @@ -0,0 +1,52 @@ +@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 new file mode 100644 index 00000000..450be0e0 --- /dev/null +++ b/src/app/atoms/modal/RawModal.jsx @@ -0,0 +1,73 @@ +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 new file mode 100644 index 00000000..d612a4bc --- /dev/null +++ b/src/app/atoms/modal/RawModal.scss @@ -0,0 +1,66 @@ +.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 new file mode 100644 index 00000000..26c0c83a --- /dev/null +++ b/src/app/atoms/scroll/ScrollView.jsx @@ -0,0 +1,37 @@ +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 new file mode 100644 index 00000000..251037e1 --- /dev/null +++ b/src/app/atoms/scroll/ScrollView.scss @@ -0,0 +1,31 @@ +@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 new file mode 100644 index 00000000..78ec75ad --- /dev/null +++ b/src/app/atoms/scroll/_scrollbar.scss @@ -0,0 +1,65 @@ +.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 new file mode 100644 index 00000000..1d54dd06 --- /dev/null +++ b/src/app/atoms/segmented-controls/SegmentedControls.jsx @@ -0,0 +1,55 @@ +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 new file mode 100644 index 00000000..fb1fd987 --- /dev/null +++ b/src/app/atoms/segmented-controls/SegmentedControls.scss @@ -0,0 +1,57 @@ +@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 new file mode 100644 index 00000000..61c9747e --- /dev/null +++ b/src/app/atoms/spinner/Spinner.jsx @@ -0,0 +1,19 @@ +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 new file mode 100644 index 00000000..73fbf676 --- /dev/null +++ b/src/app/atoms/spinner/Spinner.scss @@ -0,0 +1,22 @@ +.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 new file mode 100644 index 00000000..a6697f4f --- /dev/null +++ b/src/app/atoms/system-icons/RawIcon.jsx @@ -0,0 +1,32 @@ +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 new file mode 100644 index 00000000..56fc9b3c --- /dev/null +++ b/src/app/atoms/system-icons/RawIcon.scss @@ -0,0 +1,28 @@ +@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 new file mode 100644 index 00000000..bcdc8ef7 --- /dev/null +++ b/src/app/atoms/tabs/Tabs.jsx @@ -0,0 +1,88 @@ +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 new file mode 100644 index 00000000..39ddddec --- /dev/null +++ b/src/app/atoms/tabs/Tabs.scss @@ -0,0 +1,45 @@ +@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 new file mode 100644 index 00000000..3f507ee3 --- /dev/null +++ b/src/app/atoms/text/Text.jsx @@ -0,0 +1,42 @@ +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 new file mode 100644 index 00000000..256bf6ea --- /dev/null +++ b/src/app/atoms/text/Text.scss @@ -0,0 +1,61 @@ +@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 new file mode 100644 index 00000000..750b958f --- /dev/null +++ b/src/app/atoms/time/Time.jsx @@ -0,0 +1,44 @@ +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 new file mode 100644 index 00000000..0f9067f5 --- /dev/null +++ b/src/app/atoms/tooltip/Tooltip.jsx @@ -0,0 +1,39 @@ +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 new file mode 100644 index 00000000..f609aa58 --- /dev/null +++ b/src/app/atoms/tooltip/Tooltip.scss @@ -0,0 +1,10 @@ +.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 new file mode 100644 index 00000000..574d0ca7 --- /dev/null +++ b/src/app/components/CapabilitiesAndMediaConfigLoader.tsx @@ -0,0 +1,36 @@ +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 433fa6a1..fb09c150 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/secretStorageKeys'; +import { clearSecretStorageKeys } from '../../client/state/secretStorageKeys'; import { ActionUIA, ActionUIAFlowsLoader } from './ActionUIA'; import { useMatrixClient } from '../hooks/useMatrixClient'; import { useAlive } from '../hooks/useAlive'; diff --git a/src/app/components/JoinRulesSwitcher.tsx b/src/app/components/JoinRulesSwitcher.tsx index 9507317a..e78c19ce 100644 --- a/src/app/components/JoinRulesSwitcher.tsx +++ b/src/app/components/JoinRulesSwitcher.tsx @@ -17,16 +17,12 @@ import { JoinRule } from 'matrix-js-sdk'; import FocusTrap from 'focus-trap-react'; import { stopPropagation } from '../utils/keyboard'; -export type ExtraJoinRules = 'knock_restricted'; -export type ExtendedJoinRules = JoinRule | ExtraJoinRules; - -type JoinRuleIcons = Record; +type JoinRuleIcons = Record; export const useRoomJoinRuleIcon = (): JoinRuleIcons => useMemo( () => ({ [JoinRule.Invite]: Icons.HashLock, [JoinRule.Knock]: Icons.HashLock, - knock_restricted: Icons.Hash, [JoinRule.Restricted]: Icons.Hash, [JoinRule.Public]: Icons.HashGlobe, [JoinRule.Private]: Icons.HashLock, @@ -38,7 +34,6 @@ export const useSpaceJoinRuleIcon = (): JoinRuleIcons => () => ({ [JoinRule.Invite]: Icons.SpaceLock, [JoinRule.Knock]: Icons.SpaceLock, - knock_restricted: Icons.Space, [JoinRule.Restricted]: Icons.Space, [JoinRule.Public]: Icons.SpaceGlobe, [JoinRule.Private]: Icons.SpaceLock, @@ -46,13 +41,12 @@ export const useSpaceJoinRuleIcon = (): JoinRuleIcons => [] ); -type JoinRuleLabels = Record; +type JoinRuleLabels = Record; export const useRoomJoinRuleLabel = (): JoinRuleLabels => useMemo( () => ({ [JoinRule.Invite]: 'Invite Only', [JoinRule.Knock]: 'Knock & Invite', - knock_restricted: 'Space Members or Knock', [JoinRule.Restricted]: 'Space Members', [JoinRule.Public]: 'Public', [JoinRule.Private]: 'Invite Only', @@ -60,7 +54,7 @@ export const useRoomJoinRuleLabel = (): JoinRuleLabels => [] ); -type JoinRulesSwitcherProps = { +type JoinRulesSwitcherProps = { icons: JoinRuleIcons; labels: JoinRuleLabels; rules: T; @@ -69,7 +63,7 @@ type JoinRulesSwitcherProps = { disabled?: boolean; changing?: boolean; }; -export function JoinRulesSwitcher({ +export function JoinRulesSwitcher({ icons, labels, rules, @@ -85,7 +79,7 @@ export function JoinRulesSwitcher({ }; const handleChange = useCallback( - (selectedRule: ExtendedJoinRules) => { + (selectedRule: JoinRule) => { setCords(undefined); onChange(selectedRule); }, @@ -137,7 +131,7 @@ export function JoinRulesSwitcher({ fill="Soft" radii="300" outlined - before={} + before={} after={ changing ? ( @@ -148,7 +142,7 @@ export function JoinRulesSwitcher({ onClick={handleOpenMenu} disabled={disabled} > - {labels[value] ?? 'Unsupported'} + {labels[value]} ); diff --git a/src/app/components/ManualVerification.tsx b/src/app/components/ManualVerification.tsx index f7cde92b..03f3e710 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/secretStorageKeys'; +import { storePrivateKey } from '../../client/state/secretStorageKeys'; export enum ManualVerificationMethod { RecoveryPassphrase = 'passphrase', diff --git a/src/app/components/SecretStorage.tsx b/src/app/components/SecretStorage.tsx index 9d8628e5..55d466d7 100644 --- a/src/app/components/SecretStorage.tsx +++ b/src/app/components/SecretStorage.tsx @@ -1,6 +1,7 @@ import React, { FormEventHandler, useCallback } from 'react'; import { Box, Text, Button, Spinner, color } from 'folds'; -import { decodeRecoveryKey, deriveRecoveryKeyFromPassphrase } from 'matrix-js-sdk/lib/crypto-api'; +import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto-api'; +import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase'; import { PasswordInput } from './password-input'; import { SecretStorageKeyContent, @@ -28,16 +29,11 @@ export function SecretStorageRecoveryPassphrase({ const [driveKeyState, submitPassphrase] = useAsyncCallback< Uint8Array, Error, - Parameters + Parameters >( useCallback( async (passphrase, salt, iterations, bits) => { - const decodedRecoveryKey = await deriveRecoveryKeyFromPassphrase( - passphrase, - salt, - iterations, - bits - ); + const decodedRecoveryKey = await deriveKey(passphrase, salt, iterations, bits); const match = await mx.secretStorage.checkKey(decodedRecoveryKey, keyContent as any); diff --git a/src/app/components/ServerConfigsLoader.tsx b/src/app/components/ServerConfigsLoader.tsx deleted file mode 100644 index 3c8ce8eb..00000000 --- a/src/app/components/ServerConfigsLoader.tsx +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index ca7aa837..00000000 --- a/src/app/components/UserRoomProfileRenderer.tsx +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index 84b92231..00000000 --- a/src/app/components/create-room/AdditionalCreatorInput.tsx +++ /dev/null @@ -1,294 +0,0 @@ -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 deleted file mode 100644 index e84658c0..00000000 --- a/src/app/components/create-room/CreateRoomAliasInput.tsx +++ /dev/null @@ -1,118 +0,0 @@ -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 deleted file mode 100644 index 096954fb..00000000 --- a/src/app/components/create-room/CreateRoomKindSelector.tsx +++ /dev/null @@ -1,94 +0,0 @@ -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 deleted file mode 100644 index 219ded0c..00000000 --- a/src/app/components/create-room/RoomVersionSelector.tsx +++ /dev/null @@ -1,117 +0,0 @@ -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 deleted file mode 100644 index ffd9cb3d..00000000 --- a/src/app/components/create-room/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index a0ca7488..00000000 --- a/src/app/components/create-room/utils.ts +++ /dev/null @@ -1,140 +0,0 @@ -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 d128ed07..09a444ec 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 EditorPlaceholderContainer = style([ +export const EditorPlaceholder = style([ DefaultReset, { + position: 'absolute', + zIndex: 1, + width: '100%', opacity: config.opacity.Placeholder, pointerEvents: 'none', userSelect: 'none', - }, -]); -export const EditorPlaceholderTextVisual = style([ - DefaultReset, - { - display: 'block', - paddingTop: toRem(13), - paddingLeft: toRem(1), + selectors: { + '&:not(:first-child)': { + display: 'none', + }, + }, }, ]); diff --git a/src/app/components/editor/Editor.tsx b/src/app/components/editor/Editor.tsx index bd848dd5..044d0837 100644 --- a/src/app/components/editor/Editor.tsx +++ b/src/app/components/editor/Editor.tsx @@ -106,17 +106,22 @@ export const CustomEditor = forwardRef( [editor, onKeyDown] ); - const renderPlaceholder = useCallback( - ({ attributes, children }: RenderPlaceholderProps) => ( - - {/* Inner component to style the actual text position and appearance */} - - {children} - - - ), - [] - ); + 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} + + ); + }, []); return (
diff --git a/src/app/components/editor/Elements.tsx b/src/app/components/editor/Elements.tsx index 675c4542..a7438ecd 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 b0c64f60..cc431f58 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, isRoomAlias } from '../../../utils/matrix'; +import { getMxIdServer, validMxId } 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) => - isRoomAlias(`#${text}`) + validMxId(`#${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 7a8012eb..d6c0f302 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, isUserId } from '../../../utils/matrix'; +import { getMxIdLocalPart, getMxIdServer, validMxId } 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) => - isUserId(`@${text}`) + validMxId(`@${text}`) ? `@${text}` : `@${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`; diff --git a/src/app/components/event-readers/EventReaders.tsx b/src/app/components/event-readers/EventReaders.tsx index c7900237..de1416b6 100644 --- a/src/app/components/event-readers/EventReaders.tsx +++ b/src/app/components/event-readers/EventReaders.tsx @@ -19,11 +19,9 @@ 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; @@ -35,8 +33,6 @@ 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; @@ -61,32 +57,19 @@ 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 ( { - openProfile( - room.roomId, - space?.roomId, - readerId, - getMouseEventCords(event.nativeEvent), - 'Bottom' - ); + onClick={() => { + requestClose(); + openProfileViewer(readerId, room.roomId); }} before={ diff --git a/src/app/components/image-pack-view/RoomImagePack.tsx b/src/app/components/image-pack-view/RoomImagePack.tsx index 92b4ff21..9dd45c1f 100644 --- a/src/app/components/image-pack-view/RoomImagePack.tsx +++ b/src/app/components/image-pack-view/RoomImagePack.tsx @@ -1,14 +1,12 @@ import React, { useCallback, useMemo } from 'react'; import { Room } from 'matrix-js-sdk'; -import { usePowerLevels } from '../../hooks/usePowerLevels'; +import { usePowerLevels, usePowerLevelsAPI } 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; @@ -19,10 +17,9 @@ export function RoomImagePack({ room, stateKey }: RoomImagePackProps) { const mx = useMatrixClient(); const userId = mx.getUserId()!; const powerLevels = usePowerLevels(room); - const creators = useRoomCreators(room); - const permissions = useRoomPermissions(creators, powerLevels); - const canEditImagePack = permissions.stateEvent(StateEvent.PoniesRoomEmotes, userId); + const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); + const canEditImagePack = canSendStateEvent(StateEvent.PoniesRoomEmotes, getPowerLevel(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 deleted file mode 100644 index ddac0576..00000000 --- a/src/app/components/invite-user-prompt/InviteUserPrompt.tsx +++ /dev/null @@ -1,291 +0,0 @@ -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) -